@michaelxwb/code-flow 0.5.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 +157 -0
- package/package.json +16 -0
- package/src/adapters/claude/CLAUDE.md +51 -0
- package/src/adapters/claude/commands/cf-init.md +262 -0
- package/src/adapters/claude/commands/cf-inject.md +55 -0
- package/src/adapters/claude/commands/cf-learn.md +291 -0
- package/src/adapters/claude/commands/cf-stats.md +86 -0
- package/src/adapters/claude/commands/cf-task/align.md +283 -0
- package/src/adapters/claude/commands/cf-task/archive.md +131 -0
- package/src/adapters/claude/commands/cf-task/block.md +30 -0
- package/src/adapters/claude/commands/cf-task/graph.md +73 -0
- package/src/adapters/claude/commands/cf-task/note.md +239 -0
- package/src/adapters/claude/commands/cf-task/plan.md +270 -0
- package/src/adapters/claude/commands/cf-task/prd.md +138 -0
- package/src/adapters/claude/commands/cf-task/start.md +173 -0
- package/src/adapters/claude/commands/cf-task/status.md +59 -0
- package/src/adapters/claude/commands/cf-validate.md +73 -0
- package/src/adapters/claude/settings.local.json +60 -0
- package/src/adapters/codex/AGENTS.md +51 -0
- package/src/adapters/codex/config.toml +3 -0
- package/src/adapters/codex/hooks.json +48 -0
- package/src/adapters/codex/skills/cf-init/SKILL.md +271 -0
- package/src/adapters/codex/skills/cf-inject/SKILL.md +56 -0
- package/src/adapters/codex/skills/cf-learn/SKILL.md +296 -0
- package/src/adapters/codex/skills/cf-stats/SKILL.md +87 -0
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +284 -0
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +132 -0
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +31 -0
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +74 -0
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +236 -0
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +271 -0
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +139 -0
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +174 -0
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +60 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +74 -0
- package/src/adapters/costrict/CLAUDE.md +51 -0
- package/src/adapters/costrict/commands/cf-init.md +262 -0
- package/src/adapters/costrict/commands/cf-inject.md +55 -0
- package/src/adapters/costrict/commands/cf-learn.md +291 -0
- package/src/adapters/costrict/commands/cf-stats.md +86 -0
- package/src/adapters/costrict/commands/cf-task/align.md +283 -0
- package/src/adapters/costrict/commands/cf-task/archive.md +131 -0
- package/src/adapters/costrict/commands/cf-task/block.md +30 -0
- package/src/adapters/costrict/commands/cf-task/graph.md +73 -0
- package/src/adapters/costrict/commands/cf-task/note.md +239 -0
- package/src/adapters/costrict/commands/cf-task/plan.md +270 -0
- package/src/adapters/costrict/commands/cf-task/prd.md +138 -0
- package/src/adapters/costrict/commands/cf-task/start.md +173 -0
- package/src/adapters/costrict/commands/cf-task/status.md +59 -0
- package/src/adapters/costrict/commands/cf-validate.md +73 -0
- package/src/adapters/costrict/settings.local.json +60 -0
- package/src/adapters/opencode/AGENTS.md +51 -0
- package/src/adapters/opencode/commands/cf-init.md +275 -0
- package/src/adapters/opencode/commands/cf-inject.md +59 -0
- package/src/adapters/opencode/commands/cf-learn.md +295 -0
- package/src/adapters/opencode/commands/cf-stats.md +90 -0
- package/src/adapters/opencode/commands/cf-task/align.md +287 -0
- package/src/adapters/opencode/commands/cf-task/archive.md +135 -0
- package/src/adapters/opencode/commands/cf-task/block.md +34 -0
- package/src/adapters/opencode/commands/cf-task/graph.md +77 -0
- package/src/adapters/opencode/commands/cf-task/note.md +243 -0
- package/src/adapters/opencode/commands/cf-task/plan.md +274 -0
- package/src/adapters/opencode/commands/cf-task/prd.md +142 -0
- package/src/adapters/opencode/commands/cf-task/start.md +177 -0
- package/src/adapters/opencode/commands/cf-task/status.md +63 -0
- package/src/adapters/opencode/commands/cf-validate.md +77 -0
- package/src/adapters/opencode/opencode.json +4 -0
- package/src/adapters/opencode/plugins/code-flow/index.js +132 -0
- package/src/adapters/opencode/plugins/code-flow/package.json +6 -0
- package/src/cli.js +810 -0
- package/src/core/code-flow/config.yml +159 -0
- package/src/core/code-flow/scripts/cf_checks.py +306 -0
- package/src/core/code-flow/scripts/cf_core.py +954 -0
- package/src/core/code-flow/scripts/cf_feedback.py +56 -0
- package/src/core/code-flow/scripts/cf_inject_hook.py +178 -0
- package/src/core/code-flow/scripts/cf_log.py +146 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +196 -0
- package/src/core/code-flow/scripts/cf_scan.py +293 -0
- package/src/core/code-flow/scripts/cf_session_hook.py +42 -0
- package/src/core/code-flow/scripts/cf_stats.py +439 -0
- package/src/core/code-flow/scripts/cf_stop_hook.py +191 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +402 -0
- package/src/core/code-flow/specs/backend/_map.md +56 -0
- package/src/core/code-flow/specs/backend/code-quality-performance.md +47 -0
- package/src/core/code-flow/specs/backend/database.md +39 -0
- package/src/core/code-flow/specs/backend/directory-structure.md +42 -0
- package/src/core/code-flow/specs/backend/logging.md +38 -0
- package/src/core/code-flow/specs/backend/platform-rules.md +43 -0
- package/src/core/code-flow/specs/frontend/_map.md +52 -0
- package/src/core/code-flow/specs/frontend/component-specs.md +56 -0
- package/src/core/code-flow/specs/frontend/directory-structure.md +41 -0
- package/src/core/code-flow/specs/frontend/quality-standards.md +46 -0
- package/src/core/code-flow/specs/shared/_map.md +57 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +247 -0
- package/src/core/code-flow/specs/shared/design/design-full.md +494 -0
- package/src/core/code-flow/specs/shared/design/design-lite.md +293 -0
- package/src/core/code-flow/specs/shared/prd-template.md +187 -0
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +48 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
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
|
+
|
|
11
|
+
const usage = [
|
|
12
|
+
'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>]',
|
|
13
|
+
' code-flow -v | --version',
|
|
14
|
+
' code-flow -h | --help'
|
|
15
|
+
].join('\n');
|
|
16
|
+
|
|
17
|
+
function printUsage(stream) {
|
|
18
|
+
stream.write(`${usage}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
if (message) {
|
|
23
|
+
process.stderr.write(`${message}\n`);
|
|
24
|
+
}
|
|
25
|
+
printUsage(process.stderr);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensurePython3() {
|
|
30
|
+
const probe = spawnSync('python3', ['--version'], { stdio: 'ignore' });
|
|
31
|
+
if (probe.error || probe.status !== 0) {
|
|
32
|
+
process.stderr.write('Error: python3 is required but was not found in PATH.\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- File classification ---
|
|
38
|
+
|
|
39
|
+
function fileCategory(relPath) {
|
|
40
|
+
const p = relPath.replace(/\\/g, '/');
|
|
41
|
+
if (p.startsWith('.claude/commands/')) return 'tool';
|
|
42
|
+
if (p.startsWith('.costrict/commands/')) return 'tool';
|
|
43
|
+
if (p.startsWith('.agents/skills/')) return 'tool';
|
|
44
|
+
if (p.startsWith('.opencode/commands/')) return 'tool';
|
|
45
|
+
if (p.startsWith('.code-flow/scripts/')) return 'tool';
|
|
46
|
+
if (p === 'CLAUDE.md') return 'merge';
|
|
47
|
+
if (p === 'AGENTS.md') return 'merge';
|
|
48
|
+
if (p === '.claude/settings.local.json') return 'merge';
|
|
49
|
+
if (p === '.costrict/settings.local.json') return 'merge';
|
|
50
|
+
if (p === '.codex/hooks.json') return 'merge';
|
|
51
|
+
if (p === '.code-flow/config.yml') return 'merge';
|
|
52
|
+
if (p === '.codex/config.toml') return 'merge';
|
|
53
|
+
if (p.startsWith('.opencode/plugins/')) return 'tool';
|
|
54
|
+
if (p === 'opencode.json') return 'merge';
|
|
55
|
+
return 'user';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Version tracking ---
|
|
59
|
+
|
|
60
|
+
function readInstalledVersion(cwd) {
|
|
61
|
+
const vFile = path.join(cwd, '.code-flow', '.version');
|
|
62
|
+
if (!fs.existsSync(vFile)) return null;
|
|
63
|
+
return fs.readFileSync(vFile, 'utf8').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeVersion(cwd, version) {
|
|
67
|
+
const vFile = path.join(cwd, '.code-flow', '.version');
|
|
68
|
+
fs.mkdirSync(path.dirname(vFile), { recursive: true });
|
|
69
|
+
fs.writeFileSync(vFile, version + '\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareVersions(a, b) {
|
|
73
|
+
const pa = a.split('.').map(Number);
|
|
74
|
+
const pb = b.split('.').map(Number);
|
|
75
|
+
for (let i = 0; i < 3; i++) {
|
|
76
|
+
const va = pa[i] || 0;
|
|
77
|
+
const vb = pb[i] || 0;
|
|
78
|
+
if (va < vb) return -1;
|
|
79
|
+
if (va > vb) return 1;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Merge functions ---
|
|
85
|
+
|
|
86
|
+
// Replace fenced code blocks with same-length whitespace so byte offsets in
|
|
87
|
+
// the original text stay valid for slice operations downstream — but ## lines
|
|
88
|
+
// inside ```...``` no longer match the section regex.
|
|
89
|
+
function maskFencedCode(text) {
|
|
90
|
+
return text.replace(/```[\s\S]*?```/g, block => block.replace(/[^\n]/g, ' '));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mergeClaudeMd(srcFile, destFile) {
|
|
94
|
+
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
95
|
+
const destText = fs.readFileSync(destFile, 'utf8');
|
|
96
|
+
const srcMasked = maskFencedCode(srcText);
|
|
97
|
+
const destMasked = maskFencedCode(destText);
|
|
98
|
+
|
|
99
|
+
const sectionRegex = /^## .+$/gm;
|
|
100
|
+
const srcSections = [];
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = sectionRegex.exec(srcMasked)) !== null) {
|
|
103
|
+
srcSections.push({ heading: match[0].trim(), index: match.index });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const destSectionSet = new Set();
|
|
107
|
+
const destRegex = /^## .+$/gm;
|
|
108
|
+
while ((match = destRegex.exec(destMasked)) !== null) {
|
|
109
|
+
destSectionSet.add(match[0].trim());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const missing = srcSections.filter(s => !destSectionSet.has(s.heading));
|
|
113
|
+
if (missing.length === 0) return [];
|
|
114
|
+
|
|
115
|
+
const additions = [];
|
|
116
|
+
for (const { heading, index } of missing) {
|
|
117
|
+
// Find next real section heading using the masked text, then slice the
|
|
118
|
+
// original text by that offset so we keep any code-block content intact.
|
|
119
|
+
const nextRel = srcMasked.slice(index + heading.length).search(/^## /m);
|
|
120
|
+
const end = nextRel === -1 ? srcText.length : index + heading.length + nextRel;
|
|
121
|
+
additions.push(srcText.slice(index, end).trimEnd());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const merged = destText.trimEnd() + '\n\n' + additions.join('\n\n') + '\n';
|
|
125
|
+
fs.writeFileSync(destFile, merged);
|
|
126
|
+
return missing.map(m => m.heading);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Deep-merge a single hook event array. Each item has shape
|
|
130
|
+
// { matcher?: string, hooks: [{ type, command, ... }] }
|
|
131
|
+
// We treat (matcher || '') as the identity key. New src items become new
|
|
132
|
+
// dest items; for existing items we union the inner hooks array by command
|
|
133
|
+
// string. User-added items / commands are never removed or rewritten — the
|
|
134
|
+
// merge is purely additive, in line with cli/code-standards.md "合并策略
|
|
135
|
+
// 必须保证用户自定义内容不被覆盖".
|
|
136
|
+
// If a future release changes a managed hook command string, add an explicit
|
|
137
|
+
// migration before merging; otherwise old and new commands will both run.
|
|
138
|
+
function mergeHookEventArray(srcArr, destArr, eventName, added) {
|
|
139
|
+
if (!Array.isArray(srcArr) || !Array.isArray(destArr)) return;
|
|
140
|
+
for (const srcItem of srcArr) {
|
|
141
|
+
if (!srcItem || typeof srcItem !== 'object') continue;
|
|
142
|
+
const srcMatcher = typeof srcItem.matcher === 'string' ? srcItem.matcher : '';
|
|
143
|
+
const destItem = destArr.find(d => {
|
|
144
|
+
if (!d || typeof d !== 'object') return false;
|
|
145
|
+
const m = typeof d.matcher === 'string' ? d.matcher : '';
|
|
146
|
+
return m === srcMatcher;
|
|
147
|
+
});
|
|
148
|
+
if (!destItem) {
|
|
149
|
+
destArr.push(srcItem);
|
|
150
|
+
added.push(`hook: ${eventName}${srcMatcher ? '@' + srcMatcher : ''}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(destItem.hooks)) destItem.hooks = [];
|
|
154
|
+
const destCmds = new Set(
|
|
155
|
+
destItem.hooks.map(h => (h && typeof h.command === 'string' ? h.command : ''))
|
|
156
|
+
);
|
|
157
|
+
for (const srcHook of (Array.isArray(srcItem.hooks) ? srcItem.hooks : [])) {
|
|
158
|
+
if (!srcHook || typeof srcHook !== 'object') continue;
|
|
159
|
+
const cmd = typeof srcHook.command === 'string' ? srcHook.command : '';
|
|
160
|
+
if (destCmds.has(cmd)) continue;
|
|
161
|
+
destItem.hooks.push(srcHook);
|
|
162
|
+
destCmds.add(cmd);
|
|
163
|
+
const tag = srcMatcher ? `${eventName}@${srcMatcher}` : eventName;
|
|
164
|
+
added.push(`hook: ${tag} +${cmd.slice(0, 60)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mergeSettingsJson(srcFile, destFile) {
|
|
170
|
+
const src = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
|
|
171
|
+
const dest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
|
|
172
|
+
const added = [];
|
|
173
|
+
|
|
174
|
+
if (src.hooks && typeof src.hooks === 'object') {
|
|
175
|
+
if (!dest.hooks || typeof dest.hooks !== 'object') dest.hooks = {};
|
|
176
|
+
for (const event of Object.keys(src.hooks)) {
|
|
177
|
+
const srcEvent = src.hooks[event];
|
|
178
|
+
if (!dest.hooks[event]) {
|
|
179
|
+
dest.hooks[event] = srcEvent;
|
|
180
|
+
added.push(`hook: ${event}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
mergeHookEventArray(srcEvent, dest.hooks[event], event, added);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const key of Object.keys(src)) {
|
|
188
|
+
if (key === 'hooks') continue;
|
|
189
|
+
if (!(key in dest)) {
|
|
190
|
+
dest[key] = src[key];
|
|
191
|
+
added.push(key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (added.length > 0) {
|
|
196
|
+
fs.writeFileSync(destFile, JSON.stringify(dest, null, 2) + '\n');
|
|
197
|
+
}
|
|
198
|
+
return added;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mergeConfigYml(srcFile, destFile) {
|
|
202
|
+
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
203
|
+
const destText = fs.readFileSync(destFile, 'utf8');
|
|
204
|
+
|
|
205
|
+
// Extract top-level keys (lines starting with a non-space char followed by colon)
|
|
206
|
+
const topKeyRegex = /^([a-zA-Z_][a-zA-Z0-9_]*):/gm;
|
|
207
|
+
const srcKeys = new Map();
|
|
208
|
+
let m;
|
|
209
|
+
while ((m = topKeyRegex.exec(srcText)) !== null) {
|
|
210
|
+
srcKeys.set(m[1], m.index);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const destKeys = new Set();
|
|
214
|
+
const destRegex = /^([a-zA-Z_][a-zA-Z0-9_]*):/gm;
|
|
215
|
+
while ((m = destRegex.exec(destText)) !== null) {
|
|
216
|
+
destKeys.add(m[1]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const missing = [];
|
|
220
|
+
const blocks = [];
|
|
221
|
+
for (const [key, startIdx] of srcKeys) {
|
|
222
|
+
if (destKeys.has(key)) continue;
|
|
223
|
+
missing.push(key);
|
|
224
|
+
// Extract the entire block for this key (until next top-level key or EOF)
|
|
225
|
+
const remaining = srcText.slice(startIdx);
|
|
226
|
+
const nextKey = remaining.indexOf('\n') !== -1
|
|
227
|
+
? remaining.slice(remaining.indexOf('\n') + 1).search(/^[a-zA-Z_]/m)
|
|
228
|
+
: -1;
|
|
229
|
+
const block = nextKey === -1
|
|
230
|
+
? remaining
|
|
231
|
+
: remaining.slice(0, remaining.indexOf('\n') + 1 + nextKey);
|
|
232
|
+
blocks.push(block.trimEnd());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (blocks.length > 0) {
|
|
236
|
+
const merged = destText.trimEnd() + '\n\n' + blocks.join('\n\n') + '\n';
|
|
237
|
+
fs.writeFileSync(destFile, merged);
|
|
238
|
+
}
|
|
239
|
+
return missing;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findTomlSectionBounds(lines, sectionName) {
|
|
243
|
+
let start = -1;
|
|
244
|
+
let end = lines.length;
|
|
245
|
+
const header = new RegExp(`^\\s*\\[${sectionName}\\]\\s*$`);
|
|
246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
247
|
+
if (!/^\s*\[.+\]\s*$/.test(lines[i])) continue;
|
|
248
|
+
if (start !== -1) {
|
|
249
|
+
end = i;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (header.test(lines[i])) start = i;
|
|
253
|
+
}
|
|
254
|
+
return { start, end };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function findTomlKeyLine(lines, start, end, key) {
|
|
258
|
+
const keyRegex = new RegExp(`^\\s*${key}\\s*=`);
|
|
259
|
+
for (let i = start + 1; i < end; i++) {
|
|
260
|
+
if (keyRegex.test(lines[i])) return i;
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function findFeatureInsertIndex(lines, start, end) {
|
|
266
|
+
for (let i = start + 1; i < end; i++) {
|
|
267
|
+
if (/^\s*$/.test(lines[i])) return i;
|
|
268
|
+
}
|
|
269
|
+
return end;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function mergeCodexConfigToml(srcFile, destFile) {
|
|
273
|
+
const srcText = fs.readFileSync(srcFile, 'utf8');
|
|
274
|
+
const destText = fs.readFileSync(destFile, 'utf8');
|
|
275
|
+
const hookLineMatch = srcText.match(/^\s*hooks\s*=.*$/m);
|
|
276
|
+
if (!hookLineMatch) return [];
|
|
277
|
+
|
|
278
|
+
const lines = destText.split(/\n/);
|
|
279
|
+
const { start, end } = findTomlSectionBounds(lines, 'features');
|
|
280
|
+
|
|
281
|
+
if (start === -1) {
|
|
282
|
+
fs.writeFileSync(destFile, destText.trimEnd() + '\n\n[features]\n' + hookLineMatch[0].trim() + '\n');
|
|
283
|
+
return ['features.hooks'];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (findTomlKeyLine(lines, start, end, 'hooks') !== -1) return [];
|
|
287
|
+
|
|
288
|
+
const legacyIdx = findTomlKeyLine(lines, start, end, 'codex_hooks');
|
|
289
|
+
if (legacyIdx !== -1) {
|
|
290
|
+
lines[legacyIdx] = lines[legacyIdx].replace(/^(\s*)codex_hooks(\s*=.*)$/, '$1hooks$2');
|
|
291
|
+
fs.writeFileSync(destFile, lines.join('\n').replace(/\n*$/, '\n'));
|
|
292
|
+
return ['features.hooks'];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const insertAt = findFeatureInsertIndex(lines, start, end);
|
|
296
|
+
lines.splice(insertAt, 0, hookLineMatch[0].trim());
|
|
297
|
+
fs.writeFileSync(destFile, lines.join('\n').replace(/\n*$/, '\n'));
|
|
298
|
+
return ['features.hooks'];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- File operations ---
|
|
302
|
+
|
|
303
|
+
function collectFiles(dir, base) {
|
|
304
|
+
const results = [];
|
|
305
|
+
if (!fs.existsSync(dir)) return results;
|
|
306
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
const rel = path.join(base, entry.name);
|
|
309
|
+
if (entry.isDirectory()) {
|
|
310
|
+
results.push(...collectFiles(path.join(dir, entry.name), rel));
|
|
311
|
+
} else if (entry.isFile()) {
|
|
312
|
+
results.push(rel);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function copyDirRecursive(srcDir, destDir, overwrite) {
|
|
319
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
320
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
323
|
+
const destPath = path.join(destDir, entry.name);
|
|
324
|
+
if (entry.isDirectory()) {
|
|
325
|
+
copyDirRecursive(srcPath, destPath, overwrite);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (entry.isFile()) {
|
|
329
|
+
if (overwrite || !fs.existsSync(destPath)) {
|
|
330
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
331
|
+
fs.copyFileSync(srcPath, destPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function copyFileIfMissing(srcFile, destFile) {
|
|
338
|
+
if (fs.existsSync(destFile)) return;
|
|
339
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
340
|
+
fs.copyFileSync(srcFile, destFile);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Files renamed in 0.x → 1.x that may linger in upgraded projects.
|
|
344
|
+
// Each entry: { path: relative-to-cwd, replacedBy: hint string for the warning }.
|
|
345
|
+
const ORPHAN_FILES = [
|
|
346
|
+
{
|
|
347
|
+
path: '.code-flow/scripts/cf_codex_user_prompt_hook.py',
|
|
348
|
+
replacedBy: '.code-flow/scripts/cf_user_prompt_hook.py',
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
function removeOrphanFiles(cwd, removed) {
|
|
353
|
+
for (const orphan of ORPHAN_FILES) {
|
|
354
|
+
const abs = path.join(cwd, orphan.path);
|
|
355
|
+
if (!fs.existsSync(abs)) continue;
|
|
356
|
+
try {
|
|
357
|
+
fs.unlinkSync(abs);
|
|
358
|
+
removed.push(orphan.path);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
361
|
+
process.stderr.write(
|
|
362
|
+
`Warning: failed to remove orphan ${orphan.path}${code}; replaced by ${orphan.replacedBy}. Remove it manually.\n`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function removeLegacyClaudeSkills(cwd, removed) {
|
|
369
|
+
const legacySkills = path.join(cwd, '.claude', 'skills');
|
|
370
|
+
if (!fs.existsSync(legacySkills)) return;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
fs.rmSync(legacySkills, {
|
|
374
|
+
recursive: true,
|
|
375
|
+
force: true,
|
|
376
|
+
maxRetries: 5,
|
|
377
|
+
retryDelay: 100
|
|
378
|
+
});
|
|
379
|
+
if (!fs.existsSync(legacySkills)) {
|
|
380
|
+
removed.push('.claude/skills/');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const code = error && error.code ? ` (${error.code})` : '';
|
|
385
|
+
const message = error && error.message ? error.message : String(error);
|
|
386
|
+
process.stderr.write(`Warning: failed to remove deprecated .claude/skills/${code}: ${message}\n`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
process.stderr.write('Warning: failed to remove deprecated .claude/skills/. Remove it manually.\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
|
|
394
|
+
|
|
395
|
+
function ensurePyYaml() {
|
|
396
|
+
const probe = spawnSync('python3', ['-c', 'import yaml'], { stdio: 'ignore' });
|
|
397
|
+
if (!probe.error && probe.status === 0) return; // already installed
|
|
398
|
+
|
|
399
|
+
const attempts = [
|
|
400
|
+
['-m', 'pip', 'install', '--user', 'pyyaml'],
|
|
401
|
+
['-m', 'pip', 'install', 'pyyaml'],
|
|
402
|
+
['-m', 'pip', 'install', '--user', '--break-system-packages', 'pyyaml'],
|
|
403
|
+
];
|
|
404
|
+
for (const args of attempts) {
|
|
405
|
+
const result = spawnSync('python3', args, { stdio: 'ignore' });
|
|
406
|
+
if (!result.error && result.status === 0) return;
|
|
407
|
+
}
|
|
408
|
+
process.stderr.write(
|
|
409
|
+
'Warning: pyyaml install failed. Run manually:\n' +
|
|
410
|
+
' python3 -m pip install --user pyyaml\n' +
|
|
411
|
+
' # or, on PEP 668 systems:\n' +
|
|
412
|
+
' python3 -m pip install --user --break-system-packages pyyaml\n'
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Adapter file installer: shared by every platform branch ---
|
|
417
|
+
|
|
418
|
+
// Install one adapter file with the standard create / force / upgrade /
|
|
419
|
+
// skip semantics. `mergeFn` is consulted only on upgrade for `merge`-class
|
|
420
|
+
// files; pass null for `tool`-class files (overwrite on upgrade) or for
|
|
421
|
+
// fresh-only files (skip on upgrade).
|
|
422
|
+
function installAdapterFile(opts) {
|
|
423
|
+
const { src, dest, label, mode, mergeFn, toolOnUpgrade, results } = opts;
|
|
424
|
+
if (!fs.existsSync(dest)) {
|
|
425
|
+
results.created.push(label);
|
|
426
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
427
|
+
fs.copyFileSync(src, dest);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (mode === 'force') {
|
|
431
|
+
results.updated.push(label);
|
|
432
|
+
fs.copyFileSync(src, dest);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (mode === 'upgrade') {
|
|
436
|
+
if (toolOnUpgrade) {
|
|
437
|
+
results.updated.push(label);
|
|
438
|
+
fs.copyFileSync(src, dest);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (mergeFn) {
|
|
442
|
+
const added = mergeFn(src, dest);
|
|
443
|
+
if (added.length > 0) {
|
|
444
|
+
results.merged.push(`${label} — added: ${added.join(', ')}`);
|
|
445
|
+
} else {
|
|
446
|
+
results.skipped.push(label);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
results.skipped.push(label);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --- Platform argument parsing ---
|
|
455
|
+
|
|
456
|
+
function parsePlatform(args) {
|
|
457
|
+
for (const arg of args) {
|
|
458
|
+
if (arg.startsWith('--platform=')) {
|
|
459
|
+
return arg.slice('--platform='.length);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const idx = args.indexOf('--platform');
|
|
463
|
+
if (idx !== -1) {
|
|
464
|
+
return args[idx + 1] || '';
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// --- Main init ---
|
|
470
|
+
|
|
471
|
+
function runInit(force, platform) {
|
|
472
|
+
ensurePython3();
|
|
473
|
+
|
|
474
|
+
const cwd = process.cwd();
|
|
475
|
+
const baseDir = __dirname;
|
|
476
|
+
const coreDir = path.join(baseDir, 'core');
|
|
477
|
+
const adaptersDir = path.join(baseDir, 'adapters');
|
|
478
|
+
|
|
479
|
+
// Determine mode
|
|
480
|
+
const installedVersion = readInstalledVersion(cwd);
|
|
481
|
+
let mode;
|
|
482
|
+
if (force) {
|
|
483
|
+
mode = 'force';
|
|
484
|
+
} else if (!installedVersion) {
|
|
485
|
+
mode = 'fresh';
|
|
486
|
+
} else if (compareVersions(installedVersion, pkg.version) < 0) {
|
|
487
|
+
mode = 'upgrade';
|
|
488
|
+
} else {
|
|
489
|
+
mode = 'current';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const created = [];
|
|
493
|
+
const updated = [];
|
|
494
|
+
const merged = [];
|
|
495
|
+
const skipped = [];
|
|
496
|
+
const removed = [];
|
|
497
|
+
const results = { created, updated, merged, skipped };
|
|
498
|
+
|
|
499
|
+
// Track + copy helper for directory trees
|
|
500
|
+
const processDir = (srcDir, destDir, prefix) => {
|
|
501
|
+
if (!fs.existsSync(srcDir)) return;
|
|
502
|
+
const files = collectFiles(srcDir, '');
|
|
503
|
+
for (const rel of files) {
|
|
504
|
+
const src = path.join(srcDir, rel);
|
|
505
|
+
const dest = path.join(destDir, rel);
|
|
506
|
+
const label = path.join(prefix, rel);
|
|
507
|
+
const cat = fileCategory(label);
|
|
508
|
+
|
|
509
|
+
if (!fs.existsSync(dest)) {
|
|
510
|
+
created.push(label);
|
|
511
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
512
|
+
fs.copyFileSync(src, dest);
|
|
513
|
+
} else if (mode === 'force') {
|
|
514
|
+
updated.push(label);
|
|
515
|
+
fs.copyFileSync(src, dest);
|
|
516
|
+
} else if (mode === 'upgrade' && cat === 'tool') {
|
|
517
|
+
updated.push(label);
|
|
518
|
+
fs.copyFileSync(src, dest);
|
|
519
|
+
} else {
|
|
520
|
+
skipped.push(label);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Process .code-flow/ (core)
|
|
526
|
+
processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow');
|
|
527
|
+
|
|
528
|
+
// Process Claude adapter
|
|
529
|
+
if (platform === 'claude') {
|
|
530
|
+
installAdapterFile({
|
|
531
|
+
src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
|
|
532
|
+
dest: path.join(cwd, 'CLAUDE.md'),
|
|
533
|
+
label: 'CLAUDE.md',
|
|
534
|
+
mode,
|
|
535
|
+
mergeFn: mergeClaudeMd,
|
|
536
|
+
toolOnUpgrade: false,
|
|
537
|
+
results,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
|
|
541
|
+
processDir(
|
|
542
|
+
path.join(adaptersDir, 'claude', 'commands'),
|
|
543
|
+
path.join(cwd, '.claude', 'commands'),
|
|
544
|
+
'.claude/commands'
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
installAdapterFile({
|
|
548
|
+
src: path.join(adaptersDir, 'claude', 'settings.local.json'),
|
|
549
|
+
dest: path.join(cwd, '.claude', 'settings.local.json'),
|
|
550
|
+
label: '.claude/settings.local.json',
|
|
551
|
+
mode,
|
|
552
|
+
mergeFn: mergeSettingsJson,
|
|
553
|
+
toolOnUpgrade: false,
|
|
554
|
+
results,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Process Costrict adapter
|
|
559
|
+
if (platform === 'costrict') {
|
|
560
|
+
installAdapterFile({
|
|
561
|
+
src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
|
|
562
|
+
dest: path.join(cwd, 'CLAUDE.md'),
|
|
563
|
+
label: 'CLAUDE.md',
|
|
564
|
+
mode,
|
|
565
|
+
mergeFn: mergeClaudeMd,
|
|
566
|
+
toolOnUpgrade: false,
|
|
567
|
+
results,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
fs.mkdirSync(path.join(cwd, '.costrict', 'commands'), { recursive: true });
|
|
571
|
+
processDir(
|
|
572
|
+
path.join(adaptersDir, 'costrict', 'commands'),
|
|
573
|
+
path.join(cwd, '.costrict', 'commands'),
|
|
574
|
+
'.costrict/commands'
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
installAdapterFile({
|
|
578
|
+
src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
|
|
579
|
+
dest: path.join(cwd, '.costrict', 'settings.local.json'),
|
|
580
|
+
label: '.costrict/settings.local.json',
|
|
581
|
+
mode,
|
|
582
|
+
mergeFn: mergeSettingsJson,
|
|
583
|
+
toolOnUpgrade: false,
|
|
584
|
+
results,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Process Codex adapter
|
|
589
|
+
if (platform === 'codex') {
|
|
590
|
+
installAdapterFile({
|
|
591
|
+
src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
|
|
592
|
+
dest: path.join(cwd, 'AGENTS.md'),
|
|
593
|
+
label: 'AGENTS.md',
|
|
594
|
+
mode,
|
|
595
|
+
mergeFn: mergeClaudeMd,
|
|
596
|
+
toolOnUpgrade: false,
|
|
597
|
+
results,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
installAdapterFile({
|
|
601
|
+
src: path.join(adaptersDir, 'codex', 'hooks.json'),
|
|
602
|
+
dest: path.join(cwd, '.codex', 'hooks.json'),
|
|
603
|
+
label: '.codex/hooks.json',
|
|
604
|
+
mode,
|
|
605
|
+
mergeFn: mergeSettingsJson,
|
|
606
|
+
toolOnUpgrade: false,
|
|
607
|
+
results,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
installAdapterFile({
|
|
611
|
+
src: path.join(adaptersDir, 'codex', 'config.toml'),
|
|
612
|
+
dest: path.join(cwd, '.codex', 'config.toml'),
|
|
613
|
+
label: '.codex/config.toml',
|
|
614
|
+
mode,
|
|
615
|
+
mergeFn: mergeCodexConfigToml,
|
|
616
|
+
toolOnUpgrade: false,
|
|
617
|
+
results,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Project-level .agents/skills/ (version-controlled, committed to repo)
|
|
621
|
+
processDir(
|
|
622
|
+
path.join(adaptersDir, 'codex', 'skills'),
|
|
623
|
+
path.join(cwd, '.agents', 'skills'),
|
|
624
|
+
'.agents/skills'
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Process OpenCode adapter
|
|
629
|
+
if (platform === 'opencode') {
|
|
630
|
+
installAdapterFile({
|
|
631
|
+
src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
|
|
632
|
+
dest: path.join(cwd, 'AGENTS.md'),
|
|
633
|
+
label: 'AGENTS.md',
|
|
634
|
+
mode,
|
|
635
|
+
mergeFn: mergeClaudeMd,
|
|
636
|
+
toolOnUpgrade: false,
|
|
637
|
+
results,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Plugin files under .opencode/plugins/code-flow/
|
|
641
|
+
processDir(
|
|
642
|
+
path.join(adaptersDir, 'opencode', 'plugins'),
|
|
643
|
+
path.join(cwd, '.opencode', 'plugins'),
|
|
644
|
+
'.opencode/plugins'
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Stamp main package version into the plugin's package.json
|
|
648
|
+
const pluginPkgPath = path.join(cwd, '.opencode', 'plugins', 'code-flow', 'package.json');
|
|
649
|
+
if (fs.existsSync(pluginPkgPath)) {
|
|
650
|
+
const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf8'));
|
|
651
|
+
pluginPkg.version = pkg.version;
|
|
652
|
+
fs.writeFileSync(pluginPkgPath, JSON.stringify(pluginPkg, null, 2) + '\n');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Command files under .opencode/commands/
|
|
656
|
+
processDir(
|
|
657
|
+
path.join(adaptersDir, 'opencode', 'commands'),
|
|
658
|
+
path.join(cwd, '.opencode', 'commands'),
|
|
659
|
+
'.opencode/commands'
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
installAdapterFile({
|
|
663
|
+
src: path.join(adaptersDir, 'opencode', 'opencode.json'),
|
|
664
|
+
dest: path.join(cwd, 'opencode.json'),
|
|
665
|
+
label: 'opencode.json',
|
|
666
|
+
mode,
|
|
667
|
+
mergeFn: mergeSettingsJson,
|
|
668
|
+
toolOnUpgrade: false,
|
|
669
|
+
results,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Merge config.yml on upgrade
|
|
674
|
+
const configSrc = path.join(coreDir, 'code-flow', 'config.yml');
|
|
675
|
+
const configDest = path.join(cwd, '.code-flow', 'config.yml');
|
|
676
|
+
if (mode === 'upgrade' && fs.existsSync(configDest) && fs.existsSync(configSrc)) {
|
|
677
|
+
const added = mergeConfigYml(configSrc, configDest);
|
|
678
|
+
if (added.length > 0) {
|
|
679
|
+
// Replace the skipped entry with merged
|
|
680
|
+
const idx = skipped.indexOf('.code-flow/config.yml');
|
|
681
|
+
if (idx !== -1) skipped.splice(idx, 1);
|
|
682
|
+
merged.push(`.code-flow/config.yml — added: ${added.join(', ')}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Clean up legacy .claude/skills/
|
|
687
|
+
removeLegacyClaudeSkills(cwd, removed);
|
|
688
|
+
|
|
689
|
+
// Clean up renamed orphan scripts (only meaningful on upgrade)
|
|
690
|
+
if (mode === 'upgrade' || mode === 'force') {
|
|
691
|
+
removeOrphanFiles(cwd, removed);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Install pyyaml — probe first, fall back through PEP 668 strategies
|
|
695
|
+
ensurePyYaml();
|
|
696
|
+
|
|
697
|
+
// Write version
|
|
698
|
+
writeVersion(cwd, pkg.version);
|
|
699
|
+
|
|
700
|
+
// Output summary
|
|
701
|
+
if (mode === 'upgrade') {
|
|
702
|
+
process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
|
|
703
|
+
} else if (mode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
|
|
704
|
+
process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
|
|
705
|
+
process.exit(0);
|
|
706
|
+
} else if (mode === 'force') {
|
|
707
|
+
process.stdout.write(`\ncode-flow v${pkg.version} force-initialized!\n\n`);
|
|
708
|
+
} else {
|
|
709
|
+
process.stdout.write(`\ncode-flow v${pkg.version} initialized!\n\n`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (updated.length > 0) {
|
|
713
|
+
process.stdout.write('Updated (tool-managed):\n');
|
|
714
|
+
for (const f of updated) process.stdout.write(` ↑ ${f}\n`);
|
|
715
|
+
}
|
|
716
|
+
if (merged.length > 0) {
|
|
717
|
+
process.stdout.write('Merged (new sections added):\n');
|
|
718
|
+
for (const f of merged) process.stdout.write(` ⊕ ${f}\n`);
|
|
719
|
+
}
|
|
720
|
+
if (created.length > 0) {
|
|
721
|
+
process.stdout.write('Created:\n');
|
|
722
|
+
for (const f of created) process.stdout.write(` + ${f}\n`);
|
|
723
|
+
}
|
|
724
|
+
if (skipped.length > 0) {
|
|
725
|
+
process.stdout.write('Skipped (user-customized):\n');
|
|
726
|
+
for (const f of skipped) process.stdout.write(` · ${f}\n`);
|
|
727
|
+
}
|
|
728
|
+
if (removed.length > 0) {
|
|
729
|
+
process.stdout.write('Removed (deprecated):\n');
|
|
730
|
+
for (const f of removed) process.stdout.write(` ✕ ${f}\n`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
process.stdout.write('\nNext steps:\n');
|
|
734
|
+
if (platform === 'codex') {
|
|
735
|
+
if (mode === 'fresh') {
|
|
736
|
+
process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
|
|
737
|
+
process.stdout.write(' 2. Run /hooks in Codex CLI and trust the code-flow hooks if prompted\n');
|
|
738
|
+
process.stdout.write(' 3. Run $cf-init in Codex CLI to auto-scan and populate specs\n');
|
|
739
|
+
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
740
|
+
} else {
|
|
741
|
+
process.stdout.write(' Run /hooks in Codex CLI and trust changed code-flow hooks if prompted\n');
|
|
742
|
+
process.stdout.write(' Run $cf-learn in Codex CLI to update specs with project conventions\n');
|
|
743
|
+
process.stdout.write(' Run $cf-learn --map to update retrieval maps\n');
|
|
744
|
+
}
|
|
745
|
+
} else if (platform === 'costrict') {
|
|
746
|
+
if (mode === 'fresh') {
|
|
747
|
+
process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
|
|
748
|
+
process.stdout.write(' 2. Run /cf-init in Costrict to auto-scan and populate specs\n');
|
|
749
|
+
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
750
|
+
} else {
|
|
751
|
+
process.stdout.write(' Run /cf-learn in Costrict to update specs with project conventions\n');
|
|
752
|
+
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
753
|
+
}
|
|
754
|
+
} else if (platform === 'opencode') {
|
|
755
|
+
if (mode === 'fresh') {
|
|
756
|
+
process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
|
|
757
|
+
process.stdout.write(' 2. Start opencode in this directory — the plugin auto-loads\n');
|
|
758
|
+
process.stdout.write(' 3. Run /cf-init in OpenCode to auto-scan and populate specs\n');
|
|
759
|
+
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
760
|
+
} else {
|
|
761
|
+
process.stdout.write(' Run /cf-learn in OpenCode to update specs with project conventions\n');
|
|
762
|
+
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
if (mode === 'fresh') {
|
|
766
|
+
process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
|
|
767
|
+
process.stdout.write(' 2. Run /cf-init in Claude Code to auto-scan and populate specs\n');
|
|
768
|
+
process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
|
|
769
|
+
} else {
|
|
770
|
+
process.stdout.write(' Run /cf-learn in Claude Code to update specs with project conventions\n');
|
|
771
|
+
process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// --- CLI argument parsing ---
|
|
778
|
+
|
|
779
|
+
const args = process.argv.slice(2);
|
|
780
|
+
|
|
781
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
782
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
783
|
+
process.exit(0);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
787
|
+
process.stdout.write(`code-flow v${pkg.version}\n\n`);
|
|
788
|
+
printUsage(process.stdout);
|
|
789
|
+
process.exit(0);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (args[0] === 'init') {
|
|
793
|
+
const force = args.includes('--force');
|
|
794
|
+
const rawPlatform = parsePlatform(args);
|
|
795
|
+
const platform = rawPlatform === null ? 'claude' : rawPlatform;
|
|
796
|
+
if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
|
|
797
|
+
fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
|
|
798
|
+
}
|
|
799
|
+
runInit(force, platform);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (args.length === 0) {
|
|
803
|
+
fail('Error: missing command.');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (args.includes('--path')) {
|
|
807
|
+
fail('Error: --path is not supported; run init in the current directory.');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
fail(`Error: unknown command "${args.join(' ')}".`);
|