flow-cc 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/install.js CHANGED
@@ -1,352 +1,352 @@
1
- #!/usr/bin/env node
2
- // Flow plugin installer for Claude Code
3
- // Usage: npx flow-cc [--uninstall] [--verify]
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const os = require('os');
8
-
9
- // ---------- Node.js version check ----------
10
- const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
11
- if (nodeMajor < 18) {
12
- console.error('Flow requires Node.js 18 or later. You have ' + process.version);
13
- process.exit(1);
14
- }
15
-
16
- const homeDir = os.homedir();
17
- const claudeDir = path.join(homeDir, '.claude');
18
- const commandsDir = path.join(claudeDir, 'commands', 'flow');
19
- const hooksDir = path.join(claudeDir, 'hooks');
20
- const cacheDir = path.join(claudeDir, 'cache');
21
- const settingsPath = path.join(claudeDir, 'settings.json');
22
-
23
- // Source directories (relative to package root, one level up from bin/)
24
- const pkgRoot = path.resolve(__dirname, '..');
25
- const skillsDir = path.join(pkgRoot, 'skills');
26
- const srcHooksDir = path.join(pkgRoot, 'hooks');
27
- const templatesDir = path.join(pkgRoot, 'templates');
28
- const versionFile = path.join(pkgRoot, 'VERSION');
29
-
30
- const uninstall = process.argv.includes('--uninstall') || process.argv.includes('-u');
31
- const verify = process.argv.includes('--verify') || process.argv.includes('-v');
32
- const changelog = process.argv.includes('--changelog');
33
-
34
- // ---------- Changelog ----------
35
- if (changelog) {
36
- const changelogPath = path.join(pkgRoot, 'CHANGELOG.md');
37
- try {
38
- console.log(fs.readFileSync(changelogPath, 'utf8'));
39
- } catch (e) {
40
- console.log('CHANGELOG not available');
41
- }
42
- process.exit(0);
43
- }
44
-
45
- // ---------- Verify ----------
46
- if (verify) {
47
- console.log('Flow install health check:\n');
48
-
49
- let passed = 0;
50
- const total = 5;
51
-
52
- // 1. Skills installed
53
- try {
54
- const files = fs.existsSync(commandsDir)
55
- ? fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'))
56
- : [];
57
- if (files.length > 0) {
58
- console.log(` \u2713 Skills installed (${files.length} files)`);
59
- passed++;
60
- } else {
61
- console.log(' \u2717 Skills not installed (0 .md files in commands/flow/)');
62
- }
63
- } catch (e) {
64
- console.log(' \u2717 Skills not installed (cannot read commands/flow/)');
65
- }
66
-
67
- // 2. Hooks installed
68
- try {
69
- const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
70
- const found = hookFiles.filter(h => fs.existsSync(path.join(hooksDir, h)));
71
- if (found.length === 2) {
72
- console.log(' \u2713 Hooks installed (2 files)');
73
- passed++;
74
- } else {
75
- console.log(` \u2717 Hooks incomplete (${found.length}/2 files)`);
76
- }
77
- } catch (e) {
78
- console.log(' \u2717 Hooks not installed');
79
- }
80
-
81
- // 3. VERSION file
82
- try {
83
- const vPath = path.join(commandsDir, 'VERSION');
84
- if (fs.existsSync(vPath)) {
85
- const ver = fs.readFileSync(vPath, 'utf8').trim();
86
- console.log(` \u2713 VERSION file present (${ver})`);
87
- passed++;
88
- } else {
89
- console.log(' \u2717 VERSION file missing');
90
- }
91
- } catch (e) {
92
- console.log(' \u2717 VERSION file missing');
93
- }
94
-
95
- // 4. Templates installed
96
- try {
97
- const tDir = path.join(commandsDir, 'templates');
98
- const files = fs.existsSync(tDir) ? fs.readdirSync(tDir) : [];
99
- if (files.length > 0) {
100
- console.log(` \u2713 Templates installed (${files.length} files)`);
101
- passed++;
102
- } else {
103
- console.log(' \u2717 Templates not installed (0 files)');
104
- }
105
- } catch (e) {
106
- console.log(' \u2717 Templates not installed');
107
- }
108
-
109
- // 5. StatusLine configured
110
- try {
111
- if (fs.existsSync(settingsPath)) {
112
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
113
- if (settings.statusLine && typeof settings.statusLine.command === 'string' &&
114
- settings.statusLine.command.includes('flow-statusline')) {
115
- console.log(' \u2713 StatusLine configured');
116
- passed++;
117
- } else {
118
- console.log(' \u2717 StatusLine not configured');
119
- }
120
- } else {
121
- console.log(' \u2717 StatusLine not configured (no settings.json)');
122
- }
123
- } catch (e) {
124
- console.log(' \u2717 StatusLine not configured');
125
- }
126
-
127
- console.log(`\nResult: ${passed}/${total} checks passed`);
128
- process.exit(passed === total ? 0 : 1);
129
- }
130
-
131
- // ---------- Uninstall ----------
132
- if (uninstall) {
133
- console.log('Uninstalling Flow plugin...\n');
134
-
135
- // Remove flow commands directory
136
- if (fs.existsSync(commandsDir)) {
137
- fs.rmSync(commandsDir, { recursive: true });
138
- console.log(' Removed ~/.claude/commands/flow/');
139
- }
140
-
141
- // Remove flow hooks
142
- const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
143
- for (const hook of hookFiles) {
144
- const hookPath = path.join(hooksDir, hook);
145
- if (fs.existsSync(hookPath)) {
146
- fs.unlinkSync(hookPath);
147
- console.log(` Removed ~/.claude/hooks/${hook}`);
148
- }
149
- }
150
-
151
- // Remove update cache
152
- const cacheFile = path.join(cacheDir, 'flow-update-check.json');
153
- if (fs.existsSync(cacheFile)) {
154
- fs.unlinkSync(cacheFile);
155
- console.log(' Removed ~/.claude/cache/flow-update-check.json');
156
- }
157
-
158
- // Remove statusLine from settings.json if it points to flow
159
- if (fs.existsSync(settingsPath)) {
160
- try {
161
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
162
- if (settings.statusLine && typeof settings.statusLine.command === 'string' &&
163
- settings.statusLine.command.includes('flow-statusline')) {
164
- delete settings.statusLine;
165
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
166
- console.log(' Removed statusLine from ~/.claude/settings.json');
167
- }
168
- } catch (e) {
169
- // settings.json parse error -- leave it alone
170
- }
171
- }
172
-
173
- console.log('\nFlow plugin uninstalled.');
174
- process.exit(0);
175
- }
176
-
177
- // ---------- Install ----------
178
- console.log('Installing Flow plugin...\n');
179
-
180
- // ---------- Write permission check ----------
181
- const permTestDir = fs.existsSync(claudeDir) ? claudeDir : path.dirname(claudeDir);
182
- const permTestFile = path.join(permTestDir, '.flow-perm-test-' + process.pid);
183
- try {
184
- fs.writeFileSync(permTestFile, 'test');
185
- fs.unlinkSync(permTestFile);
186
- } catch (e) {
187
- console.error('Cannot write to ' + claudeDir + ' \u2014 check permissions.');
188
- process.exit(1);
189
- }
190
-
191
- // ---------- Rollback tracking ----------
192
- const created = []; // paths created during install (files and dirs we created fresh)
193
-
194
- function trackFile(filePath) {
195
- created.push({ type: 'file', path: filePath });
196
- }
197
-
198
- function trackDir(dirPath) {
199
- // Only track if we actually created it (didn't exist before)
200
- if (!fs.existsSync(dirPath)) {
201
- created.push({ type: 'dir', path: dirPath });
202
- }
203
- }
204
-
205
- function rollback() {
206
- console.error('\nRolling back partial install...');
207
- for (let i = created.length - 1; i >= 0; i--) {
208
- const item = created[i];
209
- try {
210
- if (item.type === 'file' && fs.existsSync(item.path)) {
211
- fs.unlinkSync(item.path);
212
- } else if (item.type === 'dir' && fs.existsSync(item.path)) {
213
- fs.rmSync(item.path, { recursive: true });
214
- }
215
- } catch (e) {
216
- // Best-effort cleanup
217
- }
218
- }
219
- console.error('Rollback complete.');
220
- }
221
-
222
- try {
223
- // 1. Create directories
224
- for (const dir of [commandsDir, hooksDir, cacheDir]) {
225
- trackDir(dir);
226
- fs.mkdirSync(dir, { recursive: true });
227
- }
228
-
229
- // 2. Copy skills: skills/flow-*.md -> commands/flow/*.md (strip "flow-" prefix)
230
- const skillFiles = fs.readdirSync(skillsDir).filter(f => f.startsWith('flow-') && f.endsWith('.md'));
231
- for (const file of skillFiles) {
232
- const dest = file.replace(/^flow-/, '');
233
- const destPath = path.join(commandsDir, dest);
234
- fs.copyFileSync(path.join(skillsDir, file), destPath);
235
- trackFile(destPath);
236
- }
237
- console.log(` Installed ${skillFiles.length} skills \u2192 ~/.claude/commands/flow/`);
238
-
239
- // 3. Copy hooks
240
- const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
241
- for (const hook of hookFiles) {
242
- const src = path.join(srcHooksDir, hook);
243
- if (fs.existsSync(src)) {
244
- const destPath = path.join(hooksDir, hook);
245
- fs.copyFileSync(src, destPath);
246
- trackFile(destPath);
247
- }
248
- }
249
- console.log(' Installed hooks \u2192 ~/.claude/hooks/');
250
-
251
- // 4. Copy VERSION
252
- const versionDest = path.join(commandsDir, 'VERSION');
253
- fs.copyFileSync(versionFile, versionDest);
254
- trackFile(versionDest);
255
- console.log(' Installed VERSION \u2192 ~/.claude/commands/flow/VERSION');
256
-
257
- // 5. Copy templates
258
- const destTemplatesDir = path.join(commandsDir, 'templates');
259
- trackDir(destTemplatesDir);
260
- fs.mkdirSync(destTemplatesDir, { recursive: true });
261
- if (fs.existsSync(templatesDir)) {
262
- const templateFiles = fs.readdirSync(templatesDir);
263
- for (const file of templateFiles) {
264
- const destPath = path.join(destTemplatesDir, file);
265
- fs.copyFileSync(path.join(templatesDir, file), destPath);
266
- trackFile(destPath);
267
- }
268
- console.log(` Installed ${templateFiles.length} templates \u2192 ~/.claude/commands/flow/templates/`);
269
- }
270
-
271
- // 6. Merge statusLine into settings.json
272
- let settings = {};
273
- if (fs.existsSync(settingsPath)) {
274
- try {
275
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
276
- } catch (e) {
277
- // Corrupted settings -- start fresh but warn
278
- console.log(' Warning: could not parse existing settings.json, preserving as backup');
279
- fs.copyFileSync(settingsPath, settingsPath + '.bak');
280
- }
281
- }
282
- settings.statusLine = {
283
- type: 'command',
284
- command: `node "${path.join(hooksDir, 'flow-statusline.js')}"`
285
- };
286
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
287
- trackFile(settingsPath);
288
- console.log(' Configured statusLine in ~/.claude/settings.json');
289
-
290
- // 7. Write .source breadcrumb (for dev/setup.sh compat)
291
- const sourcePath = path.join(commandsDir, '.source');
292
- fs.writeFileSync(sourcePath, pkgRoot + '\n');
293
- trackFile(sourcePath);
294
-
295
- // ---------- Post-install verification ----------
296
- let warnings = 0;
297
-
298
- // Check: at least 1 file in commandsDir
299
- const installedSkills = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
300
- if (installedSkills.length === 0) {
301
- console.log(' Warning: no skill files found in commands/flow/');
302
- warnings++;
303
- }
304
-
305
- // Check: both hook files
306
- const installedHooks = hookFiles.filter(h => fs.existsSync(path.join(hooksDir, h)));
307
- if (installedHooks.length < 2) {
308
- console.log(` Warning: only ${installedHooks.length}/2 hook files installed`);
309
- warnings++;
310
- }
311
-
312
- // Check: VERSION file
313
- if (!fs.existsSync(path.join(commandsDir, 'VERSION'))) {
314
- console.log(' Warning: VERSION file not found after install');
315
- warnings++;
316
- }
317
-
318
- // Check: templates directory with at least 1 file
319
- const tplDir = path.join(commandsDir, 'templates');
320
- const tplFiles = fs.existsSync(tplDir) ? fs.readdirSync(tplDir) : [];
321
- if (tplFiles.length === 0) {
322
- console.log(' Warning: no template files found after install');
323
- warnings++;
324
- }
325
-
326
- if (warnings > 0) {
327
- console.log(` (${warnings} verification warning${warnings > 1 ? 's' : ''} — install may be incomplete)`);
328
- }
329
-
330
- // Done
331
- const version = fs.readFileSync(versionFile, 'utf8').trim();
332
- console.log(`
333
- Flow v${version} installed successfully!
334
-
335
- Commands available:
336
- /flow:intro \u2014 Learn the Flow workflow
337
- /flow:setup \u2014 Set up a new project with full roadmap
338
- /flow:milestone \u2014 Add new milestones to the roadmap
339
- /flow:spec \u2014 Spec interview \u2192 executable PRD
340
- /flow:go \u2014 Execute next phase with agent teams
341
- /flow:done \u2014 Session-end documentation
342
- /flow:status \u2014 Quick orientation
343
- /flow:task \u2014 Lightweight task execution
344
- /flow:update \u2014 Update Flow to latest version
345
-
346
- Get started: run /flow:intro in any Claude Code session.
347
- `);
348
- } catch (err) {
349
- rollback();
350
- console.error('\nInstall failed: ' + err.message);
351
- process.exit(1);
352
- }
1
+ #!/usr/bin/env node
2
+ // Flow plugin installer for Claude Code
3
+ // Usage: npx flow-cc [--uninstall] [--verify]
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // ---------- Node.js version check ----------
10
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
11
+ if (nodeMajor < 18) {
12
+ console.error('Flow requires Node.js 18 or later. You have ' + process.version);
13
+ process.exit(1);
14
+ }
15
+
16
+ const homeDir = os.homedir();
17
+ const claudeDir = path.join(homeDir, '.claude');
18
+ const commandsDir = path.join(claudeDir, 'commands', 'flow');
19
+ const hooksDir = path.join(claudeDir, 'hooks');
20
+ const cacheDir = path.join(claudeDir, 'cache');
21
+ const settingsPath = path.join(claudeDir, 'settings.json');
22
+
23
+ // Source directories (relative to package root, one level up from bin/)
24
+ const pkgRoot = path.resolve(__dirname, '..');
25
+ const skillsDir = path.join(pkgRoot, 'skills');
26
+ const srcHooksDir = path.join(pkgRoot, 'hooks');
27
+ const templatesDir = path.join(pkgRoot, 'templates');
28
+ const versionFile = path.join(pkgRoot, 'VERSION');
29
+
30
+ const uninstall = process.argv.includes('--uninstall') || process.argv.includes('-u');
31
+ const verify = process.argv.includes('--verify') || process.argv.includes('-v');
32
+ const changelog = process.argv.includes('--changelog');
33
+
34
+ // ---------- Changelog ----------
35
+ if (changelog) {
36
+ const changelogPath = path.join(pkgRoot, 'CHANGELOG.md');
37
+ try {
38
+ console.log(fs.readFileSync(changelogPath, 'utf8'));
39
+ } catch (e) {
40
+ console.log('CHANGELOG not available');
41
+ }
42
+ process.exit(0);
43
+ }
44
+
45
+ // ---------- Verify ----------
46
+ if (verify) {
47
+ console.log('Flow install health check:\n');
48
+
49
+ let passed = 0;
50
+ const total = 5;
51
+
52
+ // 1. Skills installed
53
+ try {
54
+ const files = fs.existsSync(commandsDir)
55
+ ? fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'))
56
+ : [];
57
+ if (files.length > 0) {
58
+ console.log(` \u2713 Skills installed (${files.length} files)`);
59
+ passed++;
60
+ } else {
61
+ console.log(' \u2717 Skills not installed (0 .md files in commands/flow/)');
62
+ }
63
+ } catch (e) {
64
+ console.log(' \u2717 Skills not installed (cannot read commands/flow/)');
65
+ }
66
+
67
+ // 2. Hooks installed
68
+ try {
69
+ const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
70
+ const found = hookFiles.filter(h => fs.existsSync(path.join(hooksDir, h)));
71
+ if (found.length === 2) {
72
+ console.log(' \u2713 Hooks installed (2 files)');
73
+ passed++;
74
+ } else {
75
+ console.log(` \u2717 Hooks incomplete (${found.length}/2 files)`);
76
+ }
77
+ } catch (e) {
78
+ console.log(' \u2717 Hooks not installed');
79
+ }
80
+
81
+ // 3. VERSION file
82
+ try {
83
+ const vPath = path.join(commandsDir, 'VERSION');
84
+ if (fs.existsSync(vPath)) {
85
+ const ver = fs.readFileSync(vPath, 'utf8').trim();
86
+ console.log(` \u2713 VERSION file present (${ver})`);
87
+ passed++;
88
+ } else {
89
+ console.log(' \u2717 VERSION file missing');
90
+ }
91
+ } catch (e) {
92
+ console.log(' \u2717 VERSION file missing');
93
+ }
94
+
95
+ // 4. Templates installed
96
+ try {
97
+ const tDir = path.join(commandsDir, 'templates');
98
+ const files = fs.existsSync(tDir) ? fs.readdirSync(tDir) : [];
99
+ if (files.length > 0) {
100
+ console.log(` \u2713 Templates installed (${files.length} files)`);
101
+ passed++;
102
+ } else {
103
+ console.log(' \u2717 Templates not installed (0 files)');
104
+ }
105
+ } catch (e) {
106
+ console.log(' \u2717 Templates not installed');
107
+ }
108
+
109
+ // 5. StatusLine configured
110
+ try {
111
+ if (fs.existsSync(settingsPath)) {
112
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
113
+ if (settings.statusLine && typeof settings.statusLine.command === 'string' &&
114
+ settings.statusLine.command.includes('flow-statusline')) {
115
+ console.log(' \u2713 StatusLine configured');
116
+ passed++;
117
+ } else {
118
+ console.log(' \u2717 StatusLine not configured');
119
+ }
120
+ } else {
121
+ console.log(' \u2717 StatusLine not configured (no settings.json)');
122
+ }
123
+ } catch (e) {
124
+ console.log(' \u2717 StatusLine not configured');
125
+ }
126
+
127
+ console.log(`\nResult: ${passed}/${total} checks passed`);
128
+ process.exit(passed === total ? 0 : 1);
129
+ }
130
+
131
+ // ---------- Uninstall ----------
132
+ if (uninstall) {
133
+ console.log('Uninstalling Flow plugin...\n');
134
+
135
+ // Remove flow commands directory
136
+ if (fs.existsSync(commandsDir)) {
137
+ fs.rmSync(commandsDir, { recursive: true });
138
+ console.log(' Removed ~/.claude/commands/flow/');
139
+ }
140
+
141
+ // Remove flow hooks
142
+ const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
143
+ for (const hook of hookFiles) {
144
+ const hookPath = path.join(hooksDir, hook);
145
+ if (fs.existsSync(hookPath)) {
146
+ fs.unlinkSync(hookPath);
147
+ console.log(` Removed ~/.claude/hooks/${hook}`);
148
+ }
149
+ }
150
+
151
+ // Remove update cache
152
+ const cacheFile = path.join(cacheDir, 'flow-update-check.json');
153
+ if (fs.existsSync(cacheFile)) {
154
+ fs.unlinkSync(cacheFile);
155
+ console.log(' Removed ~/.claude/cache/flow-update-check.json');
156
+ }
157
+
158
+ // Remove statusLine from settings.json if it points to flow
159
+ if (fs.existsSync(settingsPath)) {
160
+ try {
161
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
162
+ if (settings.statusLine && typeof settings.statusLine.command === 'string' &&
163
+ settings.statusLine.command.includes('flow-statusline')) {
164
+ delete settings.statusLine;
165
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
166
+ console.log(' Removed statusLine from ~/.claude/settings.json');
167
+ }
168
+ } catch (e) {
169
+ // settings.json parse error -- leave it alone
170
+ }
171
+ }
172
+
173
+ console.log('\nFlow plugin uninstalled.');
174
+ process.exit(0);
175
+ }
176
+
177
+ // ---------- Install ----------
178
+ console.log('Installing Flow plugin...\n');
179
+
180
+ // ---------- Write permission check ----------
181
+ const permTestDir = fs.existsSync(claudeDir) ? claudeDir : path.dirname(claudeDir);
182
+ const permTestFile = path.join(permTestDir, '.flow-perm-test-' + process.pid);
183
+ try {
184
+ fs.writeFileSync(permTestFile, 'test');
185
+ fs.unlinkSync(permTestFile);
186
+ } catch (e) {
187
+ console.error('Cannot write to ' + claudeDir + ' \u2014 check permissions.');
188
+ process.exit(1);
189
+ }
190
+
191
+ // ---------- Rollback tracking ----------
192
+ const created = []; // paths created during install (files and dirs we created fresh)
193
+
194
+ function trackFile(filePath) {
195
+ created.push({ type: 'file', path: filePath });
196
+ }
197
+
198
+ function trackDir(dirPath) {
199
+ // Only track if we actually created it (didn't exist before)
200
+ if (!fs.existsSync(dirPath)) {
201
+ created.push({ type: 'dir', path: dirPath });
202
+ }
203
+ }
204
+
205
+ function rollback() {
206
+ console.error('\nRolling back partial install...');
207
+ for (let i = created.length - 1; i >= 0; i--) {
208
+ const item = created[i];
209
+ try {
210
+ if (item.type === 'file' && fs.existsSync(item.path)) {
211
+ fs.unlinkSync(item.path);
212
+ } else if (item.type === 'dir' && fs.existsSync(item.path)) {
213
+ fs.rmSync(item.path, { recursive: true });
214
+ }
215
+ } catch (e) {
216
+ // Best-effort cleanup
217
+ }
218
+ }
219
+ console.error('Rollback complete.');
220
+ }
221
+
222
+ try {
223
+ // 1. Create directories
224
+ for (const dir of [commandsDir, hooksDir, cacheDir]) {
225
+ trackDir(dir);
226
+ fs.mkdirSync(dir, { recursive: true });
227
+ }
228
+
229
+ // 2. Copy skills: skills/flow-*.md -> commands/flow/*.md (strip "flow-" prefix)
230
+ const skillFiles = fs.readdirSync(skillsDir).filter(f => f.startsWith('flow-') && f.endsWith('.md'));
231
+ for (const file of skillFiles) {
232
+ const dest = file.replace(/^flow-/, '');
233
+ const destPath = path.join(commandsDir, dest);
234
+ fs.copyFileSync(path.join(skillsDir, file), destPath);
235
+ trackFile(destPath);
236
+ }
237
+ console.log(` Installed ${skillFiles.length} skills \u2192 ~/.claude/commands/flow/`);
238
+
239
+ // 3. Copy hooks
240
+ const hookFiles = ['flow-check-update.js', 'flow-statusline.js'];
241
+ for (const hook of hookFiles) {
242
+ const src = path.join(srcHooksDir, hook);
243
+ if (fs.existsSync(src)) {
244
+ const destPath = path.join(hooksDir, hook);
245
+ fs.copyFileSync(src, destPath);
246
+ trackFile(destPath);
247
+ }
248
+ }
249
+ console.log(' Installed hooks \u2192 ~/.claude/hooks/');
250
+
251
+ // 4. Copy VERSION
252
+ const versionDest = path.join(commandsDir, 'VERSION');
253
+ fs.copyFileSync(versionFile, versionDest);
254
+ trackFile(versionDest);
255
+ console.log(' Installed VERSION \u2192 ~/.claude/commands/flow/VERSION');
256
+
257
+ // 5. Copy templates
258
+ const destTemplatesDir = path.join(commandsDir, 'templates');
259
+ trackDir(destTemplatesDir);
260
+ fs.mkdirSync(destTemplatesDir, { recursive: true });
261
+ if (fs.existsSync(templatesDir)) {
262
+ const templateFiles = fs.readdirSync(templatesDir);
263
+ for (const file of templateFiles) {
264
+ const destPath = path.join(destTemplatesDir, file);
265
+ fs.copyFileSync(path.join(templatesDir, file), destPath);
266
+ trackFile(destPath);
267
+ }
268
+ console.log(` Installed ${templateFiles.length} templates \u2192 ~/.claude/commands/flow/templates/`);
269
+ }
270
+
271
+ // 6. Merge statusLine into settings.json
272
+ let settings = {};
273
+ if (fs.existsSync(settingsPath)) {
274
+ try {
275
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
276
+ } catch (e) {
277
+ // Corrupted settings -- start fresh but warn
278
+ console.log(' Warning: could not parse existing settings.json, preserving as backup');
279
+ fs.copyFileSync(settingsPath, settingsPath + '.bak');
280
+ }
281
+ }
282
+ settings.statusLine = {
283
+ type: 'command',
284
+ command: `node "${path.join(hooksDir, 'flow-statusline.js')}"`
285
+ };
286
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
287
+ trackFile(settingsPath);
288
+ console.log(' Configured statusLine in ~/.claude/settings.json');
289
+
290
+ // 7. Write .source breadcrumb (for dev/setup.sh compat)
291
+ const sourcePath = path.join(commandsDir, '.source');
292
+ fs.writeFileSync(sourcePath, pkgRoot + '\n');
293
+ trackFile(sourcePath);
294
+
295
+ // ---------- Post-install verification ----------
296
+ let warnings = 0;
297
+
298
+ // Check: at least 1 file in commandsDir
299
+ const installedSkills = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
300
+ if (installedSkills.length === 0) {
301
+ console.log(' Warning: no skill files found in commands/flow/');
302
+ warnings++;
303
+ }
304
+
305
+ // Check: both hook files
306
+ const installedHooks = hookFiles.filter(h => fs.existsSync(path.join(hooksDir, h)));
307
+ if (installedHooks.length < 2) {
308
+ console.log(` Warning: only ${installedHooks.length}/2 hook files installed`);
309
+ warnings++;
310
+ }
311
+
312
+ // Check: VERSION file
313
+ if (!fs.existsSync(path.join(commandsDir, 'VERSION'))) {
314
+ console.log(' Warning: VERSION file not found after install');
315
+ warnings++;
316
+ }
317
+
318
+ // Check: templates directory with at least 1 file
319
+ const tplDir = path.join(commandsDir, 'templates');
320
+ const tplFiles = fs.existsSync(tplDir) ? fs.readdirSync(tplDir) : [];
321
+ if (tplFiles.length === 0) {
322
+ console.log(' Warning: no template files found after install');
323
+ warnings++;
324
+ }
325
+
326
+ if (warnings > 0) {
327
+ console.log(` (${warnings} verification warning${warnings > 1 ? 's' : ''} — install may be incomplete)`);
328
+ }
329
+
330
+ // Done
331
+ const version = fs.readFileSync(versionFile, 'utf8').trim();
332
+ console.log(`
333
+ Flow v${version} installed successfully!
334
+
335
+ Commands available:
336
+ /flow:intro \u2014 Learn the Flow workflow
337
+ /flow:setup \u2014 Set up a new project with full roadmap
338
+ /flow:triage \u2014 Sort brain dump into issues, milestones, lessons
339
+ /flow:spec \u2014 Spec interview \u2192 executable PRD
340
+ /flow:go \u2014 Execute next phase with agent teams
341
+ /flow:done \u2014 Session-end documentation
342
+ /flow:status \u2014 Quick orientation
343
+ /flow:task \u2014 Lightweight task execution
344
+ /flow:update \u2014 Update Flow to latest version
345
+
346
+ Get started: run /flow:intro in any Claude Code session.
347
+ `);
348
+ } catch (err) {
349
+ rollback();
350
+ console.error('\nInstall failed: ' + err.message);
351
+ process.exit(1);
352
+ }