@michaelxwb/code-flow 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapters/claude/commands/cf-task/archive.md +5 -5
- package/src/adapters/claude/commands/cf-task/block.md +15 -9
- package/src/adapters/claude/commands/cf-task/graph.md +8 -1
- package/src/adapters/claude/commands/cf-task/note.md +1 -4
- package/src/adapters/claude/commands/cf-task/start.md +18 -17
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/claude/commands/cf-validate.md +12 -13
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +5 -5
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -9
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +8 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +1 -4
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +18 -17
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +12 -13
- package/src/adapters/costrict/commands/cf-task/archive.md +5 -5
- package/src/adapters/costrict/commands/cf-task/block.md +15 -9
- package/src/adapters/costrict/commands/cf-task/graph.md +8 -1
- package/src/adapters/costrict/commands/cf-task/note.md +1 -4
- package/src/adapters/costrict/commands/cf-task/start.md +18 -17
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/costrict/commands/cf-validate.md +12 -13
- package/src/adapters/opencode/commands/cf-task/archive.md +5 -5
- package/src/adapters/opencode/commands/cf-task/block.md +15 -9
- package/src/adapters/opencode/commands/cf-task/graph.md +8 -1
- package/src/adapters/opencode/commands/cf-task/note.md +1 -4
- package/src/adapters/opencode/commands/cf-task/start.md +18 -17
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/opencode/commands/cf-validate.md +12 -13
- package/src/adapters/opencode/plugins/code-flow/index.js +50 -28
- package/src/cli.js +177 -63
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +85 -36
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +123 -38
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
- package/src/core/code-flow/scripts/cf_core.py +6 -0
- package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +8 -4
- package/src/core/code-flow/scripts/cf_scan.py +8 -6
- package/src/core/code-flow/scripts/cf_spec_context.py +178 -54
- package/src/core/code-flow/scripts/cf_spec_resolver.py +7 -2
- package/src/core/code-flow/scripts/cf_spec_router.py +2 -0
- package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
- package/src/core/code-flow/scripts/cf_spec_verify.py +42 -31
- package/src/core/code-flow/scripts/cf_stats.py +41 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +98 -59
- package/src/core/code-flow/scripts/cf_task_index.py +147 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +62 -27
- package/src/core/code-flow/scripts/cf_task_state.py +3 -0
- package/src/core/code-flow/scripts/cf_task_workflow.py +107 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +9 -5
- package/src/core/code-flow/scripts/cf_validation.py +81 -0
- package/src/core/code-flow/scripts/cf_workflow_service.py +366 -0
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +96 -0
|
@@ -8,7 +8,7 @@ description: 根据变更文件自动匹配并执行验证规则
|
|
|
8
8
|
|
|
9
9
|
## 输入
|
|
10
10
|
|
|
11
|
-
- `/project:cf-validate` —
|
|
11
|
+
- `/project:cf-validate` — 自动获取任务基线或 Git 全部变更文件
|
|
12
12
|
- `/project:cf-validate src/Foo.tsx` — 验证指定文件
|
|
13
13
|
- `/project:cf-validate --files=src/a.ts,src/b.ts` — 验证多个文件
|
|
14
14
|
|
|
@@ -19,10 +19,10 @@ description: 根据变更文件自动匹配并执行验证规则
|
|
|
19
19
|
用 Bash 执行:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
python3 .code-flow/scripts/cf_validation.py --root "$PWD" --scope-only --json
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
有 active TASK 时使用冻结基线以来的全部任务变更(含已提交文件);无 active TASK 时包含暂存、未暂存和未跟踪文件。用户指定路径时用 `--files "src/a.py" "src/b.py"` 原样传递;若结果确实为空才输出“无变更需要验证”。
|
|
26
26
|
|
|
27
27
|
### 2. 读取验证规则
|
|
28
28
|
|
|
@@ -41,14 +41,13 @@ validators:
|
|
|
41
41
|
|
|
42
42
|
### 3. 匹配并执行验证
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
- 将 `trigger` glob 与变更文件列表做匹配
|
|
46
|
-
- 匹配到 → 用 Bash 执行 `command`
|
|
47
|
-
- 如果 command 包含 `{files}` → 替换为匹配到的文件路径列表,**每个路径用单引号包裹**(如 `'src/foo.ts' 'src/bar.ts'`)
|
|
48
|
-
- 如果 command 不含 `{files}` → 直接执行原命令(如 `npx tsc --noEmit` 本身就检查全局)
|
|
49
|
-
- 未匹配 → 跳过该规则
|
|
44
|
+
执行统一验证入口:
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
```bash
|
|
47
|
+
python3 .code-flow/scripts/cf_validation.py --root "$PWD" --json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
显式范围追加 `--files "src/a.py" "src/b.py"`。程序匹配 trigger、解析 argv 并展开 `{files}`,不拼接 shell 命令;同一运行内相同 argv/cwd/timeout 去重,跨运行不复用。删除文件仍触发全局测试,但不传给文件级编译器。每条 timeout 仍按配置的毫秒值生效,总预算耗尽返回 incomplete/block。
|
|
52
51
|
|
|
53
52
|
### 4. 汇总结果
|
|
54
53
|
|
|
@@ -65,13 +64,13 @@ validators:
|
|
|
65
64
|
|
|
66
65
|
## 安全设计
|
|
67
66
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
67
|
+
- `{files}` 直接展开为 argv 参数;带空格、引号的路径仍是一个参数,不手工拼接引号或执行 shell
|
|
68
|
+
- 范围来自任务基线、Git 状态(包括 untracked)或用户显式指定的路径
|
|
70
69
|
- 用户指定的路径必须位于项目根目录内
|
|
71
70
|
|
|
72
71
|
## 异常处理
|
|
73
72
|
|
|
74
73
|
- validation.yml 不存在 → 尝试检测 package.json scripts 中的 test/lint 命令
|
|
75
74
|
- 命令执行超时 → 输出超时提示,建议增大 timeout 或缩小验证范围
|
|
76
|
-
-
|
|
75
|
+
- 命令不存在或配置无效 → 返回 block 并提示所缺依赖,不当作验证通过
|
|
77
76
|
- 无变更文件 → 输出"无变更需要验证"
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { appendFileSync, mkdirSync } from "fs";
|
|
4
4
|
|
|
5
5
|
const SCRIPT_DIR = ".code-flow/scripts";
|
|
6
6
|
|
|
7
|
-
// Per-session
|
|
7
|
+
// Per-session queued feedback. Single map, append-only: idle/stop-check and
|
|
8
|
+
// post-check feedback accumulate here and are consumed by the next
|
|
9
|
+
// system.transform. Never overwrite — chat.message arriving between idle and
|
|
10
|
+
// transform must not drop queued stop-check feedback.
|
|
8
11
|
const sessionContext = new Map();
|
|
9
12
|
|
|
13
|
+
export function mergePending(existing, incoming) {
|
|
14
|
+
if (!existing) return incoming;
|
|
15
|
+
if (!incoming) return existing;
|
|
16
|
+
return existing + "\n\n" + incoming;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
function debugLog(projectRoot, msg) {
|
|
11
20
|
if (process.env.CF_DEBUG !== "1") return;
|
|
12
21
|
try {
|
|
@@ -22,25 +31,39 @@ function pythonPath(projectRoot, script) {
|
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
function callHook(projectRoot, script, input, timeout = 5000) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Async child process: never block the plugin event thread. opencode's
|
|
35
|
+
// idle event cannot interrupt a turn, so stop-check failures queue into
|
|
36
|
+
// sessionContext for the next system.transform (documented platform gap
|
|
37
|
+
// vs. blocking Stop hooks).
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = execFile(
|
|
40
|
+
"python3",
|
|
41
|
+
[pythonPath(projectRoot, script)],
|
|
42
|
+
{ cwd: projectRoot, timeout, maxBuffer: 1024 * 1024 },
|
|
43
|
+
(error, stdout, stderr) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
debugLog(projectRoot, `callHook ${script} failed: ${error.message || stderr}`);
|
|
46
|
+
resolve(null);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const text = (stdout || "").trim();
|
|
50
|
+
if (!text) {
|
|
51
|
+
resolve(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
resolve(JSON.parse(text));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
debugLog(projectRoot, `callHook ${script} bad JSON: ${e.message}`);
|
|
58
|
+
resolve(null);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
if (input !== undefined && child.stdin) {
|
|
63
|
+
child.stdin.write(JSON.stringify(input));
|
|
64
|
+
child.stdin.end();
|
|
36
65
|
}
|
|
37
|
-
|
|
38
|
-
if (!stdout) return null;
|
|
39
|
-
return JSON.parse(stdout);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
debugLog(projectRoot, `callHook ${script} exception: ${e.message}`);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
66
|
+
});
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
function extractPromptText(output) {
|
|
@@ -65,11 +88,10 @@ export const CodeFlow = async (ctx) => {
|
|
|
65
88
|
const sid =
|
|
66
89
|
input.event?.properties?.sessionID ||
|
|
67
90
|
input.event?.properties?.info?.id || "";
|
|
68
|
-
const result = callHook(projectRoot, "cf_stop_hook.py", { session_id: sid }, 35000);
|
|
91
|
+
const result = await callHook(projectRoot, "cf_stop_hook.py", { session_id: sid }, 35000);
|
|
69
92
|
if (result?.reason) {
|
|
70
93
|
// idle 无法阻断,校验失败排队到下一轮 system prompt
|
|
71
|
-
|
|
72
|
-
sessionContext.set(sid, pending ? pending + "\n\n" + result.reason : result.reason);
|
|
94
|
+
sessionContext.set(sid, mergePending(sessionContext.get(sid), result.reason));
|
|
73
95
|
debugLog(projectRoot, `stop-check feedback queued`);
|
|
74
96
|
}
|
|
75
97
|
}
|
|
@@ -81,7 +103,7 @@ export const CodeFlow = async (ctx) => {
|
|
|
81
103
|
|
|
82
104
|
debugLog(projectRoot, `chat.message sid=${input.sessionID} prompt_len=${promptText.length}`);
|
|
83
105
|
|
|
84
|
-
const result = callHook(projectRoot, "cf_user_prompt_hook.py", {
|
|
106
|
+
const result = await callHook(projectRoot, "cf_user_prompt_hook.py", {
|
|
85
107
|
prompt: promptText,
|
|
86
108
|
session_id: input.sessionID,
|
|
87
109
|
});
|
|
@@ -89,9 +111,10 @@ export const CodeFlow = async (ctx) => {
|
|
|
89
111
|
if (result?.hookSpecificOutput?.additionalContext) {
|
|
90
112
|
const ctxLen = result.hookSpecificOutput.additionalContext.length;
|
|
91
113
|
debugLog(projectRoot, `hook matched — context ${ctxLen} chars cached`);
|
|
114
|
+
// Append, never overwrite: queued stop-check feedback survives.
|
|
92
115
|
sessionContext.set(
|
|
93
116
|
input.sessionID,
|
|
94
|
-
result.hookSpecificOutput.additionalContext
|
|
117
|
+
mergePending(sessionContext.get(input.sessionID), result.hookSpecificOutput.additionalContext)
|
|
95
118
|
);
|
|
96
119
|
} else {
|
|
97
120
|
debugLog(projectRoot, `hook returned no context`);
|
|
@@ -104,7 +127,7 @@ export const CodeFlow = async (ctx) => {
|
|
|
104
127
|
const args = output?.args || input?.args || {};
|
|
105
128
|
const filePath = args.filePath || args.file_path || args.path;
|
|
106
129
|
if (!filePath) return;
|
|
107
|
-
const result = callHook(projectRoot, "cf_post_hook.py", {
|
|
130
|
+
const result = await callHook(projectRoot, "cf_post_hook.py", {
|
|
108
131
|
tool_name: tool === "write" ? "Write" : "Edit",
|
|
109
132
|
tool_input: { file_path: filePath },
|
|
110
133
|
session_id: input?.sessionID || "",
|
|
@@ -113,8 +136,7 @@ export const CodeFlow = async (ctx) => {
|
|
|
113
136
|
if (ctx) {
|
|
114
137
|
// 反馈排队,下一轮 system.transform 注入(opencode 无法当轮插话)
|
|
115
138
|
const sid = input?.sessionID || "";
|
|
116
|
-
|
|
117
|
-
sessionContext.set(sid, pending ? pending + "\n\n" + ctx : ctx);
|
|
139
|
+
sessionContext.set(sid, mergePending(sessionContext.get(sid), ctx));
|
|
118
140
|
debugLog(projectRoot, `post-check feedback queued ${ctx.length} chars`);
|
|
119
141
|
}
|
|
120
142
|
},
|
package/src/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const pkg = require('../package.json');
|
|
|
10
10
|
const specWorkflowMigration = require('./migrate/spec-workflow');
|
|
11
11
|
|
|
12
12
|
const usage = [
|
|
13
|
-
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>]',
|
|
13
|
+
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>] [--backup-dir=<path>]',
|
|
14
14
|
' code-flow migrate --spec-workflow <--dry-run|--prepare|--apply --plan <path>|--rollback <id>>',
|
|
15
15
|
' code-flow -v | --version',
|
|
16
16
|
' code-flow -h | --help'
|
|
@@ -83,6 +83,43 @@ function compareVersions(a, b) {
|
|
|
83
83
|
return 0;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function modeFor(installed, force) {
|
|
87
|
+
if (force) return 'force';
|
|
88
|
+
if (!installed) return 'fresh';
|
|
89
|
+
if (compareVersions(installed, pkg.version) < 0) return 'upgrade';
|
|
90
|
+
return 'current';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Per-platform install records live beside the legacy global .version so one
|
|
94
|
+
// platform's upgrade never masks another platform's staleness.
|
|
95
|
+
function readAdapterVersions(cwd) {
|
|
96
|
+
const file = path.join(cwd, '.code-flow', '.adapter-versions.json');
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
99
|
+
if (data && typeof data === 'object' && data.adapters && typeof data.adapters === 'object') {
|
|
100
|
+
return { core: typeof data.core === 'string' ? data.core : null, adapters: data.adapters };
|
|
101
|
+
}
|
|
102
|
+
} catch (_) { /* missing or corrupt: fall through */ }
|
|
103
|
+
return { core: null, adapters: {} };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeAdapterVersions(cwd, versions) {
|
|
107
|
+
const file = path.join(cwd, '.code-flow', '.adapter-versions.json');
|
|
108
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
109
|
+
fs.writeFileSync(file, JSON.stringify({ core: versions.core, adapters: versions.adapters }, null, 2) + '\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Marker dirs proving a platform was installed before per-platform tracking.
|
|
113
|
+
function platformFilesExist(cwd, platform) {
|
|
114
|
+
const markers = {
|
|
115
|
+
claude: ['.claude/commands'],
|
|
116
|
+
codex: ['.agents/skills'],
|
|
117
|
+
costrict: ['.costrict/commands'],
|
|
118
|
+
opencode: ['.opencode/commands'],
|
|
119
|
+
};
|
|
120
|
+
return (markers[platform] || []).some(rel => fs.existsSync(path.join(cwd, rel)));
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
// --- Merge functions ---
|
|
87
124
|
|
|
88
125
|
// Replace fenced code blocks with same-length whitespace so byte offsets in
|
|
@@ -342,6 +379,53 @@ function copyFileIfMissing(srcFile, destFile) {
|
|
|
342
379
|
fs.copyFileSync(srcFile, destFile);
|
|
343
380
|
}
|
|
344
381
|
|
|
382
|
+
function removeLegacyClaudeSkills(cwd, removed, opts) {
|
|
383
|
+
// 0.1-era code-flow deployed its own skills (cf-*.md) under .claude/skills/.
|
|
384
|
+
// Only those historically managed names may be touched; everything else is
|
|
385
|
+
// user-owned and must survive init. Managed entries are backed up before
|
|
386
|
+
// removal. The legacy dir belongs to the claude domain: other platforms
|
|
387
|
+
// must never touch it.
|
|
388
|
+
const options = opts || {};
|
|
389
|
+
if (options.platform && options.platform !== 'claude') return;
|
|
390
|
+
const legacySkills = path.join(cwd, '.claude', 'skills');
|
|
391
|
+
if (!fs.existsSync(legacySkills)) return;
|
|
392
|
+
|
|
393
|
+
let entries;
|
|
394
|
+
try {
|
|
395
|
+
entries = fs.readdirSync(legacySkills, { withFileTypes: true });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
398
|
+
process.stderr.write(`Warning: failed to list deprecated .claude/skills/${code}; left untouched. Remove it manually.\n`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
402
|
+
const backupRoot = options.backupDir
|
|
403
|
+
? path.resolve(cwd, options.backupDir)
|
|
404
|
+
: path.join(cwd, '.code-flow', 'backups', stamp);
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (!MANAGED_LEGACY_SKILL_NAMES.has(entry.name)) continue; // user-owned: keep
|
|
407
|
+
const src = path.join(legacySkills, entry.name);
|
|
408
|
+
const rel = path.join('.claude', 'skills', entry.name);
|
|
409
|
+
try {
|
|
410
|
+
const dest = path.join(backupRoot, rel);
|
|
411
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
412
|
+
if (entry.isDirectory()) {
|
|
413
|
+
copyDirRecursive(src, dest, true);
|
|
414
|
+
} else if (entry.isFile()) {
|
|
415
|
+
fs.copyFileSync(src, dest);
|
|
416
|
+
} else {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
fs.rmSync(src, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
420
|
+
removed.push(`${rel} (backup: ${path.relative(cwd, dest)})`);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
423
|
+
const message = error && error.message ? error.message : String(error);
|
|
424
|
+
process.stderr.write(`Warning: failed to migrate deprecated ${rel}${code}: ${message}\n`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
345
429
|
// Files renamed in 0.x → 1.x that may linger in upgraded projects.
|
|
346
430
|
// Each entry: { path: relative-to-cwd, replacedBy: hint string for the warning }.
|
|
347
431
|
const ORPHAN_FILES = [
|
|
@@ -367,35 +451,30 @@ function removeOrphanFiles(cwd, removed) {
|
|
|
367
451
|
}
|
|
368
452
|
}
|
|
369
453
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
});
|
|
381
|
-
if (!fs.existsSync(legacySkills)) {
|
|
382
|
-
removed.push('.claude/skills/');
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
} catch (error) {
|
|
386
|
-
const code = error && error.code ? ` (${error.code})` : '';
|
|
387
|
-
const message = error && error.message ? error.message : String(error);
|
|
388
|
-
process.stderr.write(`Warning: failed to remove deprecated .claude/skills/${code}: ${message}\n`);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
process.stderr.write('Warning: failed to remove deprecated .claude/skills/. Remove it manually.\n');
|
|
393
|
-
}
|
|
454
|
+
// Skill entries historically deployed by code-flow 0.x under .claude/skills/.
|
|
455
|
+
// Only these names are managed legacy content; any other entry is user-owned.
|
|
456
|
+
const MANAGED_LEGACY_SKILL_NAMES = new Set([
|
|
457
|
+
'cf-init.md',
|
|
458
|
+
'cf-inject.md',
|
|
459
|
+
'cf-learn.md',
|
|
460
|
+
'cf-scan.md',
|
|
461
|
+
'cf-stats.md',
|
|
462
|
+
'cf-validate.md',
|
|
463
|
+
]);
|
|
394
464
|
|
|
395
465
|
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
396
466
|
|
|
397
|
-
|
|
398
|
-
|
|
467
|
+
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
468
|
+
// Bounded waits: each attempt carries its own timeout inside a total budget,
|
|
469
|
+
// with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
|
|
470
|
+
const PIP_PER_ATTEMPT_MS = 60000;
|
|
471
|
+
const PIP_TOTAL_MS = 120000;
|
|
472
|
+
|
|
473
|
+
function ensurePyYaml(spawnFn, logFn, nowFn) {
|
|
474
|
+
const spawn = spawnFn || spawnSync;
|
|
475
|
+
const log = logFn || ((message) => process.stderr.write(message));
|
|
476
|
+
const now = nowFn || Date.now;
|
|
477
|
+
const probe = spawn('python3', ['-c', 'import yaml'], { stdio: 'ignore' });
|
|
399
478
|
if (!probe.error && probe.status === 0) return; // already installed
|
|
400
479
|
|
|
401
480
|
const attempts = [
|
|
@@ -403,11 +482,18 @@ function ensurePyYaml() {
|
|
|
403
482
|
['-m', 'pip', 'install', 'pyyaml'],
|
|
404
483
|
['-m', 'pip', 'install', '--user', '--break-system-packages', 'pyyaml'],
|
|
405
484
|
];
|
|
406
|
-
|
|
407
|
-
|
|
485
|
+
const started = now();
|
|
486
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
487
|
+
const remaining = PIP_TOTAL_MS - (now() - started);
|
|
488
|
+
if (remaining <= 0) break;
|
|
489
|
+
log(`Installing pyyaml (attempt ${i + 1}/${attempts.length})...\n`);
|
|
490
|
+
const result = spawn('python3', attempts[i], {
|
|
491
|
+
stdio: 'ignore',
|
|
492
|
+
timeout: Math.min(PIP_PER_ATTEMPT_MS, remaining),
|
|
493
|
+
});
|
|
408
494
|
if (!result.error && result.status === 0) return;
|
|
409
495
|
}
|
|
410
|
-
|
|
496
|
+
log(
|
|
411
497
|
'Warning: pyyaml install failed. Run manually:\n' +
|
|
412
498
|
' python3 -m pip install --user pyyaml\n' +
|
|
413
499
|
' # or, on PEP 668 systems:\n' +
|
|
@@ -468,10 +554,21 @@ function parsePlatform(args) {
|
|
|
468
554
|
return null;
|
|
469
555
|
}
|
|
470
556
|
|
|
557
|
+
function parseStringFlag(args, name) {
|
|
558
|
+
const prefix = `--${name}=`;
|
|
559
|
+
for (const arg of args) {
|
|
560
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
561
|
+
}
|
|
562
|
+
const idx = args.indexOf(`--${name}`);
|
|
563
|
+
if (idx !== -1) return args[idx + 1] || '';
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
|
|
471
567
|
// --- Main init ---
|
|
472
568
|
|
|
473
|
-
function runInit(force, platform) {
|
|
569
|
+
function runInit(force, platform, opts) {
|
|
474
570
|
const cwd = process.cwd();
|
|
571
|
+
const options = opts || {};
|
|
475
572
|
const installedVersion = readInstalledVersion(cwd);
|
|
476
573
|
if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
|
|
477
574
|
process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
|
|
@@ -483,16 +580,28 @@ function runInit(force, platform) {
|
|
|
483
580
|
const coreDir = path.join(baseDir, 'core');
|
|
484
581
|
const adaptersDir = path.join(baseDir, 'adapters');
|
|
485
582
|
|
|
486
|
-
// Determine mode
|
|
487
|
-
|
|
583
|
+
// Determine mode: global mode drives core files and messaging; each
|
|
584
|
+
// platform additionally resolves its own mode from per-platform records so
|
|
585
|
+
// upgrading one platform can never mask another platform's staleness.
|
|
586
|
+
// A global upgrade also refreshes the selected platform (previous behavior).
|
|
587
|
+
const mode = modeFor(installedVersion, force);
|
|
588
|
+
const adapterVersions = readAdapterVersions(cwd);
|
|
589
|
+
const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
|
|
590
|
+
? adapterVersions.adapters[platform]
|
|
591
|
+
: null;
|
|
592
|
+
let platformMode;
|
|
488
593
|
if (force) {
|
|
489
|
-
|
|
490
|
-
} else if (!
|
|
491
|
-
|
|
492
|
-
} else if (
|
|
493
|
-
|
|
594
|
+
platformMode = 'force';
|
|
595
|
+
} else if (!adapterPrev && !platformFilesExist(cwd, platform)) {
|
|
596
|
+
platformMode = 'fresh';
|
|
597
|
+
} else if (
|
|
598
|
+
(adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
|
|
599
|
+
(!adapterPrev && platformFilesExist(cwd, platform)) ||
|
|
600
|
+
mode === 'upgrade'
|
|
601
|
+
) {
|
|
602
|
+
platformMode = 'upgrade';
|
|
494
603
|
} else {
|
|
495
|
-
|
|
604
|
+
platformMode = 'current';
|
|
496
605
|
}
|
|
497
606
|
|
|
498
607
|
const created = [];
|
|
@@ -502,8 +611,10 @@ function runInit(force, platform) {
|
|
|
502
611
|
const removed = [];
|
|
503
612
|
const results = { created, updated, merged, skipped };
|
|
504
613
|
|
|
505
|
-
// Track + copy helper for directory trees
|
|
506
|
-
|
|
614
|
+
// Track + copy helper for directory trees. dirMode is resolved per area:
|
|
615
|
+
// core files follow the global mode, platform files follow platformMode.
|
|
616
|
+
const processDir = (srcDir, destDir, prefix, dirMode) => {
|
|
617
|
+
const effective = dirMode || mode;
|
|
507
618
|
if (!fs.existsSync(srcDir)) return;
|
|
508
619
|
const files = collectFiles(srcDir, '');
|
|
509
620
|
for (const rel of files) {
|
|
@@ -516,10 +627,10 @@ function runInit(force, platform) {
|
|
|
516
627
|
created.push(label);
|
|
517
628
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
518
629
|
fs.copyFileSync(src, dest);
|
|
519
|
-
} else if (
|
|
630
|
+
} else if (effective === 'force') {
|
|
520
631
|
updated.push(label);
|
|
521
632
|
fs.copyFileSync(src, dest);
|
|
522
|
-
} else if (
|
|
633
|
+
} else if (effective === 'upgrade' && cat === 'tool') {
|
|
523
634
|
updated.push(label);
|
|
524
635
|
fs.copyFileSync(src, dest);
|
|
525
636
|
} else {
|
|
@@ -529,7 +640,7 @@ function runInit(force, platform) {
|
|
|
529
640
|
};
|
|
530
641
|
|
|
531
642
|
// Process .code-flow/ (core)
|
|
532
|
-
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow');
|
|
643
|
+
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
|
|
533
644
|
|
|
534
645
|
// Process Claude adapter
|
|
535
646
|
if (platform === 'claude') {
|
|
@@ -537,7 +648,7 @@ function runInit(force, platform) {
|
|
|
537
648
|
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
538
649
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
539
650
|
label: 'CLAUDE.md',
|
|
540
|
-
mode,
|
|
651
|
+
mode: platformMode,
|
|
541
652
|
mergeFn: mergeClaudeMd,
|
|
542
653
|
toolOnUpgrade: false,
|
|
543
654
|
results,
|
|
@@ -547,14 +658,14 @@ function runInit(force, platform) {
|
|
|
547
658
|
processDir(
|
|
548
659
|
path.join(adaptersDir, 'claude', 'commands'),
|
|
549
660
|
path.join(cwd, '.claude', 'commands'),
|
|
550
|
-
'.claude/commands'
|
|
661
|
+
'.claude/commands', platformMode
|
|
551
662
|
);
|
|
552
663
|
|
|
553
664
|
installAdapterFile({
|
|
554
665
|
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
555
666
|
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
556
667
|
label: '.claude/settings.local.json',
|
|
557
|
-
mode,
|
|
668
|
+
mode: platformMode,
|
|
558
669
|
mergeFn: mergeSettingsJson,
|
|
559
670
|
toolOnUpgrade: false,
|
|
560
671
|
results,
|
|
@@ -567,7 +678,7 @@ function runInit(force, platform) {
|
|
|
567
678
|
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
568
679
|
dest: path.join(cwd, 'CLAUDE.md'),
|
|
569
680
|
label: 'CLAUDE.md',
|
|
570
|
-
mode,
|
|
681
|
+
mode: platformMode,
|
|
571
682
|
mergeFn: mergeClaudeMd,
|
|
572
683
|
toolOnUpgrade: false,
|
|
573
684
|
results,
|
|
@@ -577,14 +688,14 @@ function runInit(force, platform) {
|
|
|
577
688
|
processDir(
|
|
578
689
|
path.join(adaptersDir, 'costrict', 'commands'),
|
|
579
690
|
path.join(cwd, '.costrict', 'commands'),
|
|
580
|
-
'.costrict/commands'
|
|
691
|
+
'.costrict/commands', platformMode
|
|
581
692
|
);
|
|
582
693
|
|
|
583
694
|
installAdapterFile({
|
|
584
695
|
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
585
696
|
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
586
697
|
label: '.costrict/settings.local.json',
|
|
587
|
-
mode,
|
|
698
|
+
mode: platformMode,
|
|
588
699
|
mergeFn: mergeSettingsJson,
|
|
589
700
|
toolOnUpgrade: false,
|
|
590
701
|
results,
|
|
@@ -597,7 +708,7 @@ function runInit(force, platform) {
|
|
|
597
708
|
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
598
709
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
599
710
|
label: 'AGENTS.md',
|
|
600
|
-
mode,
|
|
711
|
+
mode: platformMode,
|
|
601
712
|
mergeFn: mergeClaudeMd,
|
|
602
713
|
toolOnUpgrade: false,
|
|
603
714
|
results,
|
|
@@ -607,7 +718,7 @@ function runInit(force, platform) {
|
|
|
607
718
|
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
608
719
|
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
609
720
|
label: '.codex/hooks.json',
|
|
610
|
-
mode,
|
|
721
|
+
mode: platformMode,
|
|
611
722
|
mergeFn: mergeSettingsJson,
|
|
612
723
|
toolOnUpgrade: false,
|
|
613
724
|
results,
|
|
@@ -617,7 +728,7 @@ function runInit(force, platform) {
|
|
|
617
728
|
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
618
729
|
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
619
730
|
label: '.codex/config.toml',
|
|
620
|
-
mode,
|
|
731
|
+
mode: platformMode,
|
|
621
732
|
mergeFn: mergeCodexConfigToml,
|
|
622
733
|
toolOnUpgrade: false,
|
|
623
734
|
results,
|
|
@@ -627,7 +738,7 @@ function runInit(force, platform) {
|
|
|
627
738
|
processDir(
|
|
628
739
|
path.join(adaptersDir, 'codex', 'skills'),
|
|
629
740
|
path.join(cwd, '.agents', 'skills'),
|
|
630
|
-
'.agents/skills'
|
|
741
|
+
'.agents/skills', platformMode
|
|
631
742
|
);
|
|
632
743
|
}
|
|
633
744
|
|
|
@@ -637,7 +748,7 @@ function runInit(force, platform) {
|
|
|
637
748
|
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
638
749
|
dest: path.join(cwd, 'AGENTS.md'),
|
|
639
750
|
label: 'AGENTS.md',
|
|
640
|
-
mode,
|
|
751
|
+
mode: platformMode,
|
|
641
752
|
mergeFn: mergeClaudeMd,
|
|
642
753
|
toolOnUpgrade: false,
|
|
643
754
|
results,
|
|
@@ -647,7 +758,7 @@ function runInit(force, platform) {
|
|
|
647
758
|
processDir(
|
|
648
759
|
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
649
760
|
path.join(cwd, '.opencode', 'plugins'),
|
|
650
|
-
'.opencode/plugins'
|
|
761
|
+
'.opencode/plugins', platformMode
|
|
651
762
|
);
|
|
652
763
|
|
|
653
764
|
// Stamp main package version into the plugin's package.json
|
|
@@ -662,14 +773,14 @@ function runInit(force, platform) {
|
|
|
662
773
|
processDir(
|
|
663
774
|
path.join(adaptersDir, 'opencode', 'commands'),
|
|
664
775
|
path.join(cwd, '.opencode', 'commands'),
|
|
665
|
-
'.opencode/commands'
|
|
776
|
+
'.opencode/commands', platformMode
|
|
666
777
|
);
|
|
667
778
|
|
|
668
779
|
installAdapterFile({
|
|
669
780
|
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
670
781
|
dest: path.join(cwd, 'opencode.json'),
|
|
671
782
|
label: 'opencode.json',
|
|
672
|
-
mode,
|
|
783
|
+
mode: platformMode,
|
|
673
784
|
mergeFn: mergeSettingsJson,
|
|
674
785
|
toolOnUpgrade: false,
|
|
675
786
|
results,
|
|
@@ -689,8 +800,8 @@ function runInit(force, platform) {
|
|
|
689
800
|
}
|
|
690
801
|
}
|
|
691
802
|
|
|
692
|
-
// Clean up legacy .claude/skills/
|
|
693
|
-
removeLegacyClaudeSkills(cwd, removed);
|
|
803
|
+
// Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
|
|
804
|
+
removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
|
|
694
805
|
|
|
695
806
|
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
696
807
|
if (mode === 'upgrade' || mode === 'force') {
|
|
@@ -700,13 +811,16 @@ function runInit(force, platform) {
|
|
|
700
811
|
// Install pyyaml — probe first, fall back through PEP 668 strategies
|
|
701
812
|
ensurePyYaml();
|
|
702
813
|
|
|
703
|
-
// Write version
|
|
814
|
+
// Write version (legacy global file kept for compatibility) + per-platform records
|
|
704
815
|
writeVersion(cwd, pkg.version);
|
|
816
|
+
adapterVersions.adapters[platform] = pkg.version;
|
|
817
|
+
adapterVersions.core = pkg.version;
|
|
818
|
+
writeAdapterVersions(cwd, adapterVersions);
|
|
705
819
|
|
|
706
820
|
// Output summary
|
|
707
821
|
if (mode === 'upgrade') {
|
|
708
822
|
process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
|
|
709
|
-
} else if (mode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
823
|
+
} else if (mode === 'current' && platformMode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
710
824
|
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
711
825
|
process.exit(0);
|
|
712
826
|
} else if (mode === 'force') {
|
|
@@ -841,7 +955,7 @@ if (args[0] === 'init') {
|
|
|
841
955
|
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
842
956
|
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
843
957
|
}
|
|
844
|
-
runInit(force, platform);
|
|
958
|
+
runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir') });
|
|
845
959
|
}
|
|
846
960
|
|
|
847
961
|
if (args[0] === 'migrate') {
|