diff --git a/.gitignore b/.gitignore index 40b14f0..3ae55d6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # IDE .idea/ + +# 语法门包裹检查的临时写区(lib/check-workflow-syntax.mjs) +.tmp/ diff --git a/lib/check-workflow-syntax.mjs b/lib/check-workflow-syntax.mjs new file mode 100644 index 0000000..5910f7d --- /dev/null +++ b/lib/check-workflow-syntax.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +// check-workflow-syntax.mjs — Workflow 脚本的确定性语法门(附录补 18)。 +// +// 为什么不能直接 `node --check workflows/coding.mjs`:coding.mjs 是 Claude Workflow 运行时脚本, +// 运行时会把脚本体包进 async function 后执行(顶层 return 是结果通道)。node v26 见到 +// `export const meta` 即按 ESM 解析,顶层 return 必报 `Illegal return statement`——基线恒红。 +// 恒红的语法门会让人习惯性忽略红灯(掩盖真语法错),故此处复刻运行时的包裹再 --check。 +// +// 用法:node lib/check-workflow-syntax.mjs [workflow 文件路径...](缺省 workflows/coding.mjs) +// 退出码:透传 node --check(0=语法通过)。 +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' +import { spawnSync } from 'node:child_process' +import { basename, dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const targets = process.argv.slice(2).length ? process.argv.slice(2) : [join(repoRoot, 'workflows', 'coding.mjs')] + +let failed = false +for (const target of targets) { + const src = readFileSync(resolve(target), 'utf8') + // 剥行首 `export `(运行时注入 meta,不走 ESM export),再包进运行时同形的 async 包裹。 + const wrapped = [ + 'async function __wf(args, agent, parallel, phase, log) {', + src.replace(/^export /gm, ''), + '}', + ].join('\n') + // 临时文件名由目标 basename 确定性导出(无时间戳/随机数),落在仓库 .tmp/ 下。 + const tmpDir = join(repoRoot, '.tmp', 'check-workflow-syntax') + mkdirSync(tmpDir, { recursive: true }) + const tmpFile = join(tmpDir, `${basename(target)}.wrapped.mjs`) + writeFileSync(tmpFile, wrapped) + const r = spawnSync(process.execPath, ['--check', tmpFile], { stdio: 'inherit' }) + if (r.status !== 0) { + failed = true + console.error(`✗ 语法检查失败:${target}(包裹文件 ${tmpFile})`) + } else { + console.log(`✓ 语法通过:${target}`) + } +} +process.exit(failed ? 1 : 0)