@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.
Files changed (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +21 -9
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. 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(' ')}".`);