@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/bin/cli.js +0 -27
- package/lib/doctor.js +14 -42
- package/lib/install.js +121 -1359
- package/lib/panel.js +10 -26
- package/package.json +1 -1
- package/lib/hook-overrides.js +0 -262
- package/lib/preview.js +0 -360
package/lib/install.js
CHANGED
|
@@ -1,1406 +1,168 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
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
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
//
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
for (const
|
|
1162
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1170
|
-
const
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1245
|
-
const
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
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
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
|
1285
|
-
const
|
|
1286
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
134
|
+
ui.step('Authenticating with GitHub...');
|
|
135
|
+
const token = await requireAuth();
|
|
1329
136
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
.
|
|
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
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
//
|
|
1360
|
-
|
|
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
|
-
|
|
1374
|
-
const
|
|
1375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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 =
|
|
1392
|
-
|
|
1393
|
-
|
|
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;
|