@michaelxwb/code-flow 0.6.5 → 0.8.1
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 +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- 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 +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
package/src/cli.js
CHANGED
|
@@ -8,10 +8,17 @@ const { spawnSync } = require('child_process');
|
|
|
8
8
|
|
|
9
9
|
const pkg = require('../package.json');
|
|
10
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'));
|
|
11
16
|
|
|
12
17
|
const usage = [
|
|
13
18
|
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>] [--backup-dir=<path>]',
|
|
14
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]`,
|
|
15
22
|
' code-flow -v | --version',
|
|
16
23
|
' code-flow -h | --help'
|
|
17
24
|
].join('\n');
|
|
@@ -45,6 +52,7 @@ function fileCategory(relPath) {
|
|
|
45
52
|
if (p.startsWith('.agents/skills/')) return 'tool';
|
|
46
53
|
if (p.startsWith('.opencode/commands/')) return 'tool';
|
|
47
54
|
if (p.startsWith('.code-flow/scripts/')) return 'tool';
|
|
55
|
+
if (p === '.code-flow/runtime-commands.json') return 'tool';
|
|
48
56
|
if (p === 'CLAUDE.md') return 'merge';
|
|
49
57
|
if (p === 'AGENTS.md') return 'merge';
|
|
50
58
|
if (p === '.claude/settings.local.json') return 'merge';
|
|
@@ -111,13 +119,9 @@ function writeAdapterVersions(cwd, versions) {
|
|
|
111
119
|
|
|
112
120
|
// Marker dirs proving a platform was installed before per-platform tracking.
|
|
113
121
|
function platformFilesExist(cwd, platform) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
costrict: ['.costrict/commands'],
|
|
118
|
-
opencode: ['.opencode/commands'],
|
|
119
|
-
};
|
|
120
|
-
return (markers[platform] || []).some(rel => fs.existsSync(path.join(cwd, rel)));
|
|
122
|
+
const spec = installPlan.platforms[platform];
|
|
123
|
+
const markers = spec ? spec.markers : [];
|
|
124
|
+
return markers.some(rel => fs.existsSync(path.join(cwd, rel)));
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
// --- Merge functions ---
|
|
@@ -129,11 +133,38 @@ function maskFencedCode(text) {
|
|
|
129
133
|
return text.replace(/```[\s\S]*?```/g, block => block.replace(/[^\n]/g, ' '));
|
|
130
134
|
}
|
|
131
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
|
+
|
|
132
161
|
function mergeClaudeMd(srcFile, destFile) {
|
|
133
162
|
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
134
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;
|
|
135
166
|
const srcMasked = maskFencedCode(srcText);
|
|
136
|
-
const destMasked = maskFencedCode(
|
|
167
|
+
const destMasked = maskFencedCode(baseText);
|
|
137
168
|
|
|
138
169
|
const sectionRegex = /^## .+$/gm;
|
|
139
170
|
const srcSections = [];
|
|
@@ -149,20 +180,37 @@ function mergeClaudeMd(srcFile, destFile) {
|
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
const missing = srcSections.filter(s => !destSectionSet.has(s.heading));
|
|
152
|
-
if (missing.length === 0) return
|
|
183
|
+
if (missing.length === 0) return repaired;
|
|
153
184
|
|
|
154
185
|
const additions = [];
|
|
155
186
|
for (const { heading, index } of missing) {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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);
|
|
158
191
|
const nextRel = srcMasked.slice(index + heading.length).search(/^## /m);
|
|
159
|
-
const end = nextRel === -1
|
|
160
|
-
|
|
192
|
+
const end = nextRel === -1
|
|
193
|
+
? srcText.length
|
|
194
|
+
: managedSliceStart(srcMasked, index + heading.length + nextRel);
|
|
195
|
+
additions.push(srcText.slice(start, end).trimEnd());
|
|
161
196
|
}
|
|
162
197
|
|
|
163
|
-
const merged =
|
|
198
|
+
const merged = baseText.trimEnd() + '\n\n' + additions.join('\n\n') + '\n';
|
|
164
199
|
fs.writeFileSync(destFile, merged);
|
|
165
|
-
return missing.map(m => m.heading);
|
|
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));
|
|
166
214
|
}
|
|
167
215
|
|
|
168
216
|
// Deep-merge a single hook event array. Each item has shape
|
|
@@ -171,9 +219,9 @@ function mergeClaudeMd(srcFile, destFile) {
|
|
|
171
219
|
// dest items; for existing items we union the inner hooks array by command
|
|
172
220
|
// string. User-added items / commands are never removed or rewritten — the
|
|
173
221
|
// merge is purely additive, in line with cli/code-standards.md "合并策略
|
|
174
|
-
// 必须保证用户自定义内容不被覆盖".
|
|
175
|
-
//
|
|
176
|
-
//
|
|
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).
|
|
177
225
|
function mergeHookEventArray(srcArr, destArr, eventName, added) {
|
|
178
226
|
if (!Array.isArray(srcArr) || !Array.isArray(destArr)) return;
|
|
179
227
|
for (const srcItem of srcArr) {
|
|
@@ -190,6 +238,15 @@ function mergeHookEventArray(srcArr, destArr, eventName, added) {
|
|
|
190
238
|
continue;
|
|
191
239
|
}
|
|
192
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
|
+
}
|
|
193
250
|
const destCmds = new Set(
|
|
194
251
|
destItem.hooks.map(h => (h && typeof h.command === 'string' ? h.command : ''))
|
|
195
252
|
);
|
|
@@ -237,6 +294,19 @@ function mergeSettingsJson(srcFile, destFile) {
|
|
|
237
294
|
return added;
|
|
238
295
|
}
|
|
239
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
|
+
|
|
240
310
|
// OpenCode v2 merge: migrate the v1 `plugin` key to `plugins`, drop the
|
|
241
311
|
// auto-discovered local path (`.opencode/plugins/*` loads without config),
|
|
242
312
|
// and union template `plugins` entries without overwriting user content.
|
|
@@ -394,6 +464,8 @@ function collectFiles(dir, base) {
|
|
|
394
464
|
if (!fs.existsSync(dir)) return results;
|
|
395
465
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
396
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;
|
|
397
469
|
const rel = path.join(base, entry.name);
|
|
398
470
|
if (entry.isDirectory()) {
|
|
399
471
|
results.push(...collectFiles(path.join(dir, entry.name), rel));
|
|
@@ -408,6 +480,7 @@ function copyDirRecursive(srcDir, destDir, overwrite) {
|
|
|
408
480
|
fs.mkdirSync(destDir, { recursive: true });
|
|
409
481
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
410
482
|
for (const entry of entries) {
|
|
483
|
+
if (entry.name === '.DS_Store' || entry.name === 'Thumbs.db') continue;
|
|
411
484
|
const srcPath = path.join(srcDir, entry.name);
|
|
412
485
|
const destPath = path.join(destDir, entry.name);
|
|
413
486
|
if (entry.isDirectory()) {
|
|
@@ -512,9 +585,48 @@ const MANAGED_LEGACY_SKILL_NAMES = new Set([
|
|
|
512
585
|
'cf-validate.md',
|
|
513
586
|
]);
|
|
514
587
|
|
|
515
|
-
//
|
|
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
|
+
}
|
|
516
627
|
|
|
517
628
|
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
629
|
+
|
|
518
630
|
// Bounded waits: each attempt carries its own timeout inside a total budget,
|
|
519
631
|
// with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
|
|
520
632
|
const PIP_PER_ATTEMPT_MS = 60000;
|
|
@@ -553,10 +665,26 @@ function ensurePyYaml(spawnFn, logFn, nowFn) {
|
|
|
553
665
|
|
|
554
666
|
// --- Adapter file installer: shared by every platform branch ---
|
|
555
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
|
+
|
|
556
682
|
// Install one adapter file with the standard create / force / upgrade /
|
|
557
|
-
// skip semantics. `mergeFn` is consulted
|
|
558
|
-
//
|
|
559
|
-
//
|
|
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.
|
|
560
688
|
function installAdapterFile(opts) {
|
|
561
689
|
const { src, dest, label, mode, mergeFn, toolOnUpgrade, results } = opts;
|
|
562
690
|
if (!fs.existsSync(dest)) {
|
|
@@ -570,6 +698,15 @@ function installAdapterFile(opts) {
|
|
|
570
698
|
fs.copyFileSync(src, dest);
|
|
571
699
|
return;
|
|
572
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
|
+
}
|
|
573
710
|
if (mode === 'upgrade') {
|
|
574
711
|
if (toolOnUpgrade) {
|
|
575
712
|
results.updated.push(label);
|
|
@@ -577,7 +714,7 @@ function installAdapterFile(opts) {
|
|
|
577
714
|
return;
|
|
578
715
|
}
|
|
579
716
|
if (mergeFn) {
|
|
580
|
-
const added = mergeFn
|
|
717
|
+
const added = runMerge(mergeFn, src, dest, label);
|
|
581
718
|
if (added.length > 0) {
|
|
582
719
|
results.merged.push(`${label} — added: ${added.join(', ')}`);
|
|
583
720
|
} else {
|
|
@@ -614,263 +751,144 @@ function parseStringFlag(args, name) {
|
|
|
614
751
|
return '';
|
|
615
752
|
}
|
|
616
753
|
|
|
617
|
-
// ---
|
|
754
|
+
// --- Install orchestration driven by adapters/install-plan.json ---
|
|
618
755
|
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
});
|
|
626
784
|
}
|
|
627
|
-
|
|
785
|
+
}
|
|
628
786
|
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
const
|
|
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
|
+
}
|
|
632
805
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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) {
|
|
638
845
|
const adapterVersions = readAdapterVersions(cwd);
|
|
639
846
|
const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
|
|
640
847
|
? adapterVersions.adapters[platform]
|
|
641
848
|
: null;
|
|
642
|
-
|
|
643
|
-
if (force)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
platformMode = 'fresh';
|
|
647
|
-
} else if (
|
|
849
|
+
if (options.runtimeMigration) return 'upgrade';
|
|
850
|
+
if (force) return 'force';
|
|
851
|
+
if (!adapterPrev && !platformFilesExist(cwd, platform)) return 'fresh';
|
|
852
|
+
if (
|
|
648
853
|
(adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
|
|
649
854
|
(!adapterPrev && platformFilesExist(cwd, platform)) ||
|
|
650
855
|
mode === 'upgrade'
|
|
651
856
|
) {
|
|
652
|
-
|
|
653
|
-
} else {
|
|
654
|
-
platformMode = 'current';
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const created = [];
|
|
658
|
-
const updated = [];
|
|
659
|
-
const merged = [];
|
|
660
|
-
const skipped = [];
|
|
661
|
-
const removed = [];
|
|
662
|
-
const results = { created, updated, merged, skipped };
|
|
663
|
-
|
|
664
|
-
// Track + copy helper for directory trees. dirMode is resolved per area:
|
|
665
|
-
// core files follow the global mode, platform files follow platformMode.
|
|
666
|
-
const processDir = (srcDir, destDir, prefix, dirMode) => {
|
|
667
|
-
const effective = dirMode || mode;
|
|
668
|
-
if (!fs.existsSync(srcDir)) return;
|
|
669
|
-
const files = collectFiles(srcDir, '');
|
|
670
|
-
for (const rel of files) {
|
|
671
|
-
const src = path.join(srcDir, rel);
|
|
672
|
-
const dest = path.join(destDir, rel);
|
|
673
|
-
const label = path.join(prefix, rel);
|
|
674
|
-
const cat = fileCategory(label);
|
|
675
|
-
|
|
676
|
-
if (!fs.existsSync(dest)) {
|
|
677
|
-
created.push(label);
|
|
678
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
679
|
-
fs.copyFileSync(src, dest);
|
|
680
|
-
} else if (effective === 'force') {
|
|
681
|
-
updated.push(label);
|
|
682
|
-
fs.copyFileSync(src, dest);
|
|
683
|
-
} else if (effective === 'upgrade' && cat === 'tool') {
|
|
684
|
-
updated.push(label);
|
|
685
|
-
fs.copyFileSync(src, dest);
|
|
686
|
-
} else {
|
|
687
|
-
skipped.push(label);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// Process .code-flow/ (core)
|
|
693
|
-
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
|
|
694
|
-
|
|
695
|
-
// Process Claude adapter
|
|
696
|
-
if (platform === 'claude') {
|
|
697
|
-
installAdapterFile({
|
|
698
|
-
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
699
|
-
dest: path.join(cwd, 'CLAUDE.md'),
|
|
700
|
-
label: 'CLAUDE.md',
|
|
701
|
-
mode: platformMode,
|
|
702
|
-
mergeFn: mergeClaudeMd,
|
|
703
|
-
toolOnUpgrade: false,
|
|
704
|
-
results,
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
|
|
708
|
-
processDir(
|
|
709
|
-
path.join(adaptersDir, 'claude', 'commands'),
|
|
710
|
-
path.join(cwd, '.claude', 'commands'),
|
|
711
|
-
'.claude/commands', platformMode
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
installAdapterFile({
|
|
715
|
-
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
716
|
-
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
717
|
-
label: '.claude/settings.local.json',
|
|
718
|
-
mode: platformMode,
|
|
719
|
-
mergeFn: mergeSettingsJson,
|
|
720
|
-
toolOnUpgrade: false,
|
|
721
|
-
results,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Process Costrict adapter
|
|
726
|
-
if (platform === 'costrict') {
|
|
727
|
-
installAdapterFile({
|
|
728
|
-
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
729
|
-
dest: path.join(cwd, 'CLAUDE.md'),
|
|
730
|
-
label: 'CLAUDE.md',
|
|
731
|
-
mode: platformMode,
|
|
732
|
-
mergeFn: mergeClaudeMd,
|
|
733
|
-
toolOnUpgrade: false,
|
|
734
|
-
results,
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
fs.mkdirSync(path.join(cwd, '.costrict', 'commands'), { recursive: true });
|
|
738
|
-
processDir(
|
|
739
|
-
path.join(adaptersDir, 'costrict', 'commands'),
|
|
740
|
-
path.join(cwd, '.costrict', 'commands'),
|
|
741
|
-
'.costrict/commands', platformMode
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
installAdapterFile({
|
|
745
|
-
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
746
|
-
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
747
|
-
label: '.costrict/settings.local.json',
|
|
748
|
-
mode: platformMode,
|
|
749
|
-
mergeFn: mergeSettingsJson,
|
|
750
|
-
toolOnUpgrade: false,
|
|
751
|
-
results,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Process Codex adapter
|
|
756
|
-
if (platform === 'codex') {
|
|
757
|
-
installAdapterFile({
|
|
758
|
-
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
759
|
-
dest: path.join(cwd, 'AGENTS.md'),
|
|
760
|
-
label: 'AGENTS.md',
|
|
761
|
-
mode: platformMode,
|
|
762
|
-
mergeFn: mergeClaudeMd,
|
|
763
|
-
toolOnUpgrade: false,
|
|
764
|
-
results,
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
installAdapterFile({
|
|
768
|
-
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
769
|
-
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
770
|
-
label: '.codex/hooks.json',
|
|
771
|
-
mode: platformMode,
|
|
772
|
-
mergeFn: mergeSettingsJson,
|
|
773
|
-
toolOnUpgrade: false,
|
|
774
|
-
results,
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
installAdapterFile({
|
|
778
|
-
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
779
|
-
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
780
|
-
label: '.codex/config.toml',
|
|
781
|
-
mode: platformMode,
|
|
782
|
-
mergeFn: mergeCodexConfigToml,
|
|
783
|
-
toolOnUpgrade: false,
|
|
784
|
-
results,
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
// Project-level .agents/skills/ (version-controlled, committed to repo)
|
|
788
|
-
processDir(
|
|
789
|
-
path.join(adaptersDir, 'codex', 'skills'),
|
|
790
|
-
path.join(cwd, '.agents', 'skills'),
|
|
791
|
-
'.agents/skills', platformMode
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Process OpenCode adapter
|
|
796
|
-
if (platform === 'opencode') {
|
|
797
|
-
installAdapterFile({
|
|
798
|
-
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
799
|
-
dest: path.join(cwd, 'AGENTS.md'),
|
|
800
|
-
label: 'AGENTS.md',
|
|
801
|
-
mode: platformMode,
|
|
802
|
-
mergeFn: mergeClaudeMd,
|
|
803
|
-
toolOnUpgrade: false,
|
|
804
|
-
results,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
// Plugin files under .opencode/plugins/code-flow/
|
|
808
|
-
processDir(
|
|
809
|
-
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
810
|
-
path.join(cwd, '.opencode', 'plugins'),
|
|
811
|
-
'.opencode/plugins', platformMode
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
// Stamp main package version into the plugin's package.json
|
|
815
|
-
const pluginPkgPath = path.join(cwd, '.opencode', 'plugins', 'code-flow', 'package.json');
|
|
816
|
-
if (fs.existsSync(pluginPkgPath)) {
|
|
817
|
-
const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf8'));
|
|
818
|
-
pluginPkg.version = pkg.version;
|
|
819
|
-
fs.writeFileSync(pluginPkgPath, JSON.stringify(pluginPkg, null, 2) + '\n');
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Command files under .opencode/commands/
|
|
823
|
-
processDir(
|
|
824
|
-
path.join(adaptersDir, 'opencode', 'commands'),
|
|
825
|
-
path.join(cwd, '.opencode', 'commands'),
|
|
826
|
-
'.opencode/commands', platformMode
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
installAdapterFile({
|
|
830
|
-
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
831
|
-
dest: path.join(cwd, 'opencode.json'),
|
|
832
|
-
label: 'opencode.json',
|
|
833
|
-
mode: platformMode,
|
|
834
|
-
mergeFn: mergeOpencodeJson,
|
|
835
|
-
toolOnUpgrade: false,
|
|
836
|
-
results,
|
|
837
|
-
});
|
|
857
|
+
return 'upgrade';
|
|
838
858
|
}
|
|
859
|
+
return 'current';
|
|
860
|
+
}
|
|
839
861
|
|
|
840
|
-
|
|
862
|
+
function mergeConfigOnUpgrade(cwd, coreDir, mode, options, results) {
|
|
841
863
|
const configSrc = path.join(coreDir, 'code-flow', 'config.yml');
|
|
842
864
|
const configDest = path.join(cwd, '.code-flow', 'config.yml');
|
|
843
|
-
if (mode
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
// Replace the skipped entry with merged
|
|
847
|
-
const idx = skipped.indexOf('.code-flow/config.yml');
|
|
848
|
-
if (idx !== -1) skipped.splice(idx, 1);
|
|
849
|
-
merged.push(`.code-flow/config.yml — added: ${added.join(', ')}`);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
|
|
854
|
-
removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
|
|
855
|
-
|
|
856
|
-
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
857
|
-
if (mode === 'upgrade' || mode === 'force') {
|
|
858
|
-
removeOrphanFiles(cwd, removed);
|
|
865
|
+
if (mode !== 'upgrade' || options.runtimeMigration ||
|
|
866
|
+
!fs.existsSync(configDest) || !fs.existsSync(configSrc)) {
|
|
867
|
+
return;
|
|
859
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
|
+
}
|
|
860
876
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// Write version (legacy global file kept for compatibility) + per-platform records
|
|
865
|
-
writeVersion(cwd, pkg.version);
|
|
877
|
+
function recordInstall(cwd, platform) {
|
|
878
|
+
const adapterVersions = readAdapterVersions(cwd);
|
|
866
879
|
adapterVersions.adapters[platform] = pkg.version;
|
|
867
880
|
adapterVersions.core = pkg.version;
|
|
868
881
|
writeAdapterVersions(cwd, adapterVersions);
|
|
882
|
+
runtime.writeInstall(cwd, Object.keys(adapterVersions.adapters));
|
|
883
|
+
writeVersion(cwd, pkg.version);
|
|
884
|
+
}
|
|
869
885
|
|
|
870
|
-
|
|
886
|
+
function printInitSummary(mode, platformMode, installedVersion, results) {
|
|
871
887
|
if (mode === 'upgrade') {
|
|
872
888
|
process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
|
|
873
|
-
} else if (mode === 'current' && platformMode === 'current' &&
|
|
889
|
+
} else if (mode === 'current' && platformMode === 'current' &&
|
|
890
|
+
results.created.length === 0 && results.updated.length === 0 &&
|
|
891
|
+
results.merged.length === 0) {
|
|
874
892
|
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
875
893
|
process.exit(0);
|
|
876
894
|
} else if (mode === 'force') {
|
|
@@ -878,69 +896,81 @@ function runInit(force, platform, opts) {
|
|
|
878
896
|
} else {
|
|
879
897
|
process.stdout.write(`\ncode-flow v${pkg.version} initialized!\n\n`);
|
|
880
898
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
if (merged.length > 0) {
|
|
887
|
-
process.stdout.write('Merged (new sections added):\n');
|
|
888
|
-
for (const f of merged) process.stdout.write(` ⊕ ${f}\n`);
|
|
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`);
|
|
889
904
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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);
|
|
897
925
|
}
|
|
898
|
-
if (
|
|
899
|
-
process.stdout.write('
|
|
900
|
-
|
|
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);
|
|
901
929
|
}
|
|
930
|
+
ensurePython3();
|
|
902
931
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
|
|
937
|
-
process.stdout.write(' 2. Run /cf-init in Claude Code to auto-scan and populate specs\n');
|
|
938
|
-
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
939
|
-
} else {
|
|
940
|
-
process.stdout.write(' Run /cf-learn in Claude Code to update specs with project conventions\n');
|
|
941
|
-
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
942
|
-
}
|
|
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);
|
|
943
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);
|
|
944
974
|
process.exit(0);
|
|
945
975
|
}
|
|
946
976
|
|
|
@@ -948,6 +978,10 @@ function runInit(force, platform, opts) {
|
|
|
948
978
|
|
|
949
979
|
const args = process.argv.slice(2);
|
|
950
980
|
|
|
981
|
+
if (runtimeFamilies.includes(args[0]) || runtime.projectCommandFamily(args[0])) {
|
|
982
|
+
process.exit(runtime.run(args));
|
|
983
|
+
}
|
|
984
|
+
|
|
951
985
|
function argumentValue(values, name) {
|
|
952
986
|
const index = values.indexOf(name);
|
|
953
987
|
return index >= 0 ? values[index + 1] || '' : '';
|
|
@@ -971,7 +1005,7 @@ function runMigrate(values) {
|
|
|
971
1005
|
if (actions[0] === '--apply') {
|
|
972
1006
|
const plan = argumentValue(values, '--plan');
|
|
973
1007
|
if (!plan) fail('Error: --apply requires --plan <path>.');
|
|
974
|
-
result = specWorkflowMigration.apply(path.resolve(plan));
|
|
1008
|
+
result = specWorkflowMigration.apply(path.resolve(plan), { projectRoot: process.cwd() });
|
|
975
1009
|
}
|
|
976
1010
|
if (actions[0] === '--rollback') {
|
|
977
1011
|
const id = argumentValue(values, '--rollback');
|
|
@@ -1005,10 +1039,29 @@ if (args[0] === 'init') {
|
|
|
1005
1039
|
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
1006
1040
|
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
1007
1041
|
}
|
|
1008
|
-
|
|
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
|
+
}
|
|
1009
1052
|
}
|
|
1010
1053
|
|
|
1011
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
|
+
}
|
|
1012
1065
|
runMigrate(args.slice(1));
|
|
1013
1066
|
}
|
|
1014
1067
|
|