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