@michaelxwb/code-flow 0.6.5 → 1.0.0
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/README.md +75 -110
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +268 -0
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +21 -9
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -264
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -67
- package/src/adapters/claude/commands/cf-stats.md +0 -86
- package/src/adapters/claude/commands/cf-sync.md +0 -25
- package/src/adapters/claude/commands/cf-task/align.md +0 -309
- package/src/adapters/claude/commands/cf-task/archive.md +0 -161
- package/src/adapters/claude/commands/cf-task/block.md +0 -36
- package/src/adapters/claude/commands/cf-task/graph.md +0 -80
- package/src/adapters/claude/commands/cf-task/note.md +0 -236
- package/src/adapters/claude/commands/cf-task/plan.md +0 -363
- package/src/adapters/claude/commands/cf-task/prd.md +0 -162
- package/src/adapters/claude/commands/cf-task/start.md +0 -304
- package/src/adapters/claude/commands/cf-task/status.md +0 -59
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/claude/commands/cf-validate.md +0 -75
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -50
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -264
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -67
- package/src/adapters/costrict/commands/cf-stats.md +0 -86
- package/src/adapters/costrict/commands/cf-sync.md +0 -25
- package/src/adapters/costrict/commands/cf-task/align.md +0 -309
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
- package/src/adapters/costrict/commands/cf-task/block.md +0 -36
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
- package/src/adapters/costrict/commands/cf-task/note.md +0 -236
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
- package/src/adapters/costrict/commands/cf-task/start.md +0 -304
- package/src/adapters/costrict/commands/cf-task/status.md +0 -59
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/costrict/commands/cf-validate.md +0 -75
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -59
- package/src/adapters/opencode/commands/cf-stats.md +0 -90
- package/src/adapters/opencode/commands/cf-sync.md +0 -29
- package/src/adapters/opencode/commands/cf-task/align.md +0 -314
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
- package/src/adapters/opencode/commands/cf-task/block.md +0 -40
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
- package/src/adapters/opencode/commands/cf-task/note.md +0 -240
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
- package/src/adapters/opencode/commands/cf-task/start.md +0 -309
- package/src/adapters/opencode/commands/cf-task/status.md +0 -63
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/opencode/commands/cf-validate.md +0 -79
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/cli.js +0 -1023
- package/src/core/code-flow/config.yml +0 -158
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_checks.py +0 -336
- package/src/core/code-flow/scripts/cf_core.py +0 -787
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_feedback.py +0 -56
- package/src/core/code-flow/scripts/cf_log.py +0 -154
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
- package/src/core/code-flow/scripts/cf_scan.py +0 -295
- package/src/core/code-flow/scripts/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
- package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
- package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
- package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
- package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
- package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
- package/src/core/code-flow/scripts/cf_stats.py +0 -506
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
- package/src/core/code-flow/scripts/cf_sync.py +0 -140
- package/src/core/code-flow/scripts/cf_task_index.py +0 -148
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_validation.py +0 -81
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -53
- package/src/migrate/spec-workflow.js +0 -238
package/src/cli.js
DELETED
|
@@ -1,1023 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { spawnSync } = require('child_process');
|
|
8
|
-
|
|
9
|
-
const pkg = require('../package.json');
|
|
10
|
-
const specWorkflowMigration = require('./migrate/spec-workflow');
|
|
11
|
-
|
|
12
|
-
const usage = [
|
|
13
|
-
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>] [--backup-dir=<path>]',
|
|
14
|
-
' code-flow migrate --spec-workflow <--dry-run|--prepare|--apply --plan <path>|--rollback <id>>',
|
|
15
|
-
' code-flow -v | --version',
|
|
16
|
-
' code-flow -h | --help'
|
|
17
|
-
].join('\n');
|
|
18
|
-
|
|
19
|
-
function printUsage(stream) {
|
|
20
|
-
stream.write(`${usage}\n`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function fail(message) {
|
|
24
|
-
if (message) {
|
|
25
|
-
process.stderr.write(`${message}\n`);
|
|
26
|
-
}
|
|
27
|
-
printUsage(process.stderr);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ensurePython3() {
|
|
32
|
-
const probe = spawnSync('python3', ['--version'], { stdio: 'ignore' });
|
|
33
|
-
if (probe.error || probe.status !== 0) {
|
|
34
|
-
process.stderr.write('Error: python3 is required but was not found in PATH.\n');
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// --- File classification ---
|
|
40
|
-
|
|
41
|
-
function fileCategory(relPath) {
|
|
42
|
-
const p = relPath.replace(/\\/g, '/');
|
|
43
|
-
if (p.startsWith('.claude/commands/')) return 'tool';
|
|
44
|
-
if (p.startsWith('.costrict/commands/')) return 'tool';
|
|
45
|
-
if (p.startsWith('.agents/skills/')) return 'tool';
|
|
46
|
-
if (p.startsWith('.opencode/commands/')) return 'tool';
|
|
47
|
-
if (p.startsWith('.code-flow/scripts/')) return 'tool';
|
|
48
|
-
if (p === 'CLAUDE.md') return 'merge';
|
|
49
|
-
if (p === 'AGENTS.md') return 'merge';
|
|
50
|
-
if (p === '.claude/settings.local.json') return 'merge';
|
|
51
|
-
if (p === '.costrict/settings.local.json') return 'merge';
|
|
52
|
-
if (p === '.codex/hooks.json') return 'merge';
|
|
53
|
-
if (p === '.code-flow/config.yml') return 'merge';
|
|
54
|
-
if (p === '.codex/config.toml') return 'merge';
|
|
55
|
-
if (p.startsWith('.opencode/plugins/')) return 'tool';
|
|
56
|
-
if (p === 'opencode.json') return 'merge';
|
|
57
|
-
return 'user';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// --- Version tracking ---
|
|
61
|
-
|
|
62
|
-
function readInstalledVersion(cwd) {
|
|
63
|
-
const vFile = path.join(cwd, '.code-flow', '.version');
|
|
64
|
-
if (!fs.existsSync(vFile)) return null;
|
|
65
|
-
return fs.readFileSync(vFile, 'utf8').trim();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function writeVersion(cwd, version) {
|
|
69
|
-
const vFile = path.join(cwd, '.code-flow', '.version');
|
|
70
|
-
fs.mkdirSync(path.dirname(vFile), { recursive: true });
|
|
71
|
-
fs.writeFileSync(vFile, version + '\n');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function compareVersions(a, b) {
|
|
75
|
-
const pa = a.split('.').map(Number);
|
|
76
|
-
const pb = b.split('.').map(Number);
|
|
77
|
-
for (let i = 0; i < 3; i++) {
|
|
78
|
-
const va = pa[i] || 0;
|
|
79
|
-
const vb = pb[i] || 0;
|
|
80
|
-
if (va < vb) return -1;
|
|
81
|
-
if (va > vb) return 1;
|
|
82
|
-
}
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
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
|
-
|
|
123
|
-
// --- Merge functions ---
|
|
124
|
-
|
|
125
|
-
// Replace fenced code blocks with same-length whitespace so byte offsets in
|
|
126
|
-
// the original text stay valid for slice operations downstream — but ## lines
|
|
127
|
-
// inside ```...``` no longer match the section regex.
|
|
128
|
-
function maskFencedCode(text) {
|
|
129
|
-
return text.replace(/```[\s\S]*?```/g, block => block.replace(/[^\n]/g, ' '));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function mergeClaudeMd(srcFile, destFile) {
|
|
133
|
-
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
134
|
-
const destText = fs.readFileSync(destFile, 'utf8');
|
|
135
|
-
const srcMasked = maskFencedCode(srcText);
|
|
136
|
-
const destMasked = maskFencedCode(destText);
|
|
137
|
-
|
|
138
|
-
const sectionRegex = /^## .+$/gm;
|
|
139
|
-
const srcSections = [];
|
|
140
|
-
let match;
|
|
141
|
-
while ((match = sectionRegex.exec(srcMasked)) !== null) {
|
|
142
|
-
srcSections.push({ heading: match[0].trim(), index: match.index });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const destSectionSet = new Set();
|
|
146
|
-
const destRegex = /^## .+$/gm;
|
|
147
|
-
while ((match = destRegex.exec(destMasked)) !== null) {
|
|
148
|
-
destSectionSet.add(match[0].trim());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const missing = srcSections.filter(s => !destSectionSet.has(s.heading));
|
|
152
|
-
if (missing.length === 0) return [];
|
|
153
|
-
|
|
154
|
-
const additions = [];
|
|
155
|
-
for (const { heading, index } of missing) {
|
|
156
|
-
// Find next real section heading using the masked text, then slice the
|
|
157
|
-
// original text by that offset so we keep any code-block content intact.
|
|
158
|
-
const nextRel = srcMasked.slice(index + heading.length).search(/^## /m);
|
|
159
|
-
const end = nextRel === -1 ? srcText.length : index + heading.length + nextRel;
|
|
160
|
-
additions.push(srcText.slice(index, end).trimEnd());
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const merged = destText.trimEnd() + '\n\n' + additions.join('\n\n') + '\n';
|
|
164
|
-
fs.writeFileSync(destFile, merged);
|
|
165
|
-
return missing.map(m => m.heading);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Deep-merge a single hook event array. Each item has shape
|
|
169
|
-
// { matcher?: string, hooks: [{ type, command, ... }] }
|
|
170
|
-
// We treat (matcher || '') as the identity key. New src items become new
|
|
171
|
-
// dest items; for existing items we union the inner hooks array by command
|
|
172
|
-
// string. User-added items / commands are never removed or rewritten — the
|
|
173
|
-
// merge is purely additive, in line with cli/code-standards.md "合并策略
|
|
174
|
-
// 必须保证用户自定义内容不被覆盖".
|
|
175
|
-
// If a future release changes a managed hook command string, add an explicit
|
|
176
|
-
// migration before merging; otherwise old and new commands will both run.
|
|
177
|
-
function mergeHookEventArray(srcArr, destArr, eventName, added) {
|
|
178
|
-
if (!Array.isArray(srcArr) || !Array.isArray(destArr)) return;
|
|
179
|
-
for (const srcItem of srcArr) {
|
|
180
|
-
if (!srcItem || typeof srcItem !== 'object') continue;
|
|
181
|
-
const srcMatcher = typeof srcItem.matcher === 'string' ? srcItem.matcher : '';
|
|
182
|
-
const destItem = destArr.find(d => {
|
|
183
|
-
if (!d || typeof d !== 'object') return false;
|
|
184
|
-
const m = typeof d.matcher === 'string' ? d.matcher : '';
|
|
185
|
-
return m === srcMatcher;
|
|
186
|
-
});
|
|
187
|
-
if (!destItem) {
|
|
188
|
-
destArr.push(srcItem);
|
|
189
|
-
added.push(`hook: ${eventName}${srcMatcher ? '@' + srcMatcher : ''}`);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (!Array.isArray(destItem.hooks)) destItem.hooks = [];
|
|
193
|
-
const destCmds = new Set(
|
|
194
|
-
destItem.hooks.map(h => (h && typeof h.command === 'string' ? h.command : ''))
|
|
195
|
-
);
|
|
196
|
-
for (const srcHook of (Array.isArray(srcItem.hooks) ? srcItem.hooks : [])) {
|
|
197
|
-
if (!srcHook || typeof srcHook !== 'object') continue;
|
|
198
|
-
const cmd = typeof srcHook.command === 'string' ? srcHook.command : '';
|
|
199
|
-
if (destCmds.has(cmd)) continue;
|
|
200
|
-
destItem.hooks.push(srcHook);
|
|
201
|
-
destCmds.add(cmd);
|
|
202
|
-
const tag = srcMatcher ? `${eventName}@${srcMatcher}` : eventName;
|
|
203
|
-
added.push(`hook: ${tag} +${cmd.slice(0, 60)}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function mergeSettingsJson(srcFile, destFile) {
|
|
209
|
-
const src = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
|
|
210
|
-
const dest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
|
|
211
|
-
const added = [];
|
|
212
|
-
|
|
213
|
-
if (src.hooks && typeof src.hooks === 'object') {
|
|
214
|
-
if (!dest.hooks || typeof dest.hooks !== 'object') dest.hooks = {};
|
|
215
|
-
for (const event of Object.keys(src.hooks)) {
|
|
216
|
-
const srcEvent = src.hooks[event];
|
|
217
|
-
if (!dest.hooks[event]) {
|
|
218
|
-
dest.hooks[event] = srcEvent;
|
|
219
|
-
added.push(`hook: ${event}`);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
mergeHookEventArray(srcEvent, dest.hooks[event], event, added);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
for (const key of Object.keys(src)) {
|
|
227
|
-
if (key === 'hooks') continue;
|
|
228
|
-
if (!(key in dest)) {
|
|
229
|
-
dest[key] = src[key];
|
|
230
|
-
added.push(key);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (added.length > 0) {
|
|
235
|
-
fs.writeFileSync(destFile, JSON.stringify(dest, null, 2) + '\n');
|
|
236
|
-
}
|
|
237
|
-
return added;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// OpenCode v2 merge: migrate the v1 `plugin` key to `plugins`, drop the
|
|
241
|
-
// auto-discovered local path (`.opencode/plugins/*` loads without config),
|
|
242
|
-
// and union template `plugins` entries without overwriting user content.
|
|
243
|
-
function mergeOpencodeJson(srcFile, destFile) {
|
|
244
|
-
const src = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
|
|
245
|
-
const dest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
|
|
246
|
-
const added = [];
|
|
247
|
-
const isLocalEntry = (e) => {
|
|
248
|
-
const p = typeof e === 'string' ? e : (e && e.package);
|
|
249
|
-
return typeof p === 'string' && /(^|\/)\.opencode\/plugins\//.test(p);
|
|
250
|
-
};
|
|
251
|
-
if (Array.isArray(dest.plugin)) {
|
|
252
|
-
const carried = dest.plugin.filter((e) => !isLocalEntry(e));
|
|
253
|
-
delete dest.plugin;
|
|
254
|
-
added.push(`removed legacy 'plugin' key (v1 → v2)`);
|
|
255
|
-
if (carried.length > 0 && !Array.isArray(dest.plugins)) dest.plugins = carried;
|
|
256
|
-
} else if ('plugin' in dest) {
|
|
257
|
-
delete dest.plugin;
|
|
258
|
-
added.push(`removed legacy 'plugin' key (v1 → v2)`);
|
|
259
|
-
}
|
|
260
|
-
if (Array.isArray(dest.plugins)) {
|
|
261
|
-
const before = dest.plugins.length;
|
|
262
|
-
dest.plugins = dest.plugins.filter((e) => !isLocalEntry(e));
|
|
263
|
-
if (dest.plugins.length !== before) added.push('removed auto-discovered local plugin entry');
|
|
264
|
-
}
|
|
265
|
-
if (Array.isArray(src.plugins)) {
|
|
266
|
-
if (!Array.isArray(dest.plugins)) dest.plugins = [];
|
|
267
|
-
const seen = new Set(dest.plugins.map((e) => JSON.stringify(e)));
|
|
268
|
-
for (const entry of src.plugins) {
|
|
269
|
-
const key = JSON.stringify(entry);
|
|
270
|
-
if (!seen.has(key)) {
|
|
271
|
-
dest.plugins.push(entry);
|
|
272
|
-
seen.add(key);
|
|
273
|
-
added.push(`plugins +${typeof entry === 'string' ? entry : key}`.slice(0, 80));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const key of Object.keys(src)) {
|
|
278
|
-
if (key === 'plugins' || key === 'plugin') continue;
|
|
279
|
-
if (!(key in dest)) {
|
|
280
|
-
dest[key] = src[key];
|
|
281
|
-
added.push(key);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (added.length > 0) {
|
|
285
|
-
fs.writeFileSync(destFile, JSON.stringify(dest, null, 2) + '\n');
|
|
286
|
-
}
|
|
287
|
-
return added;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function mergeConfigYml(srcFile, destFile) {
|
|
291
|
-
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
292
|
-
const destText = fs.readFileSync(destFile, 'utf8');
|
|
293
|
-
|
|
294
|
-
// Extract top-level keys (lines starting with a non-space char followed by colon)
|
|
295
|
-
const topKeyRegex = /^([a-zA-Z_][a-zA-Z0-9_]*):/gm;
|
|
296
|
-
const srcKeys = new Map();
|
|
297
|
-
let m;
|
|
298
|
-
while ((m = topKeyRegex.exec(srcText)) !== null) {
|
|
299
|
-
srcKeys.set(m[1], m.index);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const destKeys = new Set();
|
|
303
|
-
const destRegex = /^([a-zA-Z_][a-zA-Z0-9_]*):/gm;
|
|
304
|
-
while ((m = destRegex.exec(destText)) !== null) {
|
|
305
|
-
destKeys.add(m[1]);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const missing = [];
|
|
309
|
-
const blocks = [];
|
|
310
|
-
for (const [key, startIdx] of srcKeys) {
|
|
311
|
-
if (destKeys.has(key)) continue;
|
|
312
|
-
missing.push(key);
|
|
313
|
-
// Extract the entire block for this key (until next top-level key or EOF)
|
|
314
|
-
const remaining = srcText.slice(startIdx);
|
|
315
|
-
const nextKey = remaining.indexOf('\n') !== -1
|
|
316
|
-
? remaining.slice(remaining.indexOf('\n') + 1).search(/^[a-zA-Z_]/m)
|
|
317
|
-
: -1;
|
|
318
|
-
const block = nextKey === -1
|
|
319
|
-
? remaining
|
|
320
|
-
: remaining.slice(0, remaining.indexOf('\n') + 1 + nextKey);
|
|
321
|
-
blocks.push(block.trimEnd());
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (blocks.length > 0) {
|
|
325
|
-
const merged = destText.trimEnd() + '\n\n' + blocks.join('\n\n') + '\n';
|
|
326
|
-
fs.writeFileSync(destFile, merged);
|
|
327
|
-
}
|
|
328
|
-
return missing;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function findTomlSectionBounds(lines, sectionName) {
|
|
332
|
-
let start = -1;
|
|
333
|
-
let end = lines.length;
|
|
334
|
-
const header = new RegExp(`^\\s*\\[${sectionName}\\]\\s*$`);
|
|
335
|
-
for (let i = 0; i < lines.length; i++) {
|
|
336
|
-
if (!/^\s*\[.+\]\s*$/.test(lines[i])) continue;
|
|
337
|
-
if (start !== -1) {
|
|
338
|
-
end = i;
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
if (header.test(lines[i])) start = i;
|
|
342
|
-
}
|
|
343
|
-
return { start, end };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function findTomlKeyLine(lines, start, end, key) {
|
|
347
|
-
const keyRegex = new RegExp(`^\\s*${key}\\s*=`);
|
|
348
|
-
for (let i = start + 1; i < end; i++) {
|
|
349
|
-
if (keyRegex.test(lines[i])) return i;
|
|
350
|
-
}
|
|
351
|
-
return -1;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function findFeatureInsertIndex(lines, start, end) {
|
|
355
|
-
for (let i = start + 1; i < end; i++) {
|
|
356
|
-
if (/^\s*$/.test(lines[i])) return i;
|
|
357
|
-
}
|
|
358
|
-
return end;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function mergeCodexConfigToml(srcFile, destFile) {
|
|
362
|
-
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
363
|
-
const destText = fs.readFileSync(destFile, 'utf8');
|
|
364
|
-
const hookLineMatch = srcText.match(/^\s*hooks\s*=.*$/m);
|
|
365
|
-
if (!hookLineMatch) return [];
|
|
366
|
-
|
|
367
|
-
const lines = destText.split(/\n/);
|
|
368
|
-
const { start, end } = findTomlSectionBounds(lines, 'features');
|
|
369
|
-
|
|
370
|
-
if (start === -1) {
|
|
371
|
-
fs.writeFileSync(destFile, destText.trimEnd() + '\n\n[features]\n' + hookLineMatch[0].trim() + '\n');
|
|
372
|
-
return ['features.hooks'];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (findTomlKeyLine(lines, start, end, 'hooks') !== -1) return [];
|
|
376
|
-
|
|
377
|
-
const legacyIdx = findTomlKeyLine(lines, start, end, 'codex_hooks');
|
|
378
|
-
if (legacyIdx !== -1) {
|
|
379
|
-
lines[legacyIdx] = lines[legacyIdx].replace(/^(\s*)codex_hooks(\s*=.*)$/, '$1hooks$2');
|
|
380
|
-
fs.writeFileSync(destFile, lines.join('\n').replace(/\n*$/, '\n'));
|
|
381
|
-
return ['features.hooks'];
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const insertAt = findFeatureInsertIndex(lines, start, end);
|
|
385
|
-
lines.splice(insertAt, 0, hookLineMatch[0].trim());
|
|
386
|
-
fs.writeFileSync(destFile, lines.join('\n').replace(/\n*$/, '\n'));
|
|
387
|
-
return ['features.hooks'];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// --- File operations ---
|
|
391
|
-
|
|
392
|
-
function collectFiles(dir, base) {
|
|
393
|
-
const results = [];
|
|
394
|
-
if (!fs.existsSync(dir)) return results;
|
|
395
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
396
|
-
for (const entry of entries) {
|
|
397
|
-
const rel = path.join(base, entry.name);
|
|
398
|
-
if (entry.isDirectory()) {
|
|
399
|
-
results.push(...collectFiles(path.join(dir, entry.name), rel));
|
|
400
|
-
} else if (entry.isFile()) {
|
|
401
|
-
results.push(rel);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return results;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function copyDirRecursive(srcDir, destDir, overwrite) {
|
|
408
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
409
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
410
|
-
for (const entry of entries) {
|
|
411
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
412
|
-
const destPath = path.join(destDir, entry.name);
|
|
413
|
-
if (entry.isDirectory()) {
|
|
414
|
-
copyDirRecursive(srcPath, destPath, overwrite);
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
if (entry.isFile()) {
|
|
418
|
-
if (overwrite || !fs.existsSync(destPath)) {
|
|
419
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
420
|
-
fs.copyFileSync(srcPath, destPath);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function copyFileIfMissing(srcFile, destFile) {
|
|
427
|
-
if (fs.existsSync(destFile)) return;
|
|
428
|
-
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
429
|
-
fs.copyFileSync(srcFile, destFile);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function removeLegacyClaudeSkills(cwd, removed, opts) {
|
|
433
|
-
// 0.1-era code-flow deployed its own skills (cf-*.md) under .claude/skills/.
|
|
434
|
-
// Only those historically managed names may be touched; everything else is
|
|
435
|
-
// user-owned and must survive init. Managed entries are backed up before
|
|
436
|
-
// removal. The legacy dir belongs to the claude domain: other platforms
|
|
437
|
-
// must never touch it.
|
|
438
|
-
const options = opts || {};
|
|
439
|
-
if (options.platform && options.platform !== 'claude') return;
|
|
440
|
-
const legacySkills = path.join(cwd, '.claude', 'skills');
|
|
441
|
-
if (!fs.existsSync(legacySkills)) return;
|
|
442
|
-
|
|
443
|
-
let entries;
|
|
444
|
-
try {
|
|
445
|
-
entries = fs.readdirSync(legacySkills, { withFileTypes: true });
|
|
446
|
-
} catch (error) {
|
|
447
|
-
const code = error && error.code ? ` (${error.code})` : '';
|
|
448
|
-
process.stderr.write(`Warning: failed to list deprecated .claude/skills/${code}; left untouched. Remove it manually.\n`);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
452
|
-
const backupRoot = options.backupDir
|
|
453
|
-
? path.resolve(cwd, options.backupDir)
|
|
454
|
-
: path.join(cwd, '.code-flow', 'backups', stamp);
|
|
455
|
-
for (const entry of entries) {
|
|
456
|
-
if (!MANAGED_LEGACY_SKILL_NAMES.has(entry.name)) continue; // user-owned: keep
|
|
457
|
-
const src = path.join(legacySkills, entry.name);
|
|
458
|
-
const rel = path.join('.claude', 'skills', entry.name);
|
|
459
|
-
try {
|
|
460
|
-
const dest = path.join(backupRoot, rel);
|
|
461
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
462
|
-
if (entry.isDirectory()) {
|
|
463
|
-
copyDirRecursive(src, dest, true);
|
|
464
|
-
} else if (entry.isFile()) {
|
|
465
|
-
fs.copyFileSync(src, dest);
|
|
466
|
-
} else {
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
fs.rmSync(src, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
470
|
-
removed.push(`${rel} (backup: ${path.relative(cwd, dest)})`);
|
|
471
|
-
} catch (error) {
|
|
472
|
-
const code = error && error.code ? ` (${error.code})` : '';
|
|
473
|
-
const message = error && error.message ? error.message : String(error);
|
|
474
|
-
process.stderr.write(`Warning: failed to migrate deprecated ${rel}${code}: ${message}\n`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Files renamed in 0.x → 1.x that may linger in upgraded projects.
|
|
480
|
-
// Each entry: { path: relative-to-cwd, replacedBy: hint string for the warning }.
|
|
481
|
-
const ORPHAN_FILES = [
|
|
482
|
-
{
|
|
483
|
-
path: '.code-flow/scripts/cf_codex_user_prompt_hook.py',
|
|
484
|
-
replacedBy: '.code-flow/scripts/cf_user_prompt_hook.py',
|
|
485
|
-
},
|
|
486
|
-
];
|
|
487
|
-
|
|
488
|
-
function removeOrphanFiles(cwd, removed) {
|
|
489
|
-
for (const orphan of ORPHAN_FILES) {
|
|
490
|
-
const abs = path.join(cwd, orphan.path);
|
|
491
|
-
if (!fs.existsSync(abs)) continue;
|
|
492
|
-
try {
|
|
493
|
-
fs.unlinkSync(abs);
|
|
494
|
-
removed.push(orphan.path);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
const code = error && error.code ? ` (${error.code})` : '';
|
|
497
|
-
process.stderr.write(
|
|
498
|
-
`Warning: failed to remove orphan ${orphan.path}${code}; replaced by ${orphan.replacedBy}. Remove it manually.\n`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Skill entries historically deployed by code-flow 0.x under .claude/skills/.
|
|
505
|
-
// Only these names are managed legacy content; any other entry is user-owned.
|
|
506
|
-
const MANAGED_LEGACY_SKILL_NAMES = new Set([
|
|
507
|
-
'cf-init.md',
|
|
508
|
-
'cf-inject.md',
|
|
509
|
-
'cf-learn.md',
|
|
510
|
-
'cf-scan.md',
|
|
511
|
-
'cf-stats.md',
|
|
512
|
-
'cf-validate.md',
|
|
513
|
-
]);
|
|
514
|
-
|
|
515
|
-
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
516
|
-
|
|
517
|
-
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
518
|
-
// Bounded waits: each attempt carries its own timeout inside a total budget,
|
|
519
|
-
// with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
|
|
520
|
-
const PIP_PER_ATTEMPT_MS = 60000;
|
|
521
|
-
const PIP_TOTAL_MS = 120000;
|
|
522
|
-
|
|
523
|
-
function ensurePyYaml(spawnFn, logFn, nowFn) {
|
|
524
|
-
const spawn = spawnFn || spawnSync;
|
|
525
|
-
const log = logFn || ((message) => process.stderr.write(message));
|
|
526
|
-
const now = nowFn || Date.now;
|
|
527
|
-
const probe = spawn('python3', ['-c', 'import yaml'], { stdio: 'ignore' });
|
|
528
|
-
if (!probe.error && probe.status === 0) return; // already installed
|
|
529
|
-
|
|
530
|
-
const attempts = [
|
|
531
|
-
['-m', 'pip', 'install', '--user', 'pyyaml'],
|
|
532
|
-
['-m', 'pip', 'install', 'pyyaml'],
|
|
533
|
-
['-m', 'pip', 'install', '--user', '--break-system-packages', 'pyyaml'],
|
|
534
|
-
];
|
|
535
|
-
const started = now();
|
|
536
|
-
for (let i = 0; i < attempts.length; i++) {
|
|
537
|
-
const remaining = PIP_TOTAL_MS - (now() - started);
|
|
538
|
-
if (remaining <= 0) break;
|
|
539
|
-
log(`Installing pyyaml (attempt ${i + 1}/${attempts.length})...\n`);
|
|
540
|
-
const result = spawn('python3', attempts[i], {
|
|
541
|
-
stdio: 'ignore',
|
|
542
|
-
timeout: Math.min(PIP_PER_ATTEMPT_MS, remaining),
|
|
543
|
-
});
|
|
544
|
-
if (!result.error && result.status === 0) return;
|
|
545
|
-
}
|
|
546
|
-
log(
|
|
547
|
-
'Warning: pyyaml install failed. Run manually:\n' +
|
|
548
|
-
' python3 -m pip install --user pyyaml\n' +
|
|
549
|
-
' # or, on PEP 668 systems:\n' +
|
|
550
|
-
' python3 -m pip install --user --break-system-packages pyyaml\n'
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// --- Adapter file installer: shared by every platform branch ---
|
|
555
|
-
|
|
556
|
-
// Install one adapter file with the standard create / force / upgrade /
|
|
557
|
-
// skip semantics. `mergeFn` is consulted only on upgrade for `merge`-class
|
|
558
|
-
// files; pass null for `tool`-class files (overwrite on upgrade) or for
|
|
559
|
-
// fresh-only files (skip on upgrade).
|
|
560
|
-
function installAdapterFile(opts) {
|
|
561
|
-
const { src, dest, label, mode, mergeFn, toolOnUpgrade, results } = opts;
|
|
562
|
-
if (!fs.existsSync(dest)) {
|
|
563
|
-
results.created.push(label);
|
|
564
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
565
|
-
fs.copyFileSync(src, dest);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (mode === 'force') {
|
|
569
|
-
results.updated.push(label);
|
|
570
|
-
fs.copyFileSync(src, dest);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (mode === 'upgrade') {
|
|
574
|
-
if (toolOnUpgrade) {
|
|
575
|
-
results.updated.push(label);
|
|
576
|
-
fs.copyFileSync(src, dest);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (mergeFn) {
|
|
580
|
-
const added = mergeFn(src, dest);
|
|
581
|
-
if (added.length > 0) {
|
|
582
|
-
results.merged.push(`${label} — added: ${added.join(', ')}`);
|
|
583
|
-
} else {
|
|
584
|
-
results.skipped.push(label);
|
|
585
|
-
}
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
results.skipped.push(label);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// --- Platform argument parsing ---
|
|
593
|
-
|
|
594
|
-
function parsePlatform(args) {
|
|
595
|
-
for (const arg of args) {
|
|
596
|
-
if (arg.startsWith('--platform=')) {
|
|
597
|
-
return arg.slice('--platform='.length);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const idx = args.indexOf('--platform');
|
|
601
|
-
if (idx !== -1) {
|
|
602
|
-
return args[idx + 1] || '';
|
|
603
|
-
}
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function parseStringFlag(args, name) {
|
|
608
|
-
const prefix = `--${name}=`;
|
|
609
|
-
for (const arg of args) {
|
|
610
|
-
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
611
|
-
}
|
|
612
|
-
const idx = args.indexOf(`--${name}`);
|
|
613
|
-
if (idx !== -1) return args[idx + 1] || '';
|
|
614
|
-
return '';
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// --- Main init ---
|
|
618
|
-
|
|
619
|
-
function runInit(force, platform, opts) {
|
|
620
|
-
const cwd = process.cwd();
|
|
621
|
-
const options = opts || {};
|
|
622
|
-
const installedVersion = readInstalledVersion(cwd);
|
|
623
|
-
if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
|
|
624
|
-
process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
|
|
625
|
-
process.exit(3);
|
|
626
|
-
}
|
|
627
|
-
ensurePython3();
|
|
628
|
-
|
|
629
|
-
const baseDir = __dirname;
|
|
630
|
-
const coreDir = path.join(baseDir, 'core');
|
|
631
|
-
const adaptersDir = path.join(baseDir, 'adapters');
|
|
632
|
-
|
|
633
|
-
// Determine mode: global mode drives core files and messaging; each
|
|
634
|
-
// platform additionally resolves its own mode from per-platform records so
|
|
635
|
-
// upgrading one platform can never mask another platform's staleness.
|
|
636
|
-
// A global upgrade also refreshes the selected platform (previous behavior).
|
|
637
|
-
const mode = modeFor(installedVersion, force);
|
|
638
|
-
const adapterVersions = readAdapterVersions(cwd);
|
|
639
|
-
const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
|
|
640
|
-
? adapterVersions.adapters[platform]
|
|
641
|
-
: null;
|
|
642
|
-
let platformMode;
|
|
643
|
-
if (force) {
|
|
644
|
-
platformMode = 'force';
|
|
645
|
-
} else if (!adapterPrev && !platformFilesExist(cwd, platform)) {
|
|
646
|
-
platformMode = 'fresh';
|
|
647
|
-
} else if (
|
|
648
|
-
(adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
|
|
649
|
-
(!adapterPrev && platformFilesExist(cwd, platform)) ||
|
|
650
|
-
mode === 'upgrade'
|
|
651
|
-
) {
|
|
652
|
-
platformMode = 'upgrade';
|
|
653
|
-
} else {
|
|
654
|
-
platformMode = 'current';
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const created = [];
|
|
658
|
-
const updated = [];
|
|
659
|
-
const merged = [];
|
|
660
|
-
const skipped = [];
|
|
661
|
-
const removed = [];
|
|
662
|
-
const results = { created, updated, merged, skipped };
|
|
663
|
-
|
|
664
|
-
// Track + copy helper for directory trees. dirMode is resolved per area:
|
|
665
|
-
// core files follow the global mode, platform files follow platformMode.
|
|
666
|
-
const processDir = (srcDir, destDir, prefix, dirMode) => {
|
|
667
|
-
const effective = dirMode || mode;
|
|
668
|
-
if (!fs.existsSync(srcDir)) return;
|
|
669
|
-
const files = collectFiles(srcDir, '');
|
|
670
|
-
for (const rel of files) {
|
|
671
|
-
const src = path.join(srcDir, rel);
|
|
672
|
-
const dest = path.join(destDir, rel);
|
|
673
|
-
const label = path.join(prefix, rel);
|
|
674
|
-
const cat = fileCategory(label);
|
|
675
|
-
|
|
676
|
-
if (!fs.existsSync(dest)) {
|
|
677
|
-
created.push(label);
|
|
678
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
679
|
-
fs.copyFileSync(src, dest);
|
|
680
|
-
} else if (effective === 'force') {
|
|
681
|
-
updated.push(label);
|
|
682
|
-
fs.copyFileSync(src, dest);
|
|
683
|
-
} else if (effective === 'upgrade' && cat === 'tool') {
|
|
684
|
-
updated.push(label);
|
|
685
|
-
fs.copyFileSync(src, dest);
|
|
686
|
-
} else {
|
|
687
|
-
skipped.push(label);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// Process .code-flow/ (core)
|
|
693
|
-
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
|
|
694
|
-
|
|
695
|
-
// Process Claude adapter
|
|
696
|
-
if (platform === 'claude') {
|
|
697
|
-
installAdapterFile({
|
|
698
|
-
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
699
|
-
dest: path.join(cwd, 'CLAUDE.md'),
|
|
700
|
-
label: 'CLAUDE.md',
|
|
701
|
-
mode: platformMode,
|
|
702
|
-
mergeFn: mergeClaudeMd,
|
|
703
|
-
toolOnUpgrade: false,
|
|
704
|
-
results,
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
|
|
708
|
-
processDir(
|
|
709
|
-
path.join(adaptersDir, 'claude', 'commands'),
|
|
710
|
-
path.join(cwd, '.claude', 'commands'),
|
|
711
|
-
'.claude/commands', platformMode
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
installAdapterFile({
|
|
715
|
-
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
716
|
-
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
717
|
-
label: '.claude/settings.local.json',
|
|
718
|
-
mode: platformMode,
|
|
719
|
-
mergeFn: mergeSettingsJson,
|
|
720
|
-
toolOnUpgrade: false,
|
|
721
|
-
results,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Process Costrict adapter
|
|
726
|
-
if (platform === 'costrict') {
|
|
727
|
-
installAdapterFile({
|
|
728
|
-
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
729
|
-
dest: path.join(cwd, 'CLAUDE.md'),
|
|
730
|
-
label: 'CLAUDE.md',
|
|
731
|
-
mode: platformMode,
|
|
732
|
-
mergeFn: mergeClaudeMd,
|
|
733
|
-
toolOnUpgrade: false,
|
|
734
|
-
results,
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
fs.mkdirSync(path.join(cwd, '.costrict', 'commands'), { recursive: true });
|
|
738
|
-
processDir(
|
|
739
|
-
path.join(adaptersDir, 'costrict', 'commands'),
|
|
740
|
-
path.join(cwd, '.costrict', 'commands'),
|
|
741
|
-
'.costrict/commands', platformMode
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
installAdapterFile({
|
|
745
|
-
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
746
|
-
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
747
|
-
label: '.costrict/settings.local.json',
|
|
748
|
-
mode: platformMode,
|
|
749
|
-
mergeFn: mergeSettingsJson,
|
|
750
|
-
toolOnUpgrade: false,
|
|
751
|
-
results,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Process Codex adapter
|
|
756
|
-
if (platform === 'codex') {
|
|
757
|
-
installAdapterFile({
|
|
758
|
-
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
759
|
-
dest: path.join(cwd, 'AGENTS.md'),
|
|
760
|
-
label: 'AGENTS.md',
|
|
761
|
-
mode: platformMode,
|
|
762
|
-
mergeFn: mergeClaudeMd,
|
|
763
|
-
toolOnUpgrade: false,
|
|
764
|
-
results,
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
installAdapterFile({
|
|
768
|
-
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
769
|
-
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
770
|
-
label: '.codex/hooks.json',
|
|
771
|
-
mode: platformMode,
|
|
772
|
-
mergeFn: mergeSettingsJson,
|
|
773
|
-
toolOnUpgrade: false,
|
|
774
|
-
results,
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
installAdapterFile({
|
|
778
|
-
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
779
|
-
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
780
|
-
label: '.codex/config.toml',
|
|
781
|
-
mode: platformMode,
|
|
782
|
-
mergeFn: mergeCodexConfigToml,
|
|
783
|
-
toolOnUpgrade: false,
|
|
784
|
-
results,
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
// Project-level .agents/skills/ (version-controlled, committed to repo)
|
|
788
|
-
processDir(
|
|
789
|
-
path.join(adaptersDir, 'codex', 'skills'),
|
|
790
|
-
path.join(cwd, '.agents', 'skills'),
|
|
791
|
-
'.agents/skills', platformMode
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Process OpenCode adapter
|
|
796
|
-
if (platform === 'opencode') {
|
|
797
|
-
installAdapterFile({
|
|
798
|
-
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
799
|
-
dest: path.join(cwd, 'AGENTS.md'),
|
|
800
|
-
label: 'AGENTS.md',
|
|
801
|
-
mode: platformMode,
|
|
802
|
-
mergeFn: mergeClaudeMd,
|
|
803
|
-
toolOnUpgrade: false,
|
|
804
|
-
results,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
// Plugin files under .opencode/plugins/code-flow/
|
|
808
|
-
processDir(
|
|
809
|
-
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
810
|
-
path.join(cwd, '.opencode', 'plugins'),
|
|
811
|
-
'.opencode/plugins', platformMode
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
// Stamp main package version into the plugin's package.json
|
|
815
|
-
const pluginPkgPath = path.join(cwd, '.opencode', 'plugins', 'code-flow', 'package.json');
|
|
816
|
-
if (fs.existsSync(pluginPkgPath)) {
|
|
817
|
-
const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf8'));
|
|
818
|
-
pluginPkg.version = pkg.version;
|
|
819
|
-
fs.writeFileSync(pluginPkgPath, JSON.stringify(pluginPkg, null, 2) + '\n');
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Command files under .opencode/commands/
|
|
823
|
-
processDir(
|
|
824
|
-
path.join(adaptersDir, 'opencode', 'commands'),
|
|
825
|
-
path.join(cwd, '.opencode', 'commands'),
|
|
826
|
-
'.opencode/commands', platformMode
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
installAdapterFile({
|
|
830
|
-
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
831
|
-
dest: path.join(cwd, 'opencode.json'),
|
|
832
|
-
label: 'opencode.json',
|
|
833
|
-
mode: platformMode,
|
|
834
|
-
mergeFn: mergeOpencodeJson,
|
|
835
|
-
toolOnUpgrade: false,
|
|
836
|
-
results,
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Merge config.yml on upgrade
|
|
841
|
-
const configSrc = path.join(coreDir, 'code-flow', 'config.yml');
|
|
842
|
-
const configDest = path.join(cwd, '.code-flow', 'config.yml');
|
|
843
|
-
if (mode === 'upgrade' && fs.existsSync(configDest) && fs.existsSync(configSrc)) {
|
|
844
|
-
const added = mergeConfigYml(configSrc, configDest);
|
|
845
|
-
if (added.length > 0) {
|
|
846
|
-
// Replace the skipped entry with merged
|
|
847
|
-
const idx = skipped.indexOf('.code-flow/config.yml');
|
|
848
|
-
if (idx !== -1) skipped.splice(idx, 1);
|
|
849
|
-
merged.push(`.code-flow/config.yml — added: ${added.join(', ')}`);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
|
|
854
|
-
removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
|
|
855
|
-
|
|
856
|
-
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
857
|
-
if (mode === 'upgrade' || mode === 'force') {
|
|
858
|
-
removeOrphanFiles(cwd, removed);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Install pyyaml — probe first, fall back through PEP 668 strategies
|
|
862
|
-
ensurePyYaml();
|
|
863
|
-
|
|
864
|
-
// Write version (legacy global file kept for compatibility) + per-platform records
|
|
865
|
-
writeVersion(cwd, pkg.version);
|
|
866
|
-
adapterVersions.adapters[platform] = pkg.version;
|
|
867
|
-
adapterVersions.core = pkg.version;
|
|
868
|
-
writeAdapterVersions(cwd, adapterVersions);
|
|
869
|
-
|
|
870
|
-
// Output summary
|
|
871
|
-
if (mode === 'upgrade') {
|
|
872
|
-
process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
|
|
873
|
-
} else if (mode === 'current' && platformMode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
874
|
-
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
875
|
-
process.exit(0);
|
|
876
|
-
} else if (mode === 'force') {
|
|
877
|
-
process.stdout.write(`\ncode-flow v${pkg.version} force-initialized!\n\n`);
|
|
878
|
-
} else {
|
|
879
|
-
process.stdout.write(`\ncode-flow v${pkg.version} initialized!\n\n`);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
if (updated.length > 0) {
|
|
883
|
-
process.stdout.write('Updated (tool-managed):\n');
|
|
884
|
-
for (const f of updated) process.stdout.write(` ↑ ${f}\n`);
|
|
885
|
-
}
|
|
886
|
-
if (merged.length > 0) {
|
|
887
|
-
process.stdout.write('Merged (new sections added):\n');
|
|
888
|
-
for (const f of merged) process.stdout.write(` ⊕ ${f}\n`);
|
|
889
|
-
}
|
|
890
|
-
if (created.length > 0) {
|
|
891
|
-
process.stdout.write('Created:\n');
|
|
892
|
-
for (const f of created) process.stdout.write(` + ${f}\n`);
|
|
893
|
-
}
|
|
894
|
-
if (skipped.length > 0) {
|
|
895
|
-
process.stdout.write('Skipped (user-customized):\n');
|
|
896
|
-
for (const f of skipped) process.stdout.write(` · ${f}\n`);
|
|
897
|
-
}
|
|
898
|
-
if (removed.length > 0) {
|
|
899
|
-
process.stdout.write('Removed (deprecated):\n');
|
|
900
|
-
for (const f of removed) process.stdout.write(` ✕ ${f}\n`);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
process.stdout.write('\nNext steps:\n');
|
|
904
|
-
if (platform === 'codex') {
|
|
905
|
-
if (mode === 'fresh') {
|
|
906
|
-
process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
|
|
907
|
-
process.stdout.write(' 2. Run /hooks in Codex CLI and trust the code-flow hooks if prompted\n');
|
|
908
|
-
process.stdout.write(' 3. Run $cf-init in Codex CLI to auto-scan and populate specs\n');
|
|
909
|
-
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
910
|
-
} else {
|
|
911
|
-
process.stdout.write(' Run /hooks in Codex CLI and trust changed code-flow hooks if prompted\n');
|
|
912
|
-
process.stdout.write(' Run $cf-learn in Codex CLI to update specs with project conventions\n');
|
|
913
|
-
process.stdout.write(' Run $cf-learn --map to update retrieval maps\n');
|
|
914
|
-
}
|
|
915
|
-
} else if (platform === 'costrict') {
|
|
916
|
-
if (mode === 'fresh') {
|
|
917
|
-
process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
|
|
918
|
-
process.stdout.write(' 2. Run /cf-init in Costrict to auto-scan and populate specs\n');
|
|
919
|
-
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
920
|
-
} else {
|
|
921
|
-
process.stdout.write(' Run /cf-learn in Costrict to update specs with project conventions\n');
|
|
922
|
-
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
923
|
-
}
|
|
924
|
-
} else if (platform === 'opencode') {
|
|
925
|
-
if (mode === 'fresh') {
|
|
926
|
-
process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
|
|
927
|
-
process.stdout.write(' 2. Start opencode in this directory — the plugin auto-loads\n');
|
|
928
|
-
process.stdout.write(' 3. Run /cf-init in OpenCode to auto-scan and populate specs\n');
|
|
929
|
-
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
930
|
-
} else {
|
|
931
|
-
process.stdout.write(' Run /cf-learn in OpenCode to update specs with project conventions\n');
|
|
932
|
-
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
933
|
-
}
|
|
934
|
-
} else {
|
|
935
|
-
if (mode === 'fresh') {
|
|
936
|
-
process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
|
|
937
|
-
process.stdout.write(' 2. Run /cf-init in Claude Code to auto-scan and populate specs\n');
|
|
938
|
-
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
939
|
-
} else {
|
|
940
|
-
process.stdout.write(' Run /cf-learn in Claude Code to update specs with project conventions\n');
|
|
941
|
-
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
process.exit(0);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// --- CLI argument parsing ---
|
|
948
|
-
|
|
949
|
-
const args = process.argv.slice(2);
|
|
950
|
-
|
|
951
|
-
function argumentValue(values, name) {
|
|
952
|
-
const index = values.indexOf(name);
|
|
953
|
-
return index >= 0 ? values[index + 1] || '' : '';
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function migrationExitCode(result) {
|
|
957
|
-
if (result.status === 'rolled_back') return 4;
|
|
958
|
-
if (result.status === 'recovery_required') return 5;
|
|
959
|
-
if (result.status === 'prepared_blocked') return 3;
|
|
960
|
-
return 0;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function runMigrate(values) {
|
|
964
|
-
if (!values.includes('--spec-workflow')) fail('Error: migrate requires --spec-workflow.');
|
|
965
|
-
const actions = ['--dry-run', '--prepare', '--apply', '--rollback'].filter(action => values.includes(action));
|
|
966
|
-
if (actions.length !== 1) fail('Error: choose exactly one migrate action.');
|
|
967
|
-
let result;
|
|
968
|
-
try {
|
|
969
|
-
if (actions[0] === '--dry-run') result = specWorkflowMigration.preview(process.cwd());
|
|
970
|
-
if (actions[0] === '--prepare') result = specWorkflowMigration.prepare(process.cwd());
|
|
971
|
-
if (actions[0] === '--apply') {
|
|
972
|
-
const plan = argumentValue(values, '--plan');
|
|
973
|
-
if (!plan) fail('Error: --apply requires --plan <path>.');
|
|
974
|
-
result = specWorkflowMigration.apply(path.resolve(plan));
|
|
975
|
-
}
|
|
976
|
-
if (actions[0] === '--rollback') {
|
|
977
|
-
const id = argumentValue(values, '--rollback');
|
|
978
|
-
if (!id) fail('Error: --rollback requires a migration id.');
|
|
979
|
-
const plan = path.join(process.cwd(), '.code-flow', 'migrations', id, 'migration-plan.json');
|
|
980
|
-
result = specWorkflowMigration.rollback(plan);
|
|
981
|
-
}
|
|
982
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
983
|
-
process.exit(migrationExitCode(result));
|
|
984
|
-
} catch (error) {
|
|
985
|
-
process.stdout.write(`${JSON.stringify({ status: 'unresolved', error: String(error.message || error) })}\n`);
|
|
986
|
-
process.exit(3);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (args.includes('-v') || args.includes('--version')) {
|
|
991
|
-
process.stdout.write(`${pkg.version}\n`);
|
|
992
|
-
process.exit(0);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if (args.includes('-h') || args.includes('--help')) {
|
|
996
|
-
process.stdout.write(`code-flow v${pkg.version}\n\n`);
|
|
997
|
-
printUsage(process.stdout);
|
|
998
|
-
process.exit(0);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (args[0] === 'init') {
|
|
1002
|
-
const force = args.includes('--force');
|
|
1003
|
-
const rawPlatform = parsePlatform(args);
|
|
1004
|
-
const platform = rawPlatform === null ? 'claude' : rawPlatform;
|
|
1005
|
-
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
1006
|
-
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
1007
|
-
}
|
|
1008
|
-
runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir') });
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (args[0] === 'migrate') {
|
|
1012
|
-
runMigrate(args.slice(1));
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (args.length === 0) {
|
|
1016
|
-
fail('Error: missing command.');
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (args.includes('--path')) {
|
|
1020
|
-
fail('Error: --path is not supported; run init in the current directory.');
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
fail(`Error: unknown command "${args.join(' ')}".`);
|