@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.
- package/README.md +73 -152
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +156 -1417
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +18 -21
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/acceptance-contract.md +0 -217
- package/docs/ci/acceptance-evidence.md +0 -77
- package/docs/migrations/runtime-0.7.0.md +0 -41
- package/docs/migrations/runtime-0.8.0.md +0 -154
- package/docs/migrations/runtime-0.9.0.md +0 -40
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/docs/spec-injection-hardening.md +0 -54
- package/docs/spec-injection-performance.json +0 -73
- package/docs/spec-optimization.md +0 -12
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -262
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -87
- package/src/adapters/claude/commands/cf-stats.md +0 -96
- package/src/adapters/claude/commands/cf-sync.md +0 -39
- package/src/adapters/claude/commands/cf-task/align.md +0 -331
- package/src/adapters/claude/commands/cf-task/archive.md +0 -155
- package/src/adapters/claude/commands/cf-task/block.md +0 -49
- package/src/adapters/claude/commands/cf-task/graph.md +0 -92
- package/src/adapters/claude/commands/cf-task/note.md +0 -248
- package/src/adapters/claude/commands/cf-task/plan.md +0 -477
- package/src/adapters/claude/commands/cf-task/prd.md +0 -173
- package/src/adapters/claude/commands/cf-task/start.md +0 -317
- package/src/adapters/claude/commands/cf-task/status.md +0 -71
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/claude/commands/cf-validate.md +0 -100
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -54
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
- package/src/adapters/commands.json +0 -115
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -262
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -87
- package/src/adapters/costrict/commands/cf-stats.md +0 -96
- package/src/adapters/costrict/commands/cf-sync.md +0 -39
- package/src/adapters/costrict/commands/cf-task/align.md +0 -331
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
- package/src/adapters/costrict/commands/cf-task/block.md +0 -49
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
- package/src/adapters/costrict/commands/cf-task/note.md +0 -248
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
- package/src/adapters/costrict/commands/cf-task/start.md +0 -317
- package/src/adapters/costrict/commands/cf-task/status.md +0 -71
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/costrict/commands/cf-validate.md +0 -100
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/install-plan.json +0 -100
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -91
- package/src/adapters/opencode/commands/cf-stats.md +0 -100
- package/src/adapters/opencode/commands/cf-sync.md +0 -44
- package/src/adapters/opencode/commands/cf-task/align.md +0 -331
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
- package/src/adapters/opencode/commands/cf-task/block.md +0 -54
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
- package/src/adapters/opencode/commands/cf-task/note.md +0 -253
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
- package/src/adapters/opencode/commands/cf-task/start.md +0 -317
- package/src/adapters/opencode/commands/cf-task/status.md +0 -76
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/opencode/commands/cf-validate.md +0 -105
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/adapters/platform-bindings.json +0 -370
- package/src/cli.js +0 -1076
- package/src/core/code-flow/config.yml +0 -161
- package/src/core/code-flow/runtime-commands.json +0 -259
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
- package/src/core/code-flow/scripts/cf_audit.py +0 -10
- package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
- package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
- package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
- package/src/core/code-flow/scripts/cf_feedback.py +0 -10
- package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
- package/src/core/code-flow/scripts/cf_stats.py +0 -10
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_sync.py +0 -10
- package/src/core/code-flow/scripts/cf_task_index.py +0 -10
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_validation.py +0 -10
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
- package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
- package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
- package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
- package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
- package/src/core/code-flow/scripts/runtime-resources.json +0 -29
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -59
- package/src/migrate/runtime.js +0 -213
- package/src/migrate/spec-workflow.js +0 -253
- package/src/runtime.js +0 -219
|
File without changes
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# validators 在每次 Stop 按“会话内编辑过的文件”匹配执行(quality_loop.stop_check)。
|
|
2
|
-
# 全量测试 / e2e 等慢命令标 `heavy: true`:Stop 与任务 finish 都跳过(deferred_heavy),
|
|
3
|
-
# 只由需求终验 verify-e2e / 归档与显式 /cf-validate 全量执行一次;结果按工作树内容指纹
|
|
4
|
-
# 缓存,同一内容不会重复执行。
|
|
5
|
-
validators:
|
|
6
|
-
- name: "Python 语法检查"
|
|
7
|
-
trigger: "**/*.py"
|
|
8
|
-
command: "python3 -m py_compile {files}"
|
|
9
|
-
timeout: 30000
|
|
10
|
-
on_fail: "检查语法错误"
|
|
11
|
-
|
|
12
|
-
- name: "TypeScript 类型检查"
|
|
13
|
-
trigger: "**/*.{ts,tsx}"
|
|
14
|
-
command: "npx tsc --noEmit"
|
|
15
|
-
timeout: 30000
|
|
16
|
-
skip_if_missing: true # 未安装 typescript 时跳过(degrade 记录),不阻断
|
|
17
|
-
on_fail: "检查类型定义,参见 specs/frontend/quality-standards.md"
|
|
18
|
-
|
|
19
|
-
- name: "Vue 类型检查"
|
|
20
|
-
trigger: "**/*.vue"
|
|
21
|
-
command: "npx vue-tsc --noEmit"
|
|
22
|
-
timeout: 30000
|
|
23
|
-
skip_if_missing: true
|
|
24
|
-
on_fail: "检查 Vue 组件类型定义,参见 specs/frontend/component-specs.md"
|
|
25
|
-
|
|
26
|
-
- name: "ESLint"
|
|
27
|
-
trigger: "**/*.{ts,tsx,js,jsx,vue}"
|
|
28
|
-
command: "npx eslint {files}"
|
|
29
|
-
timeout: 15000
|
|
30
|
-
skip_if_missing: true
|
|
31
|
-
on_fail: "运行 npx eslint --fix 自动修复,参见 specs/frontend/quality-standards.md"
|
|
32
|
-
|
|
33
|
-
- name: "Stylelint"
|
|
34
|
-
trigger: "**/*.{css,scss,less}"
|
|
35
|
-
command: "npx stylelint {files}"
|
|
36
|
-
timeout: 15000
|
|
37
|
-
skip_if_missing: true
|
|
38
|
-
on_fail: "运行 npx stylelint --fix 自动修复,参见 specs/frontend/component-specs.md"
|
|
39
|
-
|
|
40
|
-
- name: "前端单元测试"
|
|
41
|
-
trigger: "**/*.{ts,tsx,js,jsx,vue}"
|
|
42
|
-
command: "npx vitest run --reporter=dot"
|
|
43
|
-
timeout: 60000
|
|
44
|
-
heavy: true
|
|
45
|
-
on_fail: "前端测试失败,检查组件渲染和 mock 是否需要更新"
|
|
46
|
-
|
|
47
|
-
- name: "Python 类型检查"
|
|
48
|
-
trigger: "**/*.py"
|
|
49
|
-
command: "python3 -m mypy {files}"
|
|
50
|
-
timeout: 30000
|
|
51
|
-
skip_if_missing: true # 未安装 mypy 时跳过(degrade 记录),新项目不再被阻断
|
|
52
|
-
on_fail: "检查类型注解,参见 specs/backend/code-quality-performance.md"
|
|
53
|
-
|
|
54
|
-
- name: "Pytest"
|
|
55
|
-
trigger: "**/*.py"
|
|
56
|
-
command: "python3 -m pytest --tb=short -q"
|
|
57
|
-
timeout: 600000
|
|
58
|
-
heavy: true
|
|
59
|
-
on_fail: "测试失败,检查断言和 mock 是否需要更新"
|
package/src/migrate/runtime.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawnSync } = require('child_process');
|
|
6
|
-
const runtime = require('../runtime');
|
|
7
|
-
const pkg = require('../../package.json');
|
|
8
|
-
|
|
9
|
-
function guardVersion(root) {
|
|
10
|
-
const version = fs.readFileSync(path.join(root, '.code-flow/.version'), 'utf8').trim();
|
|
11
|
-
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`Invalid installed runtime version: ${version}`);
|
|
12
|
-
const installed = version.split('.').map(Number);
|
|
13
|
-
const target = pkg.version.split('.').map(Number);
|
|
14
|
-
for (let index = 0; index < 3; index++) {
|
|
15
|
-
if (installed[index] > target[index]) throw new Error(`Refusing runtime downgrade from ${version} to ${pkg.version}; update the CLI.`);
|
|
16
|
-
if (installed[index] < target[index]) return;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function walk(root, directory = '') {
|
|
21
|
-
const target = path.join(root, directory);
|
|
22
|
-
if (!fs.existsSync(target)) return [];
|
|
23
|
-
return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => {
|
|
24
|
-
if (entry.name === '__pycache__') return [];
|
|
25
|
-
const relative = path.join(directory, entry.name);
|
|
26
|
-
return entry.isDirectory() ? walk(root, relative) : [relative];
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function platforms(root) {
|
|
31
|
-
const markers = { claude: '.claude/commands/cf-init.md', codex: '.agents/skills/cf-init/SKILL.md',
|
|
32
|
-
costrict: '.costrict/commands/cf-init.md', opencode: '.opencode/commands/cf-init.md' };
|
|
33
|
-
const found = Object.entries(markers).filter(([, marker]) => fs.existsSync(path.join(root, marker))).map(([name]) => name);
|
|
34
|
-
if (!found.length) throw new Error('No installed platform found; run code-flow init first.');
|
|
35
|
-
return found;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function inputs(root, installed) {
|
|
39
|
-
const core = path.join(__dirname, '../core/code-flow');
|
|
40
|
-
const files = [...Object.keys(runtime.artifactSources(installed)), ...walk(core).map(file => `.code-flow/${file}`),
|
|
41
|
-
'.code-flow/scripts/cf_codex_user_prompt_hook.py',
|
|
42
|
-
'.code-flow/.version', '.code-flow/.adapter-versions.json', '.code-flow/.runtime-install.json',
|
|
43
|
-
'.code-flow/.gitignore', 'AGENTS.md', 'CLAUDE.md', 'opencode.json', '.codex/config.toml',
|
|
44
|
-
...Object.keys(runtime.hookContracts(installed))];
|
|
45
|
-
return [...new Set(files)].filter(file => fs.existsSync(path.join(root, file)));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function copyFile(root, target, relative) {
|
|
49
|
-
const destination = path.join(target, relative);
|
|
50
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
51
|
-
fs.copyFileSync(path.join(root, relative), destination);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function writeJson(file, value) {
|
|
55
|
-
fs.writeFileSync(`${file}.tmp`, JSON.stringify(value, null, 2) + '\n');
|
|
56
|
-
fs.renameSync(`${file}.tmp`, file);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function staleScripts(root, installed) {
|
|
60
|
-
// 0.9 moved the flat cf_*.py implementation modules under codeflow/{hooks,cli};
|
|
61
|
-
// upgrades must remove the old flat files, keeping the 22 path-frozen shims.
|
|
62
|
-
const current = new Set(Object.keys(runtime.artifactSources(installed)));
|
|
63
|
-
return walk(root, '.code-flow/scripts')
|
|
64
|
-
.filter(file => file.endsWith('.py'))
|
|
65
|
-
.filter(file => !current.has(file.replace(/\\/g, '/')));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function prepare(root, locked = false) {
|
|
69
|
-
guardVersion(root);
|
|
70
|
-
const installed = platforms(root);
|
|
71
|
-
const id = `runtime-${Date.now()}-${process.pid}`;
|
|
72
|
-
const workspace = path.join(root, '.code-flow/migrations', id);
|
|
73
|
-
const staging = path.join(workspace, 'staging');
|
|
74
|
-
fs.mkdirSync(staging, { recursive: true });
|
|
75
|
-
fs.mkdirSync(path.join(workspace, 'backup'));
|
|
76
|
-
const stale = staleScripts(root, installed);
|
|
77
|
-
const original = [...new Set([...inputs(root, installed), ...stale])];
|
|
78
|
-
const sources = Object.fromEntries(original.map(file => [file, runtime.digest(path.join(root, file))]));
|
|
79
|
-
const journal = { schema_version: 1, id, root, platforms: installed,
|
|
80
|
-
status: 'staging', sources, hashes: {}, deletions: [], written: [] };
|
|
81
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
82
|
-
if (locked) writeJson(path.join(root, '.code-flow/.runtime-migration.lock/owner.json'), { pid: process.pid, id });
|
|
83
|
-
for (const relative of original) {
|
|
84
|
-
copyFile(root, staging, relative);
|
|
85
|
-
copyFile(root, path.join(workspace, 'backup'), relative);
|
|
86
|
-
}
|
|
87
|
-
writeJson(path.join(staging, '.runtime-stage.json'), { root, id });
|
|
88
|
-
for (const platform of installed) {
|
|
89
|
-
const result = spawnSync(process.execPath, [path.join(__dirname, '../cli.js'), 'init', `--platform=${platform}`], {
|
|
90
|
-
cwd: staging, encoding: 'utf8', env: { ...process.env, CODE_FLOW_RUNTIME_STAGE: staging },
|
|
91
|
-
});
|
|
92
|
-
if (result.error || result.status !== 0) throw new Error(`Runtime staging failed: ${result.error || result.stderr || result.stdout}`);
|
|
93
|
-
}
|
|
94
|
-
for (const relative of stale) fs.rmSync(path.join(staging, relative), { force: true });
|
|
95
|
-
runtime.verify(staging);
|
|
96
|
-
const targets = walk(staging).filter(file => file !== '.runtime-stage.json' && !file.startsWith('.code-flow/backups/'));
|
|
97
|
-
const hashes = Object.fromEntries(targets.map(file => [file, runtime.digest(path.join(staging, file))]));
|
|
98
|
-
journal.deletions = original.filter(file => !fs.existsSync(path.join(staging, file)));
|
|
99
|
-
journal.hashes = hashes;
|
|
100
|
-
journal.status = 'prepared';
|
|
101
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
102
|
-
return { workspace, journal, staging };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function restore(workspace, journal) {
|
|
106
|
-
const order = [...journal.written].reverse();
|
|
107
|
-
for (const relative of order) {
|
|
108
|
-
const target = path.join(journal.root, relative);
|
|
109
|
-
const backup = path.join(workspace, 'backup', relative);
|
|
110
|
-
if (fs.existsSync(backup)) copyFile(path.join(workspace, 'backup'), journal.root, relative);
|
|
111
|
-
else fs.rmSync(target, { force: true });
|
|
112
|
-
}
|
|
113
|
-
journal.status = 'rolled_back';
|
|
114
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
115
|
-
return { status: journal.status, migration_id: journal.id };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function apply(root) {
|
|
119
|
-
let repairReason;
|
|
120
|
-
try {
|
|
121
|
-
runtime.verify(root, true);
|
|
122
|
-
return { status: 'already_migrated' };
|
|
123
|
-
} catch (error) {
|
|
124
|
-
repairReason = error.message;
|
|
125
|
-
}
|
|
126
|
-
const { workspace, journal, staging } = prepare(root, true);
|
|
127
|
-
try {
|
|
128
|
-
for (const [relative, hash] of Object.entries(journal.sources)) {
|
|
129
|
-
if (runtime.digest(path.join(root, relative)) !== hash) throw new Error(`Source changed during staging: ${relative}`);
|
|
130
|
-
}
|
|
131
|
-
journal.status = 'applying';
|
|
132
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
133
|
-
const targets = Object.keys(journal.hashes).filter(file => file !== '.code-flow/.version');
|
|
134
|
-
for (const relative of journal.deletions) {
|
|
135
|
-
journal.written.push(relative);
|
|
136
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
137
|
-
fs.rmSync(path.join(root, relative));
|
|
138
|
-
}
|
|
139
|
-
targets.push('.code-flow/.version');
|
|
140
|
-
for (const relative of targets) {
|
|
141
|
-
if (!journal.sources[relative] && fs.existsSync(path.join(root, relative))) throw new Error(`New target appeared: ${relative}`);
|
|
142
|
-
if (runtime.digest(path.join(staging, relative)) !== journal.hashes[relative]) throw new Error(`Staging drift: ${relative}`);
|
|
143
|
-
journal.written.push(relative);
|
|
144
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
145
|
-
copyFile(staging, root, relative);
|
|
146
|
-
}
|
|
147
|
-
runtime.verify(root, true);
|
|
148
|
-
journal.status = 'committed';
|
|
149
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
150
|
-
return { status: journal.status, migration_id: journal.id, platforms: journal.platforms, repair_reason: repairReason };
|
|
151
|
-
} catch (error) {
|
|
152
|
-
const result = restore(workspace, journal);
|
|
153
|
-
return { ...result, error: error.message };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function rollback(root, id) {
|
|
158
|
-
if (!/^runtime-\d+-\d+$/.test(id)) throw new Error('Invalid runtime migration id.');
|
|
159
|
-
const workspace = path.join(root, '.code-flow/migrations', id);
|
|
160
|
-
const journal = JSON.parse(fs.readFileSync(path.join(workspace, 'journal.json'), 'utf8'));
|
|
161
|
-
if (journal.root !== root) throw new Error('Migration belongs to a different project.');
|
|
162
|
-
if (journal.status === 'rolled_back') return { status: 'rolled_back', migration_id: id };
|
|
163
|
-
if (journal.status === 'committed') {
|
|
164
|
-
for (const [relative, hash] of Object.entries(journal.hashes)) {
|
|
165
|
-
if (runtime.digest(path.join(root, relative)) !== hash) throw new Error(`Rollback would overwrite subsequent changes: ${relative}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return restore(workspace, journal);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function withLock(root, values) {
|
|
172
|
-
const lock = path.join(root, '.code-flow/.runtime-migration.lock');
|
|
173
|
-
try {
|
|
174
|
-
fs.mkdirSync(lock);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
if (error.code !== 'EEXIST') throw error;
|
|
177
|
-
const owner = JSON.parse(fs.readFileSync(path.join(lock, 'owner.json'), 'utf8'));
|
|
178
|
-
let live = true;
|
|
179
|
-
try { process.kill(owner.pid, 0); } catch (signalError) {
|
|
180
|
-
if (signalError.code !== 'ESRCH') throw signalError;
|
|
181
|
-
live = false;
|
|
182
|
-
}
|
|
183
|
-
if (live) throw new Error('Another runtime migration is running.');
|
|
184
|
-
if (values[0] !== '--rollback' || values[1] !== owner.id) {
|
|
185
|
-
throw new Error(`Interrupted migration; run code-flow migrate --runtime --rollback ${owner.id}`);
|
|
186
|
-
}
|
|
187
|
-
fs.rmSync(lock, { recursive: true });
|
|
188
|
-
fs.mkdirSync(lock);
|
|
189
|
-
}
|
|
190
|
-
writeJson(path.join(lock, 'owner.json'), { pid: process.pid, id: '' });
|
|
191
|
-
try {
|
|
192
|
-
return values[0] === '--apply' ? apply(root) : rollback(root, values[1]);
|
|
193
|
-
} finally {
|
|
194
|
-
fs.rmSync(lock, { recursive: true });
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function run(root, values) {
|
|
199
|
-
if (values.length === 1 && values[0] === '--dry-run') {
|
|
200
|
-
const { workspace, journal } = prepare(root);
|
|
201
|
-
fs.rmSync(path.join(workspace, 'staging'), { recursive: true });
|
|
202
|
-
fs.rmSync(path.join(workspace, 'backup'), { recursive: true });
|
|
203
|
-
journal.status = 'previewed';
|
|
204
|
-
writeJson(path.join(workspace, 'journal.json'), journal);
|
|
205
|
-
return { status: journal.status, migration_id: journal.id, platforms: journal.platforms,
|
|
206
|
-
files: Object.keys(journal.hashes), apply: 'code-flow migrate --runtime --apply' };
|
|
207
|
-
}
|
|
208
|
-
if (values.length === 1 && values[0] === '--apply') return withLock(root, values);
|
|
209
|
-
if (values.length === 2 && values[0] === '--rollback') return withLock(root, values);
|
|
210
|
-
throw new Error('Choose --dry-run, --apply, or --rollback <id>.');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
module.exports = { run };
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const childProcess = require("node:child_process");
|
|
4
|
-
const crypto = require("node:crypto");
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
|
|
8
|
-
const PREFLIGHT = path.join(__dirname, "../core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py");
|
|
9
|
-
|
|
10
|
-
function walkFiles(root, directory = root) {
|
|
11
|
-
const files = [];
|
|
12
|
-
if (!fs.existsSync(directory)) return files;
|
|
13
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
14
|
-
const full = path.join(directory, entry.name);
|
|
15
|
-
const relative = path.relative(root, full);
|
|
16
|
-
if (entry.isDirectory() && relative === path.join(".code-flow", "migrations")) continue;
|
|
17
|
-
if (entry.isDirectory()) files.push(...walkFiles(root, full));
|
|
18
|
-
else files.push(relative);
|
|
19
|
-
}
|
|
20
|
-
return files.sort();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function migrationFiles(root) {
|
|
24
|
-
const entries = [".code-flow", ".claude", ".codex", ".costrict", ".opencode", ".agents", "AGENTS.md", "CLAUDE.md"];
|
|
25
|
-
const files = [];
|
|
26
|
-
for (const entry of entries) {
|
|
27
|
-
const target = path.join(root, entry);
|
|
28
|
-
if (!fs.existsSync(target)) continue;
|
|
29
|
-
if (fs.statSync(target).isDirectory()) files.push(...walkFiles(root, target));
|
|
30
|
-
else files.push(entry);
|
|
31
|
-
}
|
|
32
|
-
return [...new Set(files)].sort();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function hash(data) {
|
|
36
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function sourceHashes(root) {
|
|
40
|
-
const result = {};
|
|
41
|
-
for (const relative of migrationFiles(root)) {
|
|
42
|
-
result[relative] = hash(fs.readFileSync(path.join(root, relative)));
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function pythonCommand() {
|
|
48
|
-
return process.env.PYTHON || "python3";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function preview(root) {
|
|
52
|
-
const project = fs.realpathSync(root);
|
|
53
|
-
const result = childProcess.spawnSync(
|
|
54
|
-
pythonCommand(), [PREFLIGHT, "preflight", "--root", project, "--json"],
|
|
55
|
-
{ encoding: "utf8", shell: false }
|
|
56
|
-
);
|
|
57
|
-
if (result.status !== 0) throw new Error(result.stderr || result.stdout || "preflight failed");
|
|
58
|
-
return JSON.parse(result.stdout);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function migrationId(root, hashes) {
|
|
62
|
-
const seed = `${root}\0${JSON.stringify(hashes)}\0${Date.now()}`;
|
|
63
|
-
return `spec-workflow-${hash(seed).slice(0, 12)}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function copyBackup(root, backup, hashes) {
|
|
67
|
-
for (const relative of Object.keys(hashes)) {
|
|
68
|
-
const target = path.join(backup, relative);
|
|
69
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
70
|
-
fs.copyFileSync(path.join(root, relative), target);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writeJson(target, value) {
|
|
75
|
-
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function prepare(root) {
|
|
79
|
-
const project = fs.realpathSync(root);
|
|
80
|
-
const plan = preview(project);
|
|
81
|
-
const hashes = sourceHashes(project);
|
|
82
|
-
const id = migrationId(project, hashes);
|
|
83
|
-
const workspace = path.join(project, ".code-flow", "migrations", id);
|
|
84
|
-
const backup = path.join(workspace, "backup");
|
|
85
|
-
const staging = path.join(workspace, "staging");
|
|
86
|
-
fs.mkdirSync(backup, { recursive: true });
|
|
87
|
-
fs.mkdirSync(staging, { recursive: true });
|
|
88
|
-
copyBackup(project, backup, hashes);
|
|
89
|
-
const preparedPlan = { ...plan, migration_id: id, project_root: project, source_hashes: hashes };
|
|
90
|
-
const status = plan.unresolved.length ? "prepared_blocked" : "prepared";
|
|
91
|
-
writeJson(path.join(workspace, "migration-plan.json"), preparedPlan);
|
|
92
|
-
writeJson(path.join(workspace, "journal.json"), { version: 1, migration_id: id, status, operations: [] });
|
|
93
|
-
return { migration_id: id, status, workspace, plan: path.join(workspace, "migration-plan.json") };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function readJson(target) {
|
|
97
|
-
return JSON.parse(fs.readFileSync(target, "utf8"));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function journalPaths(planPath) {
|
|
101
|
-
const workspace = path.dirname(planPath);
|
|
102
|
-
return { workspace, journal: path.join(workspace, "journal.json"), staging: path.join(workspace, "staging"), backup: path.join(workspace, "backup") };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function sameHashes(first, second) {
|
|
106
|
-
return JSON.stringify(first) === JSON.stringify(second);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function runStaging(planPath, locations) {
|
|
110
|
-
const result = childProcess.spawnSync(
|
|
111
|
-
pythonCommand(), [PREFLIGHT, "stage", "--plan", planPath, "--staging", locations.staging, "--json"],
|
|
112
|
-
{ encoding: "utf8", shell: false }
|
|
113
|
-
);
|
|
114
|
-
if (result.status !== 0) throw new Error(result.stdout || result.stderr || "staging failed");
|
|
115
|
-
return JSON.parse(result.stdout);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function verifyStaging(staging, manifest) {
|
|
119
|
-
for (const item of manifest.targets) {
|
|
120
|
-
const target = path.join(staging, item.path);
|
|
121
|
-
if (!fs.existsSync(target) || hash(fs.readFileSync(target)) !== item.sha256) {
|
|
122
|
-
throw new Error(`staging hash mismatch: ${item.path}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function persistJournal(target, journal, status) {
|
|
128
|
-
journal.status = status;
|
|
129
|
-
writeJson(target, journal);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function targetEntries(manifest) {
|
|
133
|
-
return manifest.targets.filter((item) => item.path !== ".code-flow/.version");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function commitEntry(project, staging, item, journal, journalPath) {
|
|
137
|
-
const source = path.join(staging, item.path);
|
|
138
|
-
const target = path.join(project, item.path);
|
|
139
|
-
journal.operations.push({ path: item.path, status: "renaming" });
|
|
140
|
-
writeJson(journalPath, journal);
|
|
141
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
142
|
-
if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
|
143
|
-
fs.renameSync(source, target);
|
|
144
|
-
journal.operations[journal.operations.length - 1].status = "committed";
|
|
145
|
-
writeJson(journalPath, journal);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function obsoletePaths(plan, manifest) {
|
|
149
|
-
const targets = new Set(manifest.targets.map((item) => item.path));
|
|
150
|
-
return Object.keys(plan.source_hashes).filter((item) => item.startsWith(".code-flow/") && item !== ".code-flow/.version" && !targets.has(item));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function removeObsolete(project, paths) {
|
|
154
|
-
for (const relative of paths) fs.rmSync(path.join(project, relative), { recursive: true, force: true });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function restoreBackup(plan, locations, manifest) {
|
|
158
|
-
for (const item of manifest.targets || []) {
|
|
159
|
-
if (item.path !== ".code-flow/.version") fs.rmSync(path.join(plan.project_root, item.path), { recursive: true, force: true });
|
|
160
|
-
}
|
|
161
|
-
for (const relative of Object.keys(plan.source_hashes)) {
|
|
162
|
-
const source = path.join(locations.backup, relative);
|
|
163
|
-
const target = path.join(plan.project_root, relative);
|
|
164
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
165
|
-
fs.copyFileSync(source, target);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function rollback(planPath, error = "requested") {
|
|
170
|
-
const plan = readJson(planPath);
|
|
171
|
-
const locations = journalPaths(planPath);
|
|
172
|
-
const journal = readJson(locations.journal);
|
|
173
|
-
try {
|
|
174
|
-
restoreBackup(plan, locations, journal.manifest || { targets: [] });
|
|
175
|
-
// A rolled-back plan must be re-appliable: stale staging content makes the
|
|
176
|
-
// next `stage` refuse ("staging root must be a pre-created empty directory").
|
|
177
|
-
if (fs.existsSync(locations.staging)) fs.rmSync(locations.staging, { recursive: true, force: true });
|
|
178
|
-
fs.mkdirSync(locations.staging, { recursive: true });
|
|
179
|
-
journal.error = String(error);
|
|
180
|
-
journal.operations = [];
|
|
181
|
-
persistJournal(locations.journal, journal, "rolled_back");
|
|
182
|
-
return { status: "rolled_back", migration_id: plan.migration_id };
|
|
183
|
-
} catch (restoreError) {
|
|
184
|
-
journal.error = String(restoreError);
|
|
185
|
-
persistJournal(locations.journal, journal, "recovery_required");
|
|
186
|
-
return { status: "recovery_required", migration_id: plan.migration_id };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function targetsMatch(project, manifest) {
|
|
191
|
-
return targetEntries(manifest).every((item) => {
|
|
192
|
-
const target = path.join(project, item.path);
|
|
193
|
-
return fs.existsSync(target) && hash(fs.readFileSync(target)) === item.sha256;
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function finishVersion(plan, journal, journalPath) {
|
|
198
|
-
fs.writeFileSync(path.join(plan.project_root, ".code-flow/.version"), "0.6.0", "utf8");
|
|
199
|
-
persistJournal(journalPath, journal, "committed");
|
|
200
|
-
return { status: "committed", migration_id: plan.migration_id };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function recoverCommitting(plan, journal, locations, options) {
|
|
204
|
-
if (!targetsMatch(plan.project_root, journal.manifest)) return rollback(path.join(locations.workspace, "migration-plan.json"), "target hash mismatch");
|
|
205
|
-
if (options.interruptBeforeVersion) return { status: "committing", migration_id: plan.migration_id };
|
|
206
|
-
return finishVersion(plan, journal, locations.journal);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function apply(planPath, options = {}) {
|
|
210
|
-
const plan = readJson(planPath);
|
|
211
|
-
const locations = journalPaths(planPath);
|
|
212
|
-
// Guard: a plan carries an absolute project_root; applying a plan from a
|
|
213
|
-
// different project (e.g. a copy or a stale path) would hash-check and
|
|
214
|
-
// mutate that other project instead of cwd. The CLI always passes
|
|
215
|
-
// projectRoot; direct library callers keep the previous behavior.
|
|
216
|
-
if (options.projectRoot) {
|
|
217
|
-
const expectedRoot = fs.realpathSync(options.projectRoot);
|
|
218
|
-
if (fs.realpathSync(plan.project_root) !== expectedRoot) {
|
|
219
|
-
throw new Error(`migration plan targets ${plan.project_root}, not ${expectedRoot}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
const versionPath = path.join(plan.project_root, ".code-flow/.version");
|
|
223
|
-
if (fs.existsSync(versionPath) && fs.readFileSync(versionPath, "utf8").trim() === "0.6.0") return { status: "already_migrated", migration_id: plan.migration_id };
|
|
224
|
-
const journal = readJson(locations.journal);
|
|
225
|
-
if (journal.status === "committing") return recoverCommitting(plan, journal, locations, options);
|
|
226
|
-
if (plan.unresolved.length) throw new Error("migration plan has unresolved items");
|
|
227
|
-
if (!sameHashes(sourceHashes(plan.project_root), plan.source_hashes)) throw new Error("source hash drift");
|
|
228
|
-
try {
|
|
229
|
-
const manifest = runStaging(planPath, locations);
|
|
230
|
-
verifyStaging(locations.staging, manifest);
|
|
231
|
-
journal.manifest = manifest;
|
|
232
|
-
persistJournal(locations.journal, journal, "committing");
|
|
233
|
-
let completed = 0;
|
|
234
|
-
for (const item of targetEntries(manifest)) {
|
|
235
|
-
if (options.failAfter === completed) throw new Error(`injected rename failure ${completed}`);
|
|
236
|
-
if (options.exdevAfter === completed) {
|
|
237
|
-
const exdev = new Error("injected EXDEV");
|
|
238
|
-
exdev.code = "EXDEV";
|
|
239
|
-
throw exdev;
|
|
240
|
-
}
|
|
241
|
-
commitEntry(plan.project_root, locations.staging, item, journal, locations.journal);
|
|
242
|
-
completed += 1;
|
|
243
|
-
}
|
|
244
|
-
removeObsolete(plan.project_root, obsoletePaths(plan, manifest));
|
|
245
|
-
removeObsolete(plan.project_root, manifest.deletions || []);
|
|
246
|
-
if (options.interruptBeforeVersion) return { status: "committing", migration_id: plan.migration_id };
|
|
247
|
-
return finishVersion(plan, journal, locations.journal);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
return rollback(planPath, error);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
module.exports = { apply, preview, prepare, rollback, sourceHashes, walkFiles };
|