@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
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(' ')}".`);
|