@axiomatic-labs/claudeflow 2.32.22 → 2.33.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/lib/install.js CHANGED
@@ -1,1406 +1,168 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execSync, execFileSync } = require('child_process');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
4
5
  const { requireAuth } = require('./auth.js');
5
6
  const { getLatestRelease, downloadReleaseAsset } = require('./download.js');
6
7
  const { writeLocalVersion, readLocalVersion } = require('./version.js');
7
- const { applyOverridesToFile: applyHookOverridesToFile } = require('./hook-overrides.js');
8
8
  const ui = require('./ui.js');
9
9
 
10
- // Template skills are shipped inside the ZIP. Copy every managed
11
- // claudeflow-* skill from the release while preserving any user-generated
12
- // skills that are not part of the shipped asset.
13
- const TEMPLATE_MANAGED_MANIFEST = path.join('.claudeflow', 'config', 'template-managed-manifest.json');
14
- const REQUIRED_TEMPLATE_RULES = [];
15
- const LEGACY_TEMPLATE_RULES = ['claudeflow-implementation.md', 'example-rules.md'];
16
- const SHARED_APPEND_PROMPT_TEMPLATE = path.join('.claudeflow', 'templates', 'claudeflow-core-system-prompt.md');
17
- const SHARED_APPEND_PROMPT_OUTPUT = 'append-system-prompt.md';
18
- const DEFAULT_CLAUDE_MD_TEMPLATE = path.join('.claudeflow', 'templates', 'claudeflow-default-claude-md.md');
19
- const DEFAULT_CLAUDE_MD_OUTPUT = 'CLAUDE.md';
20
- const TEMPLATE_MANAGED_SETTINGS_KEYS = ['$schema', 'enabledMcpjsonServers', 'env', 'permissions', 'hooks'];
21
- const SERENA_REPO = 'git+https://github.com/oraios/serena';
22
- const TEMPLATE_SEEDED_SETTINGS_KEYS = new Set(['statusLine']);
10
+ // claudeflow installs a small, namespaced footprint. Most of it is self-contained files that are simply
11
+ // PLACED (playbooks, scripts, skills) — those can't conflict with the user's project. The only things
12
+ // that need a MERGE (never a clobber) are .claude/settings.json (register our hooks alongside theirs)
13
+ // and CLAUDE.md (append our guidance block). That merge lives here, in Node, so install works on every
14
+ // platform with no shell dependency. The GitHub auth gate (auth.js/download.js) is untouched.
23
15
 
24
- async function run() {
25
- const current = readLocalVersion();
26
- const isUpdate = !!current;
27
- let copiedTemplateSkillCount = 0;
28
- let staleRemovedCount = 0;
29
- let managedPathCount = 0;
30
-
31
- ui.banner(current || undefined);
32
-
33
- ui.step('Authenticating with GitHub...');
34
- const token = await requireAuth();
35
-
36
- ui.step('Fetching latest release...');
37
- const release = await getLatestRelease(token);
38
- const version = release.tag_name;
39
-
40
- // Find the ZIP asset
41
- const asset = release.assets.find((a) => a.name.endsWith('.zip'));
42
- if (!asset) {
43
- ui.error('No ZIP asset found in the latest release.');
44
- process.exit(1);
45
- }
46
-
47
- ui.step(`Downloading ${version}...`);
48
- const zipBuffer = await downloadReleaseAsset(asset, token);
49
-
50
- // Write ZIP to temp file and extract to a temp directory
51
- const tmpZip = path.join(require('os').tmpdir(), `claudeflow-${Date.now()}.zip`);
52
- const tmpExtract = path.join(require('os').tmpdir(), `claudeflow-extract-${Date.now()}`);
53
- fs.writeFileSync(tmpZip, zipBuffer);
54
- fs.mkdirSync(tmpExtract, { recursive: true });
55
-
56
- const cwd = process.cwd();
57
-
58
- try {
59
- ui.step('Extracting template files...');
60
- // -qq silences unzip's per-file `inflating:` output, which on a large
61
- // ZIP exceeds execSync's default 1MB maxBuffer and triggers ENOBUFS.
62
- // maxBuffer override is belt-and-suspenders for future growth.
63
- execSync(`unzip -qq -o "${tmpZip}" -d "${tmpExtract}"`, {
64
- stdio: 'pipe',
65
- maxBuffer: 50 * 1024 * 1024,
66
- });
67
- const srcClaudeflow = path.join(tmpExtract, '.claudeflow');
68
- // v2.13.148+: mode is per-build, not per-project. There are no more
69
- // variants/{boost,normal} directories — the template ships a single
70
- // .claude/ tree, and claudeflow-build asks "fast or normal?" at each
71
- // invocation. So we source directly from the zip's .claude/ root.
72
- const srcClaude = path.join(tmpExtract, '.claude');
73
- const nextManagedPaths = collectManagedPathsFromRelease(tmpExtract);
74
- const previousManifest = readTemplateManagedManifest(cwd);
75
- const previousManagedPaths = previousManifest
76
- ? previousManifest.managedPaths
77
- : collectLegacyManagedPathsFromDisk(cwd);
78
-
79
- if (isUpdate && !previousManifest) {
80
- ui.info('No template-managed manifest found. Running aggressive legacy prune for managed paths.');
81
- }
82
-
83
- const staleManagedPaths = [...previousManagedPaths].filter((relPath) => !nextManagedPaths.has(relPath));
84
- staleRemovedCount = pruneStaleManagedPaths(cwd, staleManagedPaths);
85
-
86
- // Merge settings.json so template-managed keys refresh on update while
87
- // preserving user customizations like custom statusLine commands.
88
- const srcSettings = path.join(srcClaude, 'settings.json');
89
- const dstSettings = path.join(cwd, '.claude', 'settings.json');
90
- fs.mkdirSync(path.join(cwd, '.claude'), { recursive: true });
91
- if (fs.existsSync(srcSettings)) {
92
- syncClaudeSettings(srcSettings, dstSettings);
93
- // Settings.json now ships with every hook command wrapped through
94
- // run-with-override.js. The wrapper consults user-hook-overrides.json
95
- // at invocation time, so toggles persist without any settings rewrite.
96
- }
97
-
98
- // Upsert playwright entry in .mcp.json with project-specific CDP port.
99
- // IMPORTANT: read existing .mcp.json first and merge — never overwrite
100
- // wholesale. Users may have custom MCP servers (claude-in-chrome, Linear,
101
- // GitHub, etc) that must be preserved across install/update runs. Only
102
- // the three template-managed entries (playwright, serena, context7) are
103
- // allowed to be mutated by the installer.
104
- const cdpHash = require('crypto').createHash('sha1').update(cwd).digest();
105
- const cdpPort = 9200 + (cdpHash.readUInt16BE(2) % 100);
106
- const mcpPath = path.join(cwd, '.mcp.json');
107
- let mcpConfig = { mcpServers: {} };
108
- try {
109
- const existing = fs.readFileSync(mcpPath, 'utf8');
110
- mcpConfig = JSON.parse(existing);
111
- if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
112
- mcpConfig.mcpServers = {};
113
- }
114
- } catch {
115
- // File doesn't exist or is invalid JSON — start fresh with empty servers
116
- }
117
- // Spread first so user-added fields (env with API keys, cwd, etc.) survive.
118
- // Only command/args are CLI-canonical and get overridden.
119
- mcpConfig.mcpServers.playwright = {
120
- ...mcpConfig.mcpServers.playwright,
121
- command: "npx",
122
- args: [
123
- "@playwright/mcp@latest",
124
- "--cdp-endpoint", `http://127.0.0.1:${cdpPort}`,
125
- "--caps", "vision,devtools",
126
- ],
127
- };
128
- fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
129
-
130
- // Copy all template-managed skills from the ZIP, including subskills.
131
- const srcSkills = path.join(srcClaude, 'skills');
132
- const dstSkills = path.join(cwd, '.claude', 'skills');
133
- if (fs.existsSync(srcSkills)) {
134
- fs.mkdirSync(dstSkills, { recursive: true });
135
- const skillDirs = fs.readdirSync(srcSkills, { withFileTypes: true });
136
- for (const entry of skillDirs) {
137
- if (entry.isDirectory() && entry.name.startsWith('claudeflow-')) {
138
- copyDirSync(path.join(srcSkills, entry.name), path.join(dstSkills, entry.name));
139
- copiedTemplateSkillCount += 1;
140
- }
141
- }
142
- }
143
-
144
- // Copy all hooks
145
- const srcHooks = path.join(srcClaude, 'hooks');
146
- const dstHooks = path.join(cwd, '.claude', 'hooks');
147
- if (fs.existsSync(srcHooks)) {
148
- copyDirSync(srcHooks, dstHooks);
149
- }
150
-
151
- // Copy shared runtime scripts used by skills and hooks
152
- const srcRuntime = path.join(srcClaudeflow, 'runtime');
153
- const dstRuntime = path.join(cwd, '.claudeflow', 'runtime');
154
- if (fs.existsSync(srcRuntime)) {
155
- copyDirSync(srcRuntime, dstRuntime);
156
- }
157
-
158
- // Copy canonical template-managed assets used by init/update composition
159
- const srcTemplates = path.join(srcClaudeflow, 'templates');
160
- const dstTemplates = path.join(cwd, '.claudeflow', 'templates');
161
- if (fs.existsSync(srcTemplates)) {
162
- copyDirSync(srcTemplates, dstTemplates);
163
- }
164
-
165
- // Copy v2 orchestrator playbook layer (foundation/methodologies/domain/
166
- // ui-ux/routing). Required for claudeflow-build / -test / -review skills
167
- // to @-mention. Without this, those skills' @.claudeflow/playbooks/...
168
- // refs point to non-existent files.
169
- const srcPlaybooks = path.join(srcClaudeflow, 'playbooks');
170
- const dstPlaybooks = path.join(cwd, '.claudeflow', 'playbooks');
171
- if (fs.existsSync(srcPlaybooks)) {
172
- copyDirSync(srcPlaybooks, dstPlaybooks);
173
- }
174
-
175
- materializeSharedAppendPrompt(cwd);
176
- ensureDefaultClaudeMdRules(cwd);
177
- propagateTemplateRules(cwd, srcTemplates);
178
- // v2: scaffold the 8 system rules (3 always-loaded + 5 path-scoped)
179
- // using scaffold-system-rule.js. When setup-context.json exists
180
- // (brownfield), it derives framework-aware `paths` frontmatter. When
181
- // setup-context.json is absent (greenfield), it falls back to the
182
- // template's broad-default paths. No-clobber preserved — existing
183
- // user customizations win.
184
- propagateSystemRulesViaScaffold(cwd);
185
- // After CLAUDE.md exists, append the claudeflow addendum (the load-bearing
186
- // rule "all code changes flow through /claudeflow-build" + mode-aware
187
- // mandates). Idempotent: runs every install/update; the block lives
188
- // between <!-- claudeflow:boost-mode:BEGIN/END --> markers.
189
- syncBoostAddendumOnInstall(cwd);
190
-
191
- // Copy template agents (only template-managed, preserve user agents)
192
- const srcAgents = path.join(srcClaude, 'agents');
193
- const dstAgents = path.join(cwd, '.claude', 'agents');
194
- if (fs.existsSync(srcAgents)) {
195
- fs.mkdirSync(dstAgents, { recursive: true });
196
- const agentFiles = fs.readdirSync(srcAgents);
197
- for (const file of agentFiles) {
198
- const name = file.replace(/\.md$/, '');
199
- if (isTemplateManagedAgent(name)) {
200
- fs.copyFileSync(path.join(srcAgents, file), path.join(dstAgents, file));
201
- }
202
- }
203
- }
204
-
205
- // Remove previously template-seeded base rules. Project memory now comes
206
- // from setup-context, generated project rules, and root CLAUDE.md.
207
- pruneLegacyTemplateRules(cwd);
208
- // v2 cleanup: remove v1 build-orchestrator artifacts (phases/, references/,
209
- // and v1-only enforcement hooks). These were template-managed in earlier
210
- // versions but aren't in the v2 manifest, so install.js's stale-removal
211
- // doesn't always identify them. Be explicit.
212
- pruneLegacyV1BuildArtifacts(cwd);
213
-
214
- // Copy commands
215
- const srcCommands = path.join(srcClaude, 'commands');
216
- const dstCommands = path.join(cwd, '.claude', 'commands');
217
- if (fs.existsSync(srcCommands)) {
218
- copyDirSync(srcCommands, dstCommands);
219
- }
220
-
221
- // Copy all docs
222
- const srcDocs = path.join(srcClaudeflow, 'docs');
223
- const dstDocs = path.join(cwd, '.claudeflow', 'docs');
224
- if (fs.existsSync(srcDocs)) {
225
- copyDirSync(srcDocs, dstDocs);
226
- }
227
-
228
- // Ensure .claudeflow/tmp exists and copy bundled scripts
229
- fs.mkdirSync(path.join(cwd, '.claudeflow', 'tmp'), { recursive: true });
230
- const srcTmp = path.join(srcClaudeflow, 'tmp');
231
- if (fs.existsSync(srcTmp)) {
232
- const tmpFiles = fs.readdirSync(srcTmp);
233
- for (const file of tmpFiles) {
234
- const srcFile = path.join(srcTmp, file);
235
- const dstFile = path.join(cwd, '.claudeflow', 'tmp', file);
236
- if (fs.statSync(srcFile).isFile()) {
237
- fs.copyFileSync(srcFile, dstFile);
238
- }
239
- }
240
- }
241
-
242
- // v2.13.148+: variants/ and active-mode were removed. Mode (fast/normal)
243
- // is now per-build, persisted in workflow-state.runs[run_id].top_level.mode.
244
- // The user picks the mode via AskUserQuestion at every claudeflow-build
245
- // invocation. No global project-level mode marker.
246
-
247
- // Write version file
248
- writeLocalVersion(version);
249
- nextManagedPaths.add(normalizeRelativePath(path.join('.claudeflow', 'version')));
250
-
251
- // Persist managed path manifest for deterministic stale-file pruning on update
252
- writeTemplateManagedManifest(cwd, version, nextManagedPaths);
253
- managedPathCount = nextManagedPaths.size;
254
-
255
- } finally {
256
- fs.unlinkSync(tmpZip);
257
- fs.rmSync(tmpExtract, { recursive: true, force: true });
258
- }
259
-
260
- // install does NOT touch running processes — it only writes template files.
261
- // The observer daemon / dev servers are never killed here; a fresh daemon
262
- // with the updated runtime code is picked up on the next session, or via the
263
- // /claudeflow-observer-reset skill. Killing the daemon on update used to cascade
264
- // into tearing down the user's dev servers, so update stays purely a file copy.
265
-
266
- // Install analysis tools (ast-grep + Serena MCP)
267
- installAnalysisTools();
268
-
269
- // Count installed items
270
- const skillsDir = path.join(cwd, '.claude', 'skills');
271
- let skillCount = 0;
272
- try {
273
- skillCount = fs.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).length;
274
- } catch {}
275
-
276
- const hooksDir = path.join(cwd, '.claude', 'hooks');
277
- let hookCount = 0;
278
- try {
279
- hookCount = fs.readdirSync(hooksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).length;
280
- } catch {}
281
-
282
- const docsDir = path.join(cwd, '.claudeflow', 'docs');
283
- let docCount = 0;
284
- try {
285
- docCount = fs.readdirSync(docsDir).filter((f) => !f.startsWith('.')).length;
286
- } catch {}
287
-
288
- console.log('');
289
- if (isUpdate) {
290
- ui.success(`${skillCount} skills (${copiedTemplateSkillCount} template skills updated, user skills preserved)`);
291
- ui.success(`${hookCount} hooks updated`);
292
- ui.success(`${docCount} docs updated`);
293
- ui.success(`${staleRemovedCount} stale template-managed files removed`);
294
- ui.success(`Template-managed manifest updated (${managedPathCount} paths)`);
295
- ui.info('Default Claude Code status line is seeded when missing. Existing custom statusLine settings are preserved on update and can be overridden in .claude/settings.local.json.');
296
-
297
- if (current === version) {
298
- ui.done(`Claudeflow ${version} reinstalled.`);
299
- } else {
300
- ui.done(`Claudeflow updated: ${current} → ${version}`);
301
- }
302
-
303
- // Warn if setup-context.json is missing (legacy install or never-init).
304
- // The orchestrator and skills assume this file exists; without it they
305
- // fall back to inferring from CLAUDE.md + package.json, which works but
306
- // is less precise. The user can regenerate it via /claudeflow-init or
307
- // /claudeflow-update inside Claude Code.
308
- const setupContextPath = path.join(cwd, '.claudeflow', 'config', 'setup-context.json');
309
- if (!fs.existsSync(setupContextPath)) {
310
- ui.info(`Note: .claudeflow/config/setup-context.json is missing. Builds will fall back to inferring stack from CLAUDE.md + package.json. To regenerate the canonical setup-context, run /claudeflow-init or /claudeflow-update inside Claude Code.`);
311
- }
312
-
313
- } else {
314
- ui.success(`${skillCount} skills installed`);
315
- ui.success(`${hookCount} hooks installed`);
316
- ui.success(`${docCount} docs installed`);
317
- ui.success(`Template-managed manifest written (${managedPathCount} paths)`);
318
- ui.info('Default Claude Code status line installed. Override it in .claude/settings.json or .claude/settings.local.json.');
319
-
320
- ui.done(`Claudeflow ${version} installed.`);
321
- }
322
-
323
- // v2.14.43 Gap #7: warn if enforcement is disabled. Lives outside the
324
- // update/fresh-install branches so it fires in both flows (a user can
325
- // start with enforcement on, then later toggle it off via panel, and
326
- // a subsequent fresh install in the same dir would miss the warning if
327
- // the check sat inside the update branch only).
328
- try {
329
- const overridesPath = path.join(cwd, '.claudeflow', 'config', 'user-hook-overrides.json');
330
- if (fs.existsSync(overridesPath)) {
331
- const overrides = JSON.parse(fs.readFileSync(overridesPath, 'utf8'));
332
- if (overrides && overrides.enforcementDisabled === true) {
333
- ui.info(`⚠️ ENFORCEMENT IS DISABLED — your .claudeflow/config/user-hook-overrides.json has "enforcementDisabled": true. All Claudeflow PreToolUse strict gates (phase tasks, plan agents, methodology, marker) are silenced. To re-enable: run \`claudeflow panel\` and toggle enforcement on, or edit user-hook-overrides.json to set "enforcementDisabled": false.`);
334
- }
335
- }
336
- } catch {
337
- // Best-effort: malformed JSON should not block install.
338
- }
339
-
340
- // Ensure the `claudeflow` shell command is available and matches the
341
- // just-installed template version. Runs for both fresh installs and
342
- // updates — the shell command is independent of the project template
343
- // and needs to stay in sync either way.
344
- const cliStatus = ensureGlobalCli(version);
345
-
346
- // Getting started guide with project detection
347
- showGettingStarted(cwd, cliStatus);
348
- }
349
-
350
- function ensureGlobalCli(version) {
351
- // Returns { available: boolean, autoInstalled: boolean, upgraded: boolean, fromVersion?: string, error?: string }
352
- // `commandExists('claudeflow')` yields a false positive when install.js runs
353
- // under `npx @axiomatic-labs/claudeflow ...` — npx injects its own
354
- // `node_modules/.bin` into PATH and the subprocess sees the self-referential
355
- // binary even though nothing is installed globally. Cross-check against
356
- // `npm ls -g` (which never reports the npx cache) so the auto-installer
357
- // does not skip a missing global.
358
- const installedVersion = readGlobalCliVersion();
359
- const present = Boolean(installedVersion);
360
- const needsUpgrade = present && installedVersion !== version;
361
-
362
- if (present && !needsUpgrade) {
363
- return { available: true, autoInstalled: false, upgraded: false };
364
- }
365
-
366
- if (process.env.CLAUDEFLOW_SKIP_GLOBAL_INSTALL === '1') {
367
- return {
368
- available: present,
369
- autoInstalled: false,
370
- upgraded: false,
371
- fromVersion: installedVersion || undefined,
372
- error: 'skipped',
373
- };
374
- }
375
-
376
- console.log('');
377
- if (needsUpgrade) {
378
- console.log(
379
- ` ${ui.DIM}Upgrading global claudeflow CLI from ${installedVersion} to ${version}...${ui.RESET}`
380
- );
381
- } else {
382
- console.log(
383
- ` ${ui.DIM}Installing claudeflow CLI globally so it is available as a shell command...${ui.RESET}`
384
- );
385
- }
386
- try {
387
- execSync(`npm install -g @axiomatic-labs/claudeflow@${version}`, {
388
- stdio: ['ignore', 'pipe', 'pipe'],
389
- timeout: 120000,
390
- });
391
- // Verify with `npm ls -g` rather than `command -v`. Under npx, the
392
- // subprocess PATH still contains the npx cache, so `command -v`
393
- // would report true even if the global install wrote to a prefix
394
- // that the user's shell cannot see.
395
- const afterVersion = readGlobalCliVersion();
396
- if (afterVersion) {
397
- return {
398
- available: true,
399
- autoInstalled: !needsUpgrade,
400
- upgraded: needsUpgrade,
401
- fromVersion: installedVersion || undefined,
402
- shellPath: verifyShellCanFindCli(),
403
- };
404
- }
405
- return {
406
- available: false,
407
- autoInstalled: false,
408
- upgraded: false,
409
- error: 'installed but `npm ls -g` cannot see it — verify `npm config get prefix` is on $PATH',
410
- };
411
- } catch (err) {
412
- const msg = err?.stderr?.toString?.() || err?.message || String(err);
413
- return {
414
- available: present,
415
- autoInstalled: false,
416
- upgraded: false,
417
- fromVersion: installedVersion || undefined,
418
- error: msg.split('\n')[0],
419
- };
420
- }
421
- }
422
-
423
- function readGlobalCliVersion() {
424
- try {
425
- const raw = execSync('npm ls -g --json --depth=0 @axiomatic-labs/claudeflow', {
426
- stdio: ['ignore', 'pipe', 'ignore'],
427
- timeout: 10000,
428
- }).toString();
429
- const parsed = JSON.parse(raw);
430
- const v = parsed?.dependencies?.['@axiomatic-labs/claudeflow']?.version;
431
- return typeof v === 'string' ? v : null;
432
- } catch {
433
- return null;
434
- }
435
- }
436
-
437
- // Returns { ok: boolean | null, binPath?: string, shellRcFile?: string,
438
- // nvmDetected?: boolean, suggestedFix?: string }
439
- //
440
- // Distinguishes "package installed globally" from "package findable from
441
- // the user's interactive shell". Catches the common nvm-lazy-load setup
442
- // where `npm install -g` writes a binary the shell can't see until nvm
443
- // is sourced — typically discovered only when the user opens a fresh
444
- // terminal and types `claudeflow` and gets "command not found".
445
- function verifyShellCanFindCli() {
446
- // Skip on Windows; lazy-load + nvm-windows behave differently and the
447
- // login-shell trick below is brittle there.
448
- if (process.platform === 'win32') return { ok: null };
449
-
450
- const shellPath = process.env.SHELL;
451
- if (!shellPath || !fs.existsSync(shellPath)) return { ok: null };
452
-
453
- // Run a non-inherited interactive login shell — that's the closest
454
- // simulation of "user opens a new terminal" without polluting the
455
- // current process.
456
- let foundOnUserPath = null;
457
- try {
458
- const out = execSync(`${shellPath} -lic 'command -v claudeflow 2>/dev/null'`, {
459
- encoding: 'utf8',
460
- timeout: 6000,
461
- stdio: ['ignore', 'pipe', 'ignore'],
462
- }).trim();
463
- foundOnUserPath = out.length > 0;
464
- } catch {
465
- // Some shells exit non-zero when `command -v` finds nothing — treat
466
- // that as "not found" rather than as an inconclusive check.
467
- foundOnUserPath = false;
468
- }
469
-
470
- if (foundOnUserPath) return { ok: true };
471
-
472
- // Resolve where the binary actually lives so the warning is concrete.
473
- let binPath = null;
474
- try {
475
- const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim();
476
- binPath = path.join(prefix, 'bin', 'claudeflow');
477
- } catch {}
478
-
479
- const home = process.env.HOME || '';
480
- const nvmDetected = !!process.env.NVM_DIR
481
- || fs.existsSync(path.join(home, '.nvm', 'nvm.sh'));
482
- const isZsh = shellPath.endsWith('/zsh');
483
- const shellRcFile = isZsh ? '~/.zshrc' : (shellPath.endsWith('/bash') ? '~/.bashrc' : '~/.profile');
484
-
485
- // Wrapper-function fix that mirrors the user's existing nvm lazy-load
486
- // pattern (e.g., npx, node, npm). Single line, paste into rcFile, done.
487
- const wrapperFix = `claudeflow() { unset -f claudeflow; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; claudeflow "$@"; }`;
488
-
489
- return {
490
- ok: false,
491
- binPath,
492
- shellRcFile,
493
- nvmDetected,
494
- suggestedFix: wrapperFix,
495
- };
496
- }
497
-
498
- function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled: false }) {
499
- const MANIFESTS = ['package.json', 'pyproject.toml', 'Gemfile', 'go.mod', 'Cargo.toml', 'composer.json'];
500
- const SKIP_DIRS = new Set(['node_modules', 'vendor', '__pycache__', 'dist', 'build', '.next', '.nuxt', '.output', '.claude']);
501
-
502
- const rootHasManifest = MANIFESTS.some((f) => fs.existsSync(path.join(cwd, f)));
503
-
504
- // Scan first-level subdirs for manifests (multi-stack detection)
505
- const stackDirs = [];
506
- try {
507
- const entries = fs.readdirSync(cwd, { withFileTypes: true });
508
- for (const entry of entries) {
509
- if (!entry.isDirectory() || entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
510
- if (MANIFESTS.some((f) => fs.existsSync(path.join(cwd, entry.name, f)))) {
511
- stackDirs.push(entry.name);
512
- }
513
- }
514
- } catch {}
515
-
516
- const isMultiStack = stackDirs.length > 1 || (rootHasManifest && stackDirs.length > 0);
517
- const isGreenfield = !rootHasManifest && stackDirs.length === 0;
518
-
519
- console.log(` ${ui.BOLD}Getting started:${ui.RESET}`);
520
- console.log('');
521
-
522
- const startCommand = cliStatus.available ? 'claudeflow' : 'claude';
523
- if (cliStatus.autoInstalled) {
524
- console.log(` ${ui.DIM}✓ Installed ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} globally — available as a shell command.${ui.RESET}`);
525
- console.log('');
526
- } else if (cliStatus.upgraded) {
527
- console.log(
528
- ` ${ui.DIM}✓ Upgraded global ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} CLI${cliStatus.fromVersion ? ` from ${cliStatus.fromVersion}` : ''}.${ui.RESET}`
529
- );
530
- console.log('');
531
- }
532
-
533
- // Shell PATH warning — if the global package was installed but the
534
- // user's interactive shell can't find the binary (typical with
535
- // nvm lazy-load), emit a concrete fix.
536
- if (cliStatus.shellPath && cliStatus.shellPath.ok === false) {
537
- const sp = cliStatus.shellPath;
538
- console.log(` ${ui.YELLOW}⚠ ${ui.BOLD}claudeflow${ui.RESET}${ui.YELLOW} CLI installed but NOT on your shell PATH.${ui.RESET}`);
539
- if (sp.binPath) {
540
- console.log(` ${ui.DIM}Binary lives at: ${sp.binPath}${ui.RESET}`);
541
- }
542
- if (sp.nvmDetected) {
543
- console.log(` ${ui.DIM}Likely cause: nvm lazy-load. Add this to ${sp.shellRcFile}, then reopen terminal:${ui.RESET}`);
544
- console.log(` ${ui.CYAN}${sp.suggestedFix}${ui.RESET}`);
545
- } else {
546
- console.log(` ${ui.DIM}Add the npm prefix bin directory to your PATH in ${sp.shellRcFile}.${ui.RESET}`);
547
- }
548
- console.log('');
549
- }
550
-
551
- if (!cliStatus.available) {
552
- console.log(` ${ui.DIM}To get the ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} shell command, run:${ui.RESET}`);
553
- console.log(` ${ui.CYAN}npm i -g @axiomatic-labs/claudeflow${ui.RESET}`);
554
- if (cliStatus.error && cliStatus.error !== 'skipped') {
555
- console.log(` ${ui.DIM}(auto-install failed: ${cliStatus.error})${ui.RESET}`);
556
- }
557
- console.log('');
558
- }
559
- console.log(` 1. Run ${ui.CYAN}${startCommand}${ui.RESET} to start Claude Code`);
560
-
561
- if (isGreenfield) {
562
- console.log(` 2. Type ${ui.CYAN}/claudeflow-install${ui.RESET} to start setup`);
563
- console.log('');
564
- console.log(` ${ui.DIM}Claudeflow will route you into the new-project setup flow,${ui.RESET}`);
565
- console.log(` ${ui.DIM}research the stack, and generate project-specific skills.${ui.RESET}`);
566
- } else if (isMultiStack) {
567
- console.log(` 2. Type ${ui.CYAN}/claudeflow-install${ui.RESET} to start setup`);
568
- console.log('');
569
- ui.info(`Multi-stack project detected: ${stackDirs.join(', ')}${rootHasManifest ? ' + root' : ''}`);
570
- console.log('');
571
- console.log(` ${ui.DIM}Claudeflow will route you into the update flow and detect${ui.RESET}`);
572
- console.log(` ${ui.DIM}the relevant stacks before generating project skills.${ui.RESET}`);
573
- console.log('');
574
- console.log(` ${ui.BOLD}After setup:${ui.RESET}`);
575
- console.log(` ${ui.DIM}Just describe what you want — Claudeflow handles the rest.${ui.RESET}`);
576
- } else {
577
- // Single-stack existing project
578
- console.log(` 2. Type ${ui.CYAN}/claudeflow-install${ui.RESET} to start setup`);
579
- console.log(` ${ui.DIM}Claudeflow will route you into the existing-project update flow.${ui.RESET}`);
580
- console.log(` 3. Tell Claude what you want — it handles the rest`);
581
- }
582
-
583
- console.log('');
584
- console.log(` ${ui.BOLD}Slash commands (inside Claude Code):${ui.RESET}`);
585
- console.log(` ${ui.CYAN}/claudeflow-install${ui.RESET} ${ui.DIM}Smart router — scaffold a new project or update an existing one${ui.RESET}`);
586
- console.log(` ${ui.CYAN}/claudeflow-build${ui.RESET} ${ui.DIM}Execute multi-file implementation requests with explicit verification${ui.RESET}`);
587
- console.log(` ${ui.CYAN}/claudeflow-review${ui.RESET} ${ui.DIM}Senior review of a spec, request, or whole project${ui.RESET}`);
588
- console.log(` ${ui.CYAN}/claudeflow-test${ui.RESET} ${ui.DIM}Run applicable test types with anti-falsification enforcement${ui.RESET}`);
589
- console.log(` ${ui.CYAN}/claudeflow-ship${ui.RESET} ${ui.DIM}Release gate — classify blockers and deploy when ready${ui.RESET}`);
590
- console.log(` ${ui.CYAN}/claudeflow-prototype${ui.RESET} ${ui.DIM}Navigable hi-fi prototypes via the Discovery design wave; set-master to write the design foundation${ui.RESET}`);
591
- console.log(` ${ui.CYAN}/claudeflow-regen-claudemd${ui.RESET} ${ui.DIM}Regenerate CLAUDE.md from the latest template + setup-context${ui.RESET}`);
592
- console.log('');
593
- console.log(` ${ui.BOLD}Shell commands (outside Claude Code):${ui.RESET}`);
594
- console.log(` ${ui.CYAN}claudeflow${ui.RESET} ${ui.DIM}Start Claude Code with this project's claudeflow context${ui.RESET}`);
595
- console.log(` ${ui.CYAN}claudeflow panel${ui.RESET} ${ui.DIM}Open the local web dashboard — toggle hooks, reminders, enforcement${ui.RESET}`);
596
- console.log(` ${ui.CYAN}claudeflow doctor${ui.RESET} ${ui.DIM}Diagnose CDP-port and lockfile issues; ${ui.CYAN}--fix${ui.RESET}${ui.DIM} to auto-repair${ui.RESET}`);
597
- console.log(` ${ui.CYAN}claudeflow install${ui.RESET} ${ui.DIM}Refresh the template + global CLI in this project${ui.RESET}`);
598
- console.log(` ${ui.CYAN}claudeflow version${ui.RESET} ${ui.DIM}Show installed framework + CLI versions${ui.RESET}`);
599
- console.log('');
600
- console.log(` ${ui.DIM}Docs: https://claudeflow.dev${ui.RESET}`);
601
- console.log('');
602
- }
603
-
604
- function syncClaudeSettings(srcSettingsPath, dstSettingsPath) {
605
- const templateSettings = readJsonObject(srcSettingsPath, {
606
- label: 'template .claude/settings.json',
607
- throwOnInvalid: true,
608
- });
609
- const existingSettings = readJsonObject(dstSettingsPath, {
610
- label: 'existing .claude/settings.json',
611
- warnOnInvalid: true,
612
- }) || {};
613
- const mergedSettings = mergeClaudeSettings(templateSettings, existingSettings);
614
- fs.writeFileSync(dstSettingsPath, `${JSON.stringify(mergedSettings, null, 2)}\n`);
615
- }
616
-
617
- function mergeClaudeSettings(templateSettings, existingSettings = {}) {
618
- const template = isPlainObject(templateSettings) ? templateSettings : {};
619
- const existing = isPlainObject(existingSettings) ? existingSettings : {};
620
- const merged = {};
621
-
622
- for (const [key, value] of Object.entries(template)) {
623
- if (TEMPLATE_MANAGED_SETTINGS_KEYS.includes(key)) {
624
- merged[key] = cloneJson(value);
625
- continue;
626
- }
627
- if (TEMPLATE_SEEDED_SETTINGS_KEYS.has(key)) {
628
- merged[key] = Object.prototype.hasOwnProperty.call(existing, key)
629
- ? cloneJson(existing[key])
630
- : cloneJson(value);
631
- }
632
- }
633
-
634
- for (const [key, value] of Object.entries(existing)) {
635
- if (Object.prototype.hasOwnProperty.call(merged, key)) continue;
636
- if (TEMPLATE_MANAGED_SETTINGS_KEYS.includes(key)) continue;
637
- merged[key] = cloneJson(value);
638
- }
639
-
640
- return merged;
641
- }
642
-
643
- function readJsonObject(filePath, options = {}) {
644
- if (!fs.existsSync(filePath)) return null;
645
- const { label = filePath, throwOnInvalid = false, warnOnInvalid = false } = options;
646
-
647
- try {
648
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
649
- if (isPlainObject(parsed)) return parsed;
650
-
651
- const message = `${label} must contain a top-level JSON object`;
652
- if (throwOnInvalid) throw new Error(message);
653
- if (warnOnInvalid) ui.warn(`${message}. Replacing it with template-managed defaults.`);
654
- return null;
655
- } catch (error) {
656
- if (throwOnInvalid) throw error;
657
- if (warnOnInvalid) ui.warn(`${label} is invalid JSON. Replacing it with template-managed defaults.`);
658
- return null;
659
- }
660
- }
661
-
662
- function isPlainObject(value) {
663
- return value !== null && typeof value === 'object' && !Array.isArray(value);
664
- }
665
-
666
- function cloneJson(value) {
667
- if (value === undefined) return undefined;
668
- return JSON.parse(JSON.stringify(value));
669
- }
670
-
671
- // Install ast-grep CLI, uv, and Serena MCP for runtime and analysis.
672
- // All steps are best-effort — failures warn but never block the install.
673
- function installAnalysisTools() {
674
- console.log('');
675
- ui.step('Setting up runtime & analysis tools...');
676
-
677
- // 1. ast-grep CLI
678
- const hasAstGrep = commandExists('sg');
679
- if (hasAstGrep) {
680
- ui.success('ast-grep CLI already installed');
681
- } else {
682
- ui.step('Installing ast-grep CLI...');
683
- try {
684
- execSync('npm install -g @ast-grep/cli', { stdio: 'pipe', timeout: 60000 });
685
- ui.success('ast-grep CLI installed');
686
- } catch {
687
- ui.warn('ast-grep install failed — code exploration will use Grep fallback');
688
- }
689
- }
690
-
691
- // 2. uv (required by Serena)
692
- let hasUv = commandExists('uv');
693
- if (hasUv) {
694
- ui.success('uv already installed');
695
- } else {
696
- ui.step('Installing uv (Python package manager)...');
697
- try {
698
- if (process.platform === 'win32') {
699
- execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
700
- stdio: 'pipe',
701
- timeout: 60000,
702
- });
703
- } else {
704
- execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
705
- stdio: 'pipe',
706
- timeout: 60000,
707
- });
708
- }
709
- // uv installs to ~/.local/bin or ~/.cargo/bin — check both
710
- hasUv = commandExists('uv') || commandExists(path.join(homedir(), '.local', 'bin', 'uv'));
711
- if (hasUv) {
712
- ui.success('uv installed');
713
- } else {
714
- ui.warn('uv installed but not in PATH — restart your terminal before running init/update');
715
- }
716
- } catch {
717
- ui.warn('uv install failed — Serena MCP will not be available');
718
- }
719
- }
720
-
721
- // 3. Write MCP servers directly to .mcp.json (more reliable than claude mcp add)
722
- configureMcpServers(hasUv);
723
-
724
- // 4. Prime Serena for the current project when possible.
725
- if (hasUv) {
726
- indexSerenaProject(process.cwd());
727
- }
728
- }
729
-
730
- // Write Serena and context7 MCP configs directly to .mcp.json.
731
- // This is more reliable than `claude mcp add` which can misparse flags.
732
- function configureMcpServers(hasUv) {
733
- const cwd = process.cwd();
734
- const mcpPath = path.join(cwd, '.mcp.json');
735
-
736
- // Read existing .mcp.json or start fresh
737
- let mcpConfig = { mcpServers: {} };
738
- try {
739
- const existing = fs.readFileSync(mcpPath, 'utf8');
740
- mcpConfig = JSON.parse(existing);
741
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
742
- } catch {
743
- // File doesn't exist or is invalid — start fresh
744
- }
745
-
746
- let changed = false;
747
-
748
- // Serena MCP (requires uv)
749
- if (hasUv) {
750
- const serenaCommand = commandExists('uvx') ? 'uvx' : 'uv';
751
- const serenaArgs = serenaCommand === 'uvx'
752
- ? [
753
- '--from', SERENA_REPO,
754
- 'serena', 'start-mcp-server',
755
- '--context', 'claude-code',
756
- '--project-from-cwd',
757
- ]
758
- : [
759
- 'run', '--with', SERENA_REPO,
760
- 'serena', 'start-mcp-server',
761
- '--context', 'claude-code',
762
- '--project-from-cwd',
763
- ];
764
- ui.step(mcpConfig.mcpServers.serena ? 'Refreshing Serena MCP configuration...' : 'Adding Serena MCP for semantic code analysis...');
765
- // Spread first so user-added fields (env with API keys, cwd, etc.) survive.
766
- mcpConfig.mcpServers.serena = {
767
- ...mcpConfig.mcpServers.serena,
768
- type: 'stdio',
769
- command: serenaCommand,
770
- args: serenaArgs,
771
- };
772
- changed = true;
773
- ui.success('Serena MCP configured');
774
- }
775
-
776
- // context7 MCP
777
- ui.step(mcpConfig.mcpServers.context7 ? 'Refreshing context7 MCP configuration...' : 'Adding context7 MCP for library documentation...');
778
- // Spread first so user-added fields (env with API keys, cwd, etc.) survive.
779
- mcpConfig.mcpServers.context7 = {
780
- ...mcpConfig.mcpServers.context7,
781
- type: 'stdio',
782
- command: 'npx',
783
- args: ['-y', '@upstash/context7-mcp@latest'],
784
- };
785
- changed = true;
786
- ui.success('context7 MCP configured');
787
-
788
- // codegraph MCP — AST-local code-graph for blast-radius / impact analysis.
789
- // Runs via npx (no uv/Ollama/API key needed), so it is unconditional like context7.
790
- // The build agents' investigation workflow uses it when available ("code-graph MCP
791
- // if available, else grep") for transitive impact/callers; superior to grep for
792
- // blast-radius. Index is built on first use; blind to .astro/.sql (agents fall back to grep).
793
- ui.step(mcpConfig.mcpServers.codegraph ? 'Refreshing codegraph MCP configuration...' : 'Adding codegraph MCP for blast-radius analysis...');
794
- // Spread first so user-added fields (cwd, env, etc.) survive.
795
- mcpConfig.mcpServers.codegraph = {
796
- ...mcpConfig.mcpServers.codegraph,
797
- type: 'stdio',
798
- command: 'npx',
799
- args: ['--yes', '@colbymchenry/codegraph@latest', 'serve', '--mcp'],
800
- };
801
- changed = true;
802
- ui.success('codegraph MCP configured');
803
-
804
- // Write .mcp.json if anything changed
805
- if (changed) {
806
- try {
807
- fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
808
- } catch (e) {
809
- ui.warn(`Could not write .mcp.json: ${e.message}`);
810
- }
811
- }
812
- }
813
-
814
- // Check if a command exists in PATH
815
- function commandExists(cmd) {
816
- try {
817
- const check = process.platform === 'win32' ? `where "${cmd}"` : `command -v "${cmd}"`;
818
- execSync(check, { stdio: 'pipe' });
819
- return true;
820
- } catch {
821
- return false;
822
- }
823
- }
824
-
825
- function indexSerenaProject(projectRoot) {
826
- const statusPath = path.join(projectRoot, '.claudeflow', 'tmp', 'tooling', 'serena-status.json');
827
-
828
- // Idempotent reinstall: skip indexing if a previous install already indexed
829
- // the project successfully. To force a reindex, delete the marker file.
830
- try {
831
- const existing = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
832
- if (existing && existing.indexed === true) {
833
- ui.success('Serena project index already present (delete .claudeflow/tmp/tooling/serena-status.json to force reindex)');
834
- return;
835
- }
836
- } catch {}
837
-
838
- ui.step('Indexing project for Serena...');
839
- const attempts = [
840
- { command: 'uvx', args: ['--from', SERENA_REPO, 'serena', 'project', 'index', projectRoot] },
841
- { command: 'uv', args: ['run', '--with', SERENA_REPO, 'serena', 'project', 'index', projectRoot] },
842
- ];
843
-
844
- let failureReason = 'uv/uvx not available';
845
- let indexed = false;
846
- for (const attempt of attempts) {
847
- if (!commandExists(attempt.command)) continue;
848
- try {
849
- execFileSync(attempt.command, attempt.args, { cwd: projectRoot, stdio: 'pipe', timeout: 120000 });
850
- indexed = true;
851
- failureReason = null;
852
- break;
853
- } catch (error) {
854
- failureReason = error.stderr ? String(error.stderr).trim() : error.message;
855
- }
856
- }
857
-
858
- fs.mkdirSync(path.dirname(statusPath), { recursive: true });
859
- fs.writeFileSync(statusPath, `${JSON.stringify({
860
- configured: true,
861
- indexed,
862
- last_attempt_at: new Date().toISOString(),
863
- failure_reason: failureReason,
864
- }, null, 2)}\n`);
865
-
866
- if (indexed) {
867
- ui.success('Serena project index ready');
868
- } else {
869
- ui.warn('Serena indexing failed — semantic navigation may need a manual retry');
870
- }
871
- }
872
-
873
-
874
- function normalizeRelativePath(value) {
875
- return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
876
- }
877
-
878
- function isPathInside(candidatePath, parentPath) {
879
- const relative = path.relative(parentPath, candidatePath);
880
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
881
- }
882
-
883
- function collectFilesRecursive(rootDir) {
884
- if (!fs.existsSync(rootDir)) return [];
885
- const files = [];
886
- const stack = [rootDir];
887
- while (stack.length > 0) {
888
- const current = stack.pop();
889
- const entries = fs.readdirSync(current, { withFileTypes: true });
890
- for (const entry of entries) {
891
- const fullPath = path.join(current, entry.name);
892
- if (entry.isDirectory()) {
893
- stack.push(fullPath);
894
- } else if (entry.isFile()) {
895
- files.push(fullPath);
896
- }
897
- }
898
- }
899
- return files;
900
- }
901
-
902
- function addFilesFromDir(set, rootBase, targetDir, opts = {}) {
903
- const { destPrefix } = opts;
904
- for (const filePath of collectFilesRecursive(targetDir)) {
905
- let relPath;
906
- if (destPrefix !== undefined) {
907
- const subRel = normalizeRelativePath(path.relative(targetDir, filePath));
908
- relPath = normalizeRelativePath(subRel ? `${destPrefix}/${subRel}` : destPrefix);
909
- } else {
910
- relPath = normalizeRelativePath(path.relative(rootBase, filePath));
911
- }
912
- if (relPath.startsWith('.claude/')) {
913
- set.add(relPath);
914
- }
915
- }
916
- }
917
-
918
- function collectManagedPathsFromRelease(extractRoot) {
919
- const srcClaudeflow = path.join(extractRoot, '.claudeflow');
920
- // v2.13.148+: single .claude/ tree at the zip root, no variants.
921
- const srcClaude = path.join(extractRoot, '.claude');
922
- const managed = new Set();
923
-
924
- const settingsPath = path.join(srcClaude, 'settings.json');
925
- if (fs.existsSync(settingsPath)) {
926
- managed.add('.claude/settings.json');
927
- }
928
-
929
- addFilesFromDir(managed, extractRoot, path.join(srcClaude, 'hooks'), {
930
- destPrefix: '.claude/hooks',
931
- });
932
-
933
- // Claudeflow dirs: runtime, templates, docs, playbooks in .claudeflow/.
934
- // playbooks/ added in v2 — required for the claudeflow-build orchestrator
935
- // and its always-loaded foundation/routing references.
936
- ['runtime', 'templates', 'docs', 'playbooks'].forEach((folder) => {
937
- addFilesFromDir(managed, extractRoot, path.join(srcClaudeflow, folder));
938
- });
16
+ const CLAUDE_MARK = '<!-- claudeflow:begin -->';
17
+ const HOOK_TAG = 'scripts/claudeflow/hook_'; // identifies OUR hook entries in settings.json
18
+ const PER_PROJECT_CONFIGS = ['brand.json', 'risk-signals.json']; // create-if-absent, never clobber
19
+ const SHARED_CONFIGS = ['playbook-map.json']; // always refreshed (shared system)
939
20
 
940
- const skillsDir = path.join(srcClaude, 'skills');
941
- if (fs.existsSync(skillsDir)) {
942
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
943
- for (const entry of entries) {
944
- if (entry.isDirectory() && entry.name.startsWith('claudeflow-')) {
945
- addFilesFromDir(managed, extractRoot, path.join(skillsDir, entry.name), {
946
- destPrefix: `.claude/skills/${entry.name}`,
947
- });
948
- }
949
- }
950
- }
21
+ function rmrf(p) { fs.rmSync(p, { recursive: true, force: true }); }
951
22
 
952
- const agentsDir = path.join(srcClaude, 'agents');
953
- if (fs.existsSync(agentsDir)) {
954
- const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
955
- for (const entry of entries) {
956
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
957
- const agentName = entry.name.replace(/\.md$/, '');
958
- if (!isTemplateManagedAgent(agentName)) continue;
959
- managed.add(normalizeRelativePath(path.join('.claude', 'agents', entry.name)));
960
- }
961
- }
962
-
963
- const tmpDir = path.join(srcClaudeflow, 'tmp');
964
- if (fs.existsSync(tmpDir)) {
965
- const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
966
- for (const entry of entries) {
967
- if (entry.isFile()) {
968
- managed.add(normalizeRelativePath(path.join('.claudeflow', 'tmp', entry.name)));
969
- }
970
- }
971
- }
972
-
973
- const versionFile = path.join(srcClaudeflow, 'version');
974
- if (fs.existsSync(versionFile)) {
975
- managed.add('.claudeflow/version');
976
- }
977
-
978
- return managed;
979
- }
980
-
981
- function collectLegacyManagedPathsFromDisk(projectRoot) {
982
- const claudeDir = path.join(projectRoot, '.claude');
983
- const claudeflowDir = path.join(projectRoot, '.claudeflow');
984
- const managed = new Set();
985
-
986
- const settingsPath = path.join(claudeDir, 'settings.json');
987
- if (fs.existsSync(settingsPath)) {
988
- managed.add('.claude/settings.json');
989
- }
990
-
991
- // Claude Code native: hooks stay in .claude/
992
- addFilesFromDir(managed, projectRoot, path.join(claudeDir, 'hooks'));
993
-
994
- // Claudeflow dirs: runtime, templates, docs, playbooks in .claudeflow/.
995
- // playbooks/ added in v2 — required for the claudeflow-build orchestrator.
996
- // (variants/ removed in v2.13.148. .claudeflow/cli/ removed in v2.13.153.)
997
- ['runtime', 'templates', 'docs', 'playbooks'].forEach((folder) => {
998
- addFilesFromDir(managed, projectRoot, path.join(claudeflowDir, folder));
999
- });
1000
-
1001
- const skillsDir = path.join(claudeDir, 'skills');
1002
- if (fs.existsSync(skillsDir)) {
1003
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
1004
- for (const entry of entries) {
1005
- if (entry.isDirectory() && entry.name.startsWith('claudeflow-')) {
1006
- addFilesFromDir(managed, projectRoot, path.join(skillsDir, entry.name));
1007
- }
1008
- }
1009
- }
1010
-
1011
- const agentsDir = path.join(claudeDir, 'agents');
1012
- if (fs.existsSync(agentsDir)) {
1013
- const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
1014
- for (const entry of entries) {
1015
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
1016
- const agentName = entry.name.replace(/\.md$/, '');
1017
- if (!isTemplateManagedAgent(agentName)) continue;
1018
- managed.add(normalizeRelativePath(path.join('.claude', 'agents', entry.name)));
1019
- }
1020
- }
1021
-
1022
- const tmpDir = path.join(claudeflowDir, 'tmp');
1023
- if (fs.existsSync(tmpDir)) {
1024
- const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
1025
- for (const entry of entries) {
1026
- if (entry.isFile() && entry.name.endsWith('.py')) {
1027
- managed.add(normalizeRelativePath(path.join('.claudeflow', 'tmp', entry.name)));
1028
- }
1029
- }
1030
- }
1031
-
1032
- const versionFile = path.join(claudeflowDir, 'version');
1033
- if (fs.existsSync(versionFile)) {
1034
- managed.add('.claudeflow/version');
23
+ function copyDir(src, dst) {
24
+ fs.mkdirSync(dst, { recursive: true });
25
+ for (const e of fs.readdirSync(src, { withFileTypes: true })) {
26
+ const s = path.join(src, e.name), d = path.join(dst, e.name);
27
+ if (e.isDirectory()) copyDir(s, d);
28
+ else if (e.isFile()) fs.copyFileSync(s, d);
1035
29
  }
1036
-
1037
- return managed;
1038
30
  }
1039
31
 
1040
- function readTemplateManagedManifest(projectRoot) {
1041
- const manifestPath = path.join(projectRoot, TEMPLATE_MANAGED_MANIFEST);
1042
- if (!fs.existsSync(manifestPath)) return null;
1043
-
1044
- try {
1045
- const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1046
- if (!Array.isArray(parsed.managed_paths)) return null;
1047
- const managedPaths = new Set(
1048
- parsed.managed_paths
1049
- .filter((entry) => typeof entry === 'string')
1050
- .map((entry) => normalizeRelativePath(entry))
1051
- .filter((entry) => entry.startsWith('.claude/')),
1052
- );
1053
- return {
1054
- version: typeof parsed.version === 'string' ? parsed.version : null,
1055
- managedPaths,
1056
- };
1057
- } catch {
1058
- return null;
32
+ function countFiles(dir) {
33
+ let n = 0;
34
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ if (e.isDirectory()) n += countFiles(path.join(dir, e.name));
36
+ else n++;
1059
37
  }
38
+ return n;
1060
39
  }
1061
40
 
1062
- function writeTemplateManagedManifest(projectRoot, version, managedPaths) {
1063
- const manifestPath = path.join(projectRoot, TEMPLATE_MANAGED_MANIFEST);
1064
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
1065
- const payload = {
1066
- schema_version: 1,
1067
- version,
1068
- updated_at: new Date().toISOString(),
1069
- managed_paths: [...managedPaths].sort(),
1070
- };
1071
- fs.writeFileSync(manifestPath, `${JSON.stringify(payload, null, 2)}\n`);
1072
- }
1073
-
1074
- function pruneStaleManagedPaths(projectRoot, staleRelativePaths) {
1075
- if (!Array.isArray(staleRelativePaths) || staleRelativePaths.length === 0) {
1076
- return 0;
41
+ function mergeSettings(cwd, payloadSettings) {
42
+ const sp = path.join(cwd, '.claude', 'settings.json');
43
+ let data = {};
44
+ if (fs.existsSync(sp)) { try { data = JSON.parse(fs.readFileSync(sp, 'utf8')); } catch { data = {}; } }
45
+ const hooks = data.hooks || (data.hooks = {});
46
+ // Drop any existing claudeflow hook entries (identified by our script path) so a re-install replaces
47
+ // stale matchers/timeouts; the user's OWN hooks (not referencing scripts/claudeflow/hook_) are kept.
48
+ for (const evt of Object.keys(hooks)) {
49
+ hooks[evt] = (hooks[evt] || []).filter(e => !(e.hooks || []).some(h => (h.command || '').includes(HOOK_TAG)));
50
+ if (!hooks[evt].length) delete hooks[evt];
1077
51
  }
1078
-
1079
- const claudeflowRoot = path.resolve(projectRoot, '.claude');
1080
- let removedCount = 0;
1081
- const uniquePaths = [...new Set(staleRelativePaths.map((entry) => normalizeRelativePath(entry)))]
1082
- .filter((entry) => entry.startsWith('.claude/'))
1083
- .sort((a, b) => b.length - a.length);
1084
-
1085
- for (const relPath of uniquePaths) {
1086
- const absolutePath = path.resolve(projectRoot, relPath);
1087
- if (!isPathInside(absolutePath, claudeflowRoot)) continue;
1088
- if (!fs.existsSync(absolutePath)) continue;
1089
-
1090
- fs.rmSync(absolutePath, { recursive: true, force: true });
1091
- removedCount += 1;
1092
- pruneEmptyAncestors(path.dirname(absolutePath), claudeflowRoot);
52
+ // Add the shipped claudeflow hook entries.
53
+ const ph = (payloadSettings && payloadSettings.hooks) || {};
54
+ for (const evt of Object.keys(ph)) {
55
+ const arr = hooks[evt] || (hooks[evt] = []);
56
+ for (const entry of ph[evt]) arr.push(entry);
1093
57
  }
1094
-
1095
- return removedCount;
58
+ if (payloadSettings && payloadSettings['$schema'] && !data['$schema']) data['$schema'] = payloadSettings['$schema'];
59
+ fs.mkdirSync(path.dirname(sp), { recursive: true });
60
+ fs.writeFileSync(sp, JSON.stringify(data, null, 2) + '\n');
1096
61
  }
1097
62
 
1098
- function pruneEmptyAncestors(startDir, stopDir) {
1099
- let current = startDir;
1100
- while (isPathInside(current, stopDir) && current !== stopDir) {
1101
- if (!fs.existsSync(current)) {
1102
- current = path.dirname(current);
1103
- continue;
1104
- }
1105
- const entries = fs.readdirSync(current);
1106
- if (entries.length > 0) break;
1107
- fs.rmdirSync(current);
1108
- current = path.dirname(current);
1109
- }
63
+ function appendClaudeMd(cwd, snippetPath) {
64
+ if (!fs.existsSync(snippetPath)) return false;
65
+ const target = path.join(cwd, 'CLAUDE.md');
66
+ if (fs.existsSync(target) && fs.readFileSync(target, 'utf8').includes(CLAUDE_MARK)) return false;
67
+ const snippet = fs.readFileSync(snippetPath, 'utf8');
68
+ const prefix = fs.existsSync(target) ? '\n' : '';
69
+ fs.appendFileSync(target, prefix + snippet);
70
+ return true;
1110
71
  }
1111
72
 
1112
- function assertRequiredTemplateRules(rulesDir) {
1113
- for (const filename of REQUIRED_TEMPLATE_RULES) {
1114
- const rulePath = path.join(rulesDir, filename);
1115
- if (!fs.existsSync(rulePath)) {
1116
- throw new Error(
1117
- `Release asset is missing required template rule .claude/rules/${filename}. Rebuild the release before installing.`,
1118
- );
1119
- }
1120
- }
1121
- }
73
+ // Place + merge the framework from an extracted payload dir into a project. Pure filesystem, no network —
74
+ // so it is testable on a throwaway dir.
75
+ function installFromPayload(src, cwd) {
76
+ const out = { playbooks: 0, scripts: 0, skills: 0, claudeMd: false };
1122
77
 
1123
- // v1 build-orchestrator artifacts that v2 replaces. When upgrading from a
1124
- // pre-v2 install, these stay on disk because install.js's stale cleanup
1125
- // can't match files that exist on disk but aren't in the v2 ZIP manifest.
1126
- // Listing them explicitly here makes the v1 → v2 upgrade leave a clean
1127
- // .claude/.
1128
- const LEGACY_V1_BUILD_ARTIFACTS = [
1129
- // v1 had phases/references inside build/test/review skills; v2 doesn't.
1130
- '.claude/skills/claudeflow-build/phases',
1131
- '.claude/skills/claudeflow-build/references',
1132
- '.claude/skills/claudeflow-test/phases',
1133
- '.claude/skills/claudeflow-test/references',
1134
- '.claude/skills/claudeflow-review/phases',
1135
- '.claude/skills/claudeflow-review/references',
1136
- // v1 enforcement hooks; v2 uses guard-sensitive-files + validate-closure +
1137
- // session-env + filter-command-output instead.
1138
- '.claude/hooks/PreToolUse/enforce-task-creation.js',
1139
- '.claude/hooks/PreToolUse/enforce-ecosystem-artifacts.js',
1140
- '.claude/hooks/PreToolUse/freeze-step-contract.js',
1141
- '.claude/hooks/PreToolUse/freeze-workflow-state.js',
1142
- '.claude/hooks/PreToolUse/route-exit-plan-mode.js',
1143
- '.claude/hooks/PreToolUse/stack-debt-guard.js',
1144
- ];
1145
-
1146
- function pruneLegacyV1BuildArtifacts(projectRoot) {
1147
- for (const relPath of LEGACY_V1_BUILD_ARTIFACTS) {
1148
- const absPath = path.join(projectRoot, relPath);
1149
- if (!fs.existsSync(absPath)) continue;
1150
- try {
1151
- fs.rmSync(absPath, { recursive: true, force: true });
1152
- } catch {
1153
- // Best-effort. If the file is locked or permissions issue, skip;
1154
- // the user can clean up manually. Don't abort the install.
1155
- }
78
+ // 1) Playbooks always refreshed (shared system).
79
+ const srcPlaybooks = path.join(src, '.claudeflow', 'playbooks');
80
+ if (fs.existsSync(srcPlaybooks)) {
81
+ const dst = path.join(cwd, '.claudeflow', 'playbooks');
82
+ rmrf(dst); copyDir(srcPlaybooks, dst);
83
+ out.playbooks = countFiles(dst);
1156
84
  }
1157
- }
1158
85
 
1159
- function pruneLegacyTemplateRules(projectRoot) {
1160
- const rulesDir = path.join(projectRoot, '.claude', 'rules');
1161
- for (const filename of LEGACY_TEMPLATE_RULES) {
1162
- fs.rmSync(path.join(rulesDir, filename), { force: true });
86
+ // 2) Configs — per-project ones create-if-absent (never clobber a customized brand); shared refresh.
87
+ fs.mkdirSync(path.join(cwd, '.claudeflow'), { recursive: true });
88
+ for (const c of PER_PROJECT_CONFIGS) {
89
+ const s = path.join(src, '.claudeflow', c), d = path.join(cwd, '.claudeflow', c);
90
+ if (fs.existsSync(s) && !fs.existsSync(d)) fs.copyFileSync(s, d);
1163
91
  }
1164
- if (fs.existsSync(rulesDir) && fs.readdirSync(rulesDir).length === 0) {
1165
- fs.rmdirSync(rulesDir);
92
+ for (const c of SHARED_CONFIGS) {
93
+ const s = path.join(src, '.claudeflow', c);
94
+ if (fs.existsSync(s)) fs.copyFileSync(s, path.join(cwd, '.claudeflow', c));
1166
95
  }
1167
- }
1168
96
 
1169
- function propagateTemplateRules(projectRoot, srcTemplates) {
1170
- const srcRulesDir = path.join(srcTemplates, 'rules');
1171
- if (!fs.existsSync(srcRulesDir)) return;
1172
- const dstRulesDir = path.join(projectRoot, '.claude', 'rules');
1173
- fs.mkdirSync(dstRulesDir, { recursive: true });
1174
- for (const filename of fs.readdirSync(srcRulesDir)) {
1175
- if (!filename.endsWith('.md')) continue;
1176
- // Skip the 8 v2 system rules — they're handled by
1177
- // propagateSystemRulesViaScaffold, which derives framework-aware
1178
- // `paths` from setup-context.json when available.
1179
- if (V2_SYSTEM_RULES.has(filename)) continue;
1180
- const dstPath = path.join(dstRulesDir, filename);
1181
- if (fs.existsSync(dstPath)) continue; // no-clobber: preserve project-specific or user rules
1182
- fs.copyFileSync(path.join(srcRulesDir, filename), dstPath);
97
+ // 3) Scripts (hooks + scans + servers) — always refreshed.
98
+ const srcScripts = path.join(src, 'scripts', 'claudeflow');
99
+ if (fs.existsSync(srcScripts)) {
100
+ const dst = path.join(cwd, 'scripts', 'claudeflow');
101
+ fs.mkdirSync(dst, { recursive: true });
102
+ for (const e of fs.readdirSync(srcScripts)) { fs.copyFileSync(path.join(srcScripts, e), path.join(dst, e)); out.scripts++; }
1183
103
  }
1184
- }
1185
-
1186
- // The 8 v2 system rules that ship with claudeflow. propagateTemplateRules
1187
- // skips these because they need scaffold-system-rule.js to derive
1188
- // framework-aware `paths` frontmatter (e.g., a Next.js+Prisma project
1189
- // gets `**/*.prisma` and `**/app/api/**` instead of broad defaults).
1190
- const V2_SYSTEM_RULES = new Set([
1191
- 'claudeflow-context-engineering.md',
1192
- 'claudeflow-engineering-quality.md',
1193
- 'claudeflow-testing.md',
1194
- 'claudeflow-security.md',
1195
- 'claudeflow-api.md',
1196
- 'claudeflow-database.md',
1197
- 'claudeflow-ui-ux.md',
1198
- 'claudeflow-evidence.md',
1199
- ]);
1200
-
1201
- // Map of v2 rule filenames → rule slug used by scaffold-system-rule.js
1202
- function ruleSlugFromFilename(filename) {
1203
- // Strip "claudeflow-" prefix and ".md" suffix. e.g.,
1204
- // "claudeflow-ui-ux.md" → "ui-ux".
1205
- return filename.replace(/^claudeflow-/, '').replace(/\.md$/, '');
1206
- }
1207
104
 
1208
- function propagateSystemRulesViaScaffold(projectRoot) {
1209
- const scaffoldScript = path.join(
1210
- projectRoot, '.claudeflow', 'runtime', 'scaffold-system-rule.js',
1211
- );
1212
- if (!fs.existsSync(scaffoldScript)) {
1213
- // scaffold-system-rule.js is part of v2 if missing, the project
1214
- // was bootstrapped with a pre-v2 ZIP. Skip silently; user can run
1215
- // /claudeflow-update later to refresh.
1216
- return;
1217
- }
1218
-
1219
- for (const filename of V2_SYSTEM_RULES) {
1220
- const slug = ruleSlugFromFilename(filename);
1221
- try {
1222
- execFileSync('node', [
1223
- scaffoldScript,
1224
- '--rule', slug,
1225
- '--project-root', projectRoot,
1226
- '--mode', 'init',
1227
- // No --force: respect no-clobber. User edits win.
1228
- ], { stdio: ['ignore', 'ignore', 'pipe'] });
1229
- } catch (err) {
1230
- // scaffold-system-rule.js exits 1 when target exists (no-clobber).
1231
- // That's expected on re-install; don't warn. Other exit codes are
1232
- // real errors; log to stderr but don't abort the install.
1233
- const exitCode = err && err.status;
1234
- if (exitCode !== 1) {
1235
- process.stderr.write(
1236
- `Warning: failed to scaffold system rule "${slug}" (exit ${exitCode}). ` +
1237
- `You can re-run with /claudeflow-update later.\n`,
1238
- );
1239
- }
105
+ // 4) Skills — replace each shipped claudeflow-* skill; preserve the user's own skills.
106
+ const srcSkills = path.join(src, '.claude', 'skills');
107
+ if (fs.existsSync(srcSkills)) {
108
+ const dst = path.join(cwd, '.claude', 'skills');
109
+ fs.mkdirSync(dst, { recursive: true });
110
+ for (const e of fs.readdirSync(srcSkills, { withFileTypes: true })) {
111
+ if (!e.isDirectory() || !e.name.startsWith('claudeflow-')) continue;
112
+ const d = path.join(dst, e.name);
113
+ rmrf(d); copyDir(path.join(srcSkills, e.name), d); out.skills++;
1240
114
  }
1241
115
  }
1242
- }
1243
116
 
1244
- function materializeSharedAppendPrompt(projectRoot) {
1245
- const templatePath = path.join(projectRoot, SHARED_APPEND_PROMPT_TEMPLATE);
1246
- if (!fs.existsSync(templatePath)) {
1247
- throw new Error(
1248
- `Shared append prompt template is missing: ${SHARED_APPEND_PROMPT_TEMPLATE}. Rebuild the release before installing.`,
1249
- );
117
+ // 5) settings.json hooks — MERGE (never clobber the user's settings).
118
+ const srcSettings = path.join(src, '.claude', 'settings.json');
119
+ if (fs.existsSync(srcSettings)) {
120
+ try { mergeSettings(cwd, JSON.parse(fs.readFileSync(srcSettings, 'utf8'))); } catch {}
1250
121
  }
1251
122
 
1252
- const outputPath = path.join(projectRoot, SHARED_APPEND_PROMPT_OUTPUT);
1253
- fs.copyFileSync(templatePath, outputPath);
1254
- }
1255
-
1256
- // Inject the canonical claudeflow rules block into the project's CLAUDE.md.
1257
- //
1258
- // Three cases:
1259
- // 1. CLAUDE.md absent → create with the block plus user-content section
1260
- // 2. CLAUDE.md present, no sentinel, no opt-out → prepend the block,
1261
- // preserving every byte of the existing file
1262
- // 3. CLAUDE.md present with sentinel → replace the block in place
1263
- // (keeps future template revisions propagating idempotently)
1264
- //
1265
- // User opt-out: drop the literal string `claudeflow:default-rules:opt-out`
1266
- // anywhere in CLAUDE.md (e.g. in a comment) to skip injection forever.
1267
- //
1268
- // The block is wrapped in HTML comments — markdown renders them invisibly
1269
- // but they survive editor round-trips and remain machine-detectable.
1270
- const CLAUDE_MD_INJECT_BEGIN = '<!-- claudeflow:default-rules:start (do not edit between markers) -->';
1271
- const CLAUDE_MD_INJECT_END = '<!-- claudeflow:default-rules:end -->';
1272
- const CLAUDE_MD_OPT_OUT = 'claudeflow:default-rules:opt-out';
123
+ // 6) CLAUDE.md — append the guidance block (idempotent).
124
+ out.claudeMd = appendClaudeMd(cwd, path.join(src, 'assets', 'CLAUDE.snippet.md'));
1273
125
 
1274
- function buildDefaultRulesBlock(projectRoot) {
1275
- const templatePath = path.join(projectRoot, DEFAULT_CLAUDE_MD_TEMPLATE);
1276
- if (!fs.existsSync(templatePath)) return null;
1277
- let body = fs.readFileSync(templatePath, 'utf8');
1278
- // Strip the leading `# CLAUDE.md` heading — when injected into an existing
1279
- // file the heading would conflict with the user's own H1.
1280
- body = body.replace(/^\s*#\s*CLAUDE\.md\s*\r?\n+/, '').trimEnd();
1281
- return `${CLAUDE_MD_INJECT_BEGIN}\n${body}\n${CLAUDE_MD_INJECT_END}`;
126
+ return out;
1282
127
  }
1283
128
 
1284
- function ensureDefaultClaudeMdRules(projectRoot) {
1285
- const block = buildDefaultRulesBlock(projectRoot);
1286
- if (!block) return { action: 'no-template' };
1287
-
1288
- const outputPath = path.join(projectRoot, DEFAULT_CLAUDE_MD_OUTPUT);
1289
-
1290
- if (!fs.existsSync(outputPath)) {
1291
- fs.writeFileSync(outputPath, `${block}\n`);
1292
- return { action: 'created' };
1293
- }
1294
-
1295
- const existing = fs.readFileSync(outputPath, 'utf8');
1296
-
1297
- if (existing.includes(CLAUDE_MD_OPT_OUT)) {
1298
- return { action: 'opt-out' };
1299
- }
1300
-
1301
- // The block always lives at the bottom. Two paths get us there:
1302
- // - sentinels found at the bottom and content current → unchanged
1303
- // - sentinels found anywhere else (top, middle, stale content) →
1304
- // remove the old block and append a fresh one at the tail.
1305
- // - sentinels absent → append for the first time.
1306
- const beginIdx = existing.indexOf(CLAUDE_MD_INJECT_BEGIN);
1307
- const endIdx = existing.indexOf(CLAUDE_MD_INJECT_END);
1308
- let userContent;
1309
- if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
1310
- const before = existing.slice(0, beginIdx);
1311
- const after = existing.slice(endIdx + CLAUDE_MD_INJECT_END.length);
1312
- userContent = `${before.replace(/\s+$/, '')}${after.startsWith('\n') ? '' : '\n'}${after}`.replace(/^\s+/, '').replace(/\s+$/, '');
1313
- } else {
1314
- userContent = existing.replace(/\s+$/, '');
1315
- }
1316
-
1317
- const next = userContent.length === 0
1318
- ? `${block}\n`
1319
- : `${userContent}\n\n${block}\n`;
1320
-
1321
- if (next === existing) return { action: 'unchanged' };
1322
- fs.writeFileSync(outputPath, next);
1323
- return { action: beginIdx !== -1 ? 'updated' : 'injected' };
1324
- }
129
+ async function run() {
130
+ const current = readLocalVersion();
131
+ ui.banner(current || undefined);
132
+ const cwd = process.cwd();
1325
133
 
1326
- function isTemplateManagedAgent(agentName) {
1327
- return agentName.startsWith('claudeflow-');
1328
- }
134
+ ui.step('Authenticating with GitHub...');
135
+ const token = await requireAuth();
1329
136
 
1330
- function listTemplateManagedAgents(agentsDir) {
1331
- if (!fs.existsSync(agentsDir)) return [];
1332
- return fs.readdirSync(agentsDir)
1333
- .filter((entry) => entry.endsWith('.md'))
1334
- .map((entry) => entry.replace(/\.md$/, ''))
1335
- .filter((name) => isTemplateManagedAgent(name))
1336
- .sort();
1337
- }
137
+ ui.step('Fetching latest release...');
138
+ const release = await getLatestRelease(token);
139
+ const asset = (release.assets || []).find(a => /\.zip$/.test(a.name));
140
+ if (!asset) throw new Error('No .zip asset found in the latest release.');
141
+ const version = (release.tag_name || '').replace(/^v/, '')
142
+ || asset.name.replace(/^claudeflow-?v?/, '').replace(/\.zip$/, '');
1338
143
 
1339
- // Recursively copy a directory, creating parents as needed
1340
- function copyDirSync(src, dst, skipNames) {
1341
- fs.mkdirSync(dst, { recursive: true });
1342
- const entries = fs.readdirSync(src, { withFileTypes: true });
1343
- for (const entry of entries) {
1344
- if (skipNames && skipNames.has(entry.name)) continue;
1345
- const srcPath = path.join(src, entry.name);
1346
- const dstPath = path.join(dst, entry.name);
1347
- if (entry.isDirectory()) {
1348
- copyDirSync(srcPath, dstPath);
1349
- } else {
1350
- fs.copyFileSync(srcPath, dstPath);
1351
- }
1352
- }
1353
- }
144
+ ui.step(`Downloading ${asset.name}...`);
145
+ const zipBuffer = await downloadReleaseAsset(asset, token);
1354
146
 
1355
- // Append the claudeflow addendum to CLAUDE.md (idempotent).
1356
- // The block between <!-- claudeflow:boost-mode:BEGIN/END --> markers carries
1357
- // the load-bearing rule ("all code changes flow through /claudeflow-build")
1358
- // plus mode-aware mandates. Re-runs on every install/update; refreshes a
1359
- // stale block if present. Marker name preserved for backwards compatibility
1360
- // with projects upgraded across the v2.13.148 variants-removal boundary.
1361
- function syncBoostAddendumOnInstall(cwd) {
1362
- // v2.13.152: in unified mode (mode is per-build, picked at /claudeflow-build
1363
- // invocation), the addendum is always injected when CLAUDE.md exists. The
1364
- // load-bearing rule "all code changes flow through /claudeflow-build" applies
1365
- // regardless of which mode is picked per build, so we no longer gate on
1366
- // .claudeflow/active-mode (which was removed in v2.13.148). Mode-specific
1367
- // mandates inside the addendum body explicitly say "fast-only" or
1368
- // "normal-only" so the LLM applies them conditionally.
1369
- const claudeMdPath = path.join(cwd, 'CLAUDE.md');
1370
- const addendumPath = path.join(cwd, '.claudeflow', 'templates', 'claudeflow-append-claude-md.md');
1371
- if (!fs.existsSync(claudeMdPath) || !fs.existsSync(addendumPath)) return;
147
+ const tmpZip = path.join(os.tmpdir(), `claudeflow-${Date.now()}.zip`);
148
+ const tmp = path.join(os.tmpdir(), `claudeflow-extract-${Date.now()}`);
149
+ fs.writeFileSync(tmpZip, zipBuffer);
150
+ fs.mkdirSync(tmp, { recursive: true });
151
+ // -qq silences unzip's per-file output (large ZIPs otherwise blow execSync's 1MB buffer).
152
+ execSync(`unzip -qq -o "${tmpZip}" -d "${tmp}"`, { stdio: 'ignore' });
1372
153
 
1373
- const ADDENDUM_BEGIN = '<!-- claudeflow:boost-mode:BEGIN -->';
1374
- const ADDENDUM_END = '<!-- claudeflow:boost-mode:END -->';
1375
- const original = fs.readFileSync(claudeMdPath, 'utf8');
1376
- const beginIdx = original.indexOf(ADDENDUM_BEGIN);
1377
- const endIdx = original.indexOf(ADDENDUM_END);
1378
- const hasBlock = beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx;
154
+ ui.step('Installing framework...');
155
+ const summary = installFromPayload(tmp, cwd);
156
+ writeLocalVersion(`v${version}`);
1379
157
 
1380
- let base = original;
1381
- if (hasBlock) {
1382
- const blockEnd = endIdx + ADDENDUM_END.length;
1383
- base = original.slice(0, beginIdx).replace(/\s*$/, '') + original.slice(blockEnd).replace(/^\s*/, '\n');
1384
- }
158
+ try { rmrf(tmp); fs.rmSync(tmpZip, { force: true }); } catch {}
1385
159
 
1386
- const addendumBody = fs.readFileSync(addendumPath, 'utf8').trimEnd();
1387
- const block = `\n\n${ADDENDUM_BEGIN}\n${addendumBody}\n${ADDENDUM_END}\n`;
1388
- fs.writeFileSync(claudeMdPath, base.replace(/\s*$/, '') + block);
160
+ ui.success(`claudeflow v${version} installed`);
161
+ ui.info(` ${summary.playbooks} playbooks · ${summary.scripts} scripts · ${summary.skills} skills`
162
+ + (summary.claudeMd ? ' · CLAUDE.md guidance added' : ''));
163
+ ui.done(`Describe a change and the ${ui.BOLD}claudeflow-implement${ui.RESET} skill will drive it.`);
1389
164
  }
1390
165
 
1391
- module.exports = Object.assign(run, {
1392
- assertRequiredTemplateRules,
1393
- materializeSharedAppendPrompt,
1394
- ensureDefaultClaudeMdRules,
1395
- buildDefaultRulesBlock,
1396
- CLAUDE_MD_INJECT_BEGIN,
1397
- CLAUDE_MD_INJECT_END,
1398
- CLAUDE_MD_OPT_OUT,
1399
- isTemplateManagedAgent,
1400
- listTemplateManagedAgents,
1401
- mergeClaudeSettings,
1402
- commandExists,
1403
- readGlobalCliVersion,
1404
- verifyShellCanFindCli,
1405
- showGettingStarted,
1406
- });
166
+ module.exports = run;
167
+ module.exports.installFromPayload = installFromPayload;
168
+ module.exports.mergeSettings = mergeSettings;