/** * Script preprocessor for Davis-style workflow scripts. * * Extracts `export const meta = { name, description, phases }` from * the script and prepares it for execution in the sandbox child. * * Uses acorn for AST-based extraction. The actual execution happens * in the sandbox child process (not here). */ import { parse } from "acorn"; import type { WorkflowMeta } from "./types.ts"; // ── Public API ────────────────────────────────────────────────────────────────── export interface ExtractMetaResult { success: boolean; meta?: WorkflowMeta; error?: string; } /** * Extract the `meta` export from a Davis-style workflow script. * * Parses `export const meta = { name: "...", description: "...", phases: [...] }` * and returns the extracted metadata. */ export function extractMeta(source: string): ExtractMetaResult { const trimmed = source.trim(); if (!trimmed) { return { success: false, error: "Workflow script is empty." }; } let ast: any; try { ast = parse(trimmed, { ecmaVersion: "latest", sourceType: "module", locations: true, }); } catch (err: unknown) { // Retry with script sourceType try { ast = parse(trimmed, { ecmaVersion: "latest", sourceType: "script", locations: true, }); } catch (err2: unknown) { const message = err2 instanceof SyntaxError ? err2.message : String(err2); return { success: false, error: `Syntax error: ${message}` }; } } try { const body = ast.body; if (!Array.isArray(body)) { return { success: false, error: "Could not parse script body." }; } for (const stmt of body) { // export const meta = { ... } if (stmt.type === "ExportNamedDeclaration") { const decl = stmt.declaration; if ( decl && decl.type === "VariableDeclaration" && Array.isArray(decl.declarations) ) { for (const d of decl.declarations) { if ( d.id?.type === "Identifier" && d.id.name === "meta" && d.init?.type === "ObjectExpression" ) { const meta = parseMetaObject(d.init); if (meta) { return { success: true, meta }; } } } } } } return { success: false, error: "No `export const meta = {...}` found in script." }; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); return { success: false, error: `Parse error: ${message}` }; } } /** * Check if a script is a valid workflow script (has export const meta). */ export function isValidWorkflowScript(source: string): boolean { const result = extractMeta(source); return result.success; } // ── AST Helpers ───────────────────────────────────────────────────────────────── function parseMetaObject(node: any): WorkflowMeta | null { if (node.type !== "ObjectExpression" || !Array.isArray(node.properties)) { return null; } const meta: WorkflowMeta = { name: "" }; for (const prop of node.properties) { if (prop.type !== "Property") continue; const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? String(prop.key.value) : null; if (!key) continue; const value = extractLiteralValue(prop.value); if (key === "name" && typeof value === "string") { meta.name = value; } else if (key === "description" && typeof value === "string") { meta.description = value; } else if (key === "phases" && Array.isArray(value)) { meta.phases = value.filter((v): v is string => typeof v === "string"); } } if (!meta.name) return null; return meta; } function extractLiteralValue(node: any): unknown { if (!node) return undefined; if (node.type === "Literal") { return node.value; } if (node.type === "ArrayExpression" && Array.isArray(node.elements)) { return node.elements .map((el: any) => (el ? extractLiteralValue(el) : null)) .filter(Boolean); } if (node.type === "ObjectExpression") { const obj: Record = {}; if (Array.isArray(node.properties)) { for (const prop of node.properties) { if (prop.type !== "Property") continue; const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? String(prop.key.value) : null; if (key) { obj[key] = extractLiteralValue(prop.value); } } } return obj; } if (node.type === "TemplateLiteral" && Array.isArray(node.quasis)) { return node.quasis.map((q: any) => q.value?.cooked ?? "").join(""); } if (node.type === "UnaryExpression" && node.operator === "-") { const val = extractLiteralValue(node.argument); if (typeof val === "number") return -val; } return undefined; } // Re-exported to keep the module's public surface export { extractMeta as parseWorkflowScript };