@aslomon/effectum 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/install.js CHANGED
@@ -1,98 +1,62 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const readline = require('readline');
7
- const os = require('os');
8
- const { spawnSync } = require('child_process');
9
-
10
- // ─── ANSI colors ───────────────────────────────────────────────────────────
11
- const c = {
12
- reset: '\x1b[0m',
13
- bold: '\x1b[1m',
14
- dim: '\x1b[2m',
15
- yellow: '\x1b[33m',
16
- cyan: '\x1b[36m',
17
- green: '\x1b[32m',
18
- red: '\x1b[31m',
19
- magenta: '\x1b[35m',
20
- blue: '\x1b[34m',
21
- white: '\x1b[37m',
22
- bgBlack: '\x1b[40m',
23
- };
24
-
25
- const bold = s => `${c.bold}${s}${c.reset}`;
26
- const yellow = s => `${c.yellow}${s}${c.reset}`;
27
- const cyan = s => `${c.cyan}${s}${c.reset}`;
28
- const green = s => `${c.green}${s}${c.reset}`;
29
- const red = s => `${c.red}${s}${c.reset}`;
30
- const dim = s => `${c.dim}${s}${c.reset}`;
31
-
32
- // ─── Banner ────────────────────────────────────────────────────────────────
33
- function printBanner() {
34
- console.log();
35
- console.log(yellow(' ⚡') + bold(yellow(' EFFECTUM')));
36
- console.log(dim(' Autonomous development system for Claude Code'));
37
- console.log(dim(' Describe what you want. Get production-ready code.'));
38
- console.log();
39
- }
40
-
41
- // ─── Readline helpers ──────────────────────────────────────────────────────
42
- function createRL() {
43
- return readline.createInterface({
44
- input: process.stdin,
45
- output: process.stdout,
46
- });
47
- }
48
-
49
- function ask(rl, question) {
50
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
51
- }
52
-
53
- async function askChoice(rl, question, choices, defaultIdx = 0) {
54
- console.log(question);
55
- choices.forEach((ch, i) => {
56
- const marker = i === defaultIdx ? green('▶') : ' ';
57
- const num = cyan(`${i + 1}`);
58
- const label = i === defaultIdx ? bold(ch.label) : ch.label;
59
- console.log(` ${marker} ${num}) ${label}${ch.desc ? dim(' — ' + ch.desc) : ''}`);
60
- });
61
- const answer = await ask(rl, ` ${dim(`[1-${choices.length}, default ${defaultIdx + 1}]:`)} `);
62
- const n = parseInt(answer, 10);
63
- if (!answer || isNaN(n) || n < 1 || n > choices.length) return defaultIdx;
64
- return n - 1;
65
- }
66
-
67
- async function confirm(rl, question, defaultYes = true) {
68
- const hint = defaultYes ? dim('[Y/n]') : dim('[y/N]');
69
- const answer = await ask(rl, `${question} ${hint} `);
70
- if (!answer) return defaultYes;
71
- return answer.toLowerCase().startsWith('y');
72
- }
73
-
74
- // ─── File helpers ──────────────────────────────────────────────────────────
75
- function ensureDir(dir) {
76
- fs.mkdirSync(dir, { recursive: true });
77
- }
2
+ /**
3
+ * Effectum interactive installer.
4
+ * Rewritten with @clack/prompts for full TUI experience.
5
+ *
6
+ * Usage:
7
+ * npx @aslomon/effectum → interactive install
8
+ * npx @aslomon/effectum --global → non-interactive global install
9
+ * npx @aslomon/effectum --local → non-interactive local install
10
+ * npx @aslomon/effectum --yes → non-interactive with smart defaults
11
+ * npx @aslomon/effectum --dry-run → show plan without writing
12
+ */
13
+ "use strict";
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const os = require("os");
18
+ const { spawnSync } = require("child_process");
19
+ const p = require("@clack/prompts");
20
+
21
+ const { detectAll } = require("./lib/detect");
22
+ const { loadStackPreset } = require("./lib/stack-parser");
23
+ const {
24
+ buildSubstitutionMap,
25
+ renderTemplate,
26
+ findTemplatePath,
27
+ findRemainingPlaceholders,
28
+ } = require("./lib/template");
29
+ const { writeConfig } = require("./lib/config");
30
+ const { AUTONOMY_MAP, FORMATTER_MAP, MCP_SERVERS } = require("./lib/constants");
31
+ const { ensureDir, deepMerge, findRepoRoot: findRepoRootShared } = require("./lib/utils");
32
+ const {
33
+ printBanner,
34
+ askProjectName,
35
+ askStack,
36
+ askLanguage,
37
+ askAutonomy,
38
+ askMcpServers,
39
+ askPlaywright,
40
+ askGitBranch,
41
+ showSummary,
42
+ showOutro,
43
+ } = require("./lib/ui");
78
44
 
79
45
  function copyFile(src, dest, opts = {}) {
80
- const { skipExisting = false } = opts;
81
- if (fs.existsSync(dest) && skipExisting) {
82
- return { status: 'skipped', dest };
46
+ if (fs.existsSync(dest) && opts.skipExisting) {
47
+ return { status: "skipped", dest };
83
48
  }
84
49
  ensureDir(path.dirname(dest));
85
50
  fs.copyFileSync(src, dest);
86
- return { status: 'created', dest };
51
+ return { status: "created", dest };
87
52
  }
88
53
 
89
54
  function copyDir(srcDir, destDir, opts = {}) {
90
55
  const results = [];
91
56
  if (!fs.existsSync(srcDir)) return results;
92
-
93
57
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
94
58
  for (const entry of entries) {
95
- const srcPath = path.join(srcDir, entry.name);
59
+ const srcPath = path.join(srcDir, entry.name);
96
60
  const destPath = path.join(destDir, entry.name);
97
61
  if (entry.isDirectory()) {
98
62
  results.push(...copyDir(srcPath, destPath, opts));
@@ -103,125 +67,35 @@ function copyDir(srcDir, destDir, opts = {}) {
103
67
  return results;
104
68
  }
105
69
 
106
- // ─── Deep-merge two plain objects ─────────────────────────────────────────
107
- function deepMerge(target, source) {
108
- const out = Object.assign({}, target);
109
- for (const key of Object.keys(source)) {
110
- if (
111
- source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
112
- out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])
113
- ) {
114
- out[key] = deepMerge(out[key], source[key]);
115
- } else {
116
- out[key] = source[key];
117
- }
118
- }
119
- return out;
120
- }
121
-
122
- // ─── Merge settings.json (template → existing) ────────────────────────────
123
- function mergeSettings(templatePath, destPath) {
124
- let template;
125
- try {
126
- template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
127
- } catch (e) {
128
- return { status: 'error', dest: destPath, error: `Could not read template: ${e.message}` };
129
- }
130
-
131
- let existing = {};
132
- if (fs.existsSync(destPath)) {
133
- try {
134
- existing = JSON.parse(fs.readFileSync(destPath, 'utf8'));
135
- } catch (_) {
136
- // corrupted/empty — overwrite
137
- }
138
- }
139
-
140
- // Template wins for all keys, but preserve keys only in existing
141
- const merged = deepMerge(existing, template);
142
-
143
- ensureDir(path.dirname(destPath));
144
- fs.writeFileSync(destPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
145
- return { status: 'created', dest: destPath };
146
- }
147
-
148
- // ─── Find repo root ────────────────────────────────────────────────────────
149
70
  function findRepoRoot() {
150
- const binDir = path.dirname(__filename);
151
- const repoRoot = path.resolve(binDir, '..');
152
- if (fs.existsSync(path.join(repoRoot, 'system', 'commands'))) {
153
- return repoRoot;
154
- }
155
- return repoRoot;
71
+ return findRepoRootShared();
156
72
  }
157
73
 
158
- // ─── Parse CLI args ────────────────────────────────────────────────────────
74
+ // ─── Parse CLI args ───────────────────────────────────────────────────────
75
+
159
76
  function parseArgs(argv) {
160
77
  const args = argv.slice(2);
161
78
  return {
162
- global: args.includes('--global') || args.includes('-g'),
163
- local: args.includes('--local') || args.includes('-l'),
164
- claude: args.includes('--claude'),
165
- withMcp: args.includes('--with-mcp'),
166
- withPlaywright: args.includes('--with-playwright'),
167
- nonInteractive: args.includes('--yes') || args.includes('-y') ||
168
- args.includes('--global') || args.includes('--local'),
169
- help: args.includes('--help') || args.includes('-h'),
79
+ global: args.includes("--global") || args.includes("-g"),
80
+ local: args.includes("--local") || args.includes("-l"),
81
+ claude: args.includes("--claude"),
82
+ withMcp: args.includes("--with-mcp"),
83
+ withPlaywright: args.includes("--with-playwright"),
84
+ yes: args.includes("--yes") || args.includes("-y"),
85
+ dryRun: args.includes("--dry-run"),
86
+ help: args.includes("--help") || args.includes("-h"),
87
+ nonInteractive: false, // computed below
170
88
  };
171
89
  }
172
90
 
173
- // ─── MCP Server definitions ────────────────────────────────────────────────
174
- const MCP_SERVERS = [
175
- {
176
- key: 'context7',
177
- label: 'Context7',
178
- package: '@upstash/context7-mcp',
179
- desc: 'Context management — up-to-date library docs for Claude',
180
- config: {
181
- command: 'npx',
182
- args: ['-y', '@upstash/context7-mcp'],
183
- },
184
- },
185
- {
186
- key: 'playwright',
187
- label: 'Playwright MCP',
188
- package: '@playwright/mcp',
189
- desc: 'E2E browser automation — required for /e2e command',
190
- config: {
191
- command: 'npx',
192
- args: ['-y', '@playwright/mcp'],
193
- },
194
- },
195
- {
196
- key: 'sequential-thinking',
197
- label: 'Sequential Thinking',
198
- package: '@modelcontextprotocol/server-sequential-thinking',
199
- desc: 'Complex planning and multi-step reasoning',
200
- config: {
201
- command: 'npx',
202
- args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
203
- },
204
- },
205
- {
206
- key: 'filesystem',
207
- label: 'Filesystem',
208
- package: '@modelcontextprotocol/server-filesystem',
209
- desc: 'File operations (read/write/search)',
210
- config: {
211
- command: 'npx',
212
- args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
213
- },
214
- },
215
- ];
91
+ // ─── MCP server install helpers ───────────────────────────────────────────
216
92
 
217
- // ─── Check if package is available via npx (quick dry-run) ────────────────
218
93
  function checkPackageAvailable(pkg) {
219
94
  try {
220
- // Try `npm view` — fast, no install, works offline cache check
221
- const result = spawnSync('npm', ['view', pkg, 'version'], {
95
+ const result = spawnSync("npm", ["view", pkg, "version"], {
222
96
  timeout: 8000,
223
- stdio: 'pipe',
224
- encoding: 'utf8',
97
+ stdio: "pipe",
98
+ encoding: "utf8",
225
99
  });
226
100
  return result.status === 0 && result.stdout.trim().length > 0;
227
101
  } catch (_) {
@@ -229,64 +103,63 @@ function checkPackageAvailable(pkg) {
229
103
  }
230
104
  }
231
105
 
232
- // ─── Install / verify MCP servers ─────────────────────────────────────────
233
- function installMcpServers(verbose = true) {
106
+ function installMcpServers(selectedKeys) {
234
107
  const results = [];
108
+ const selected = MCP_SERVERS.filter((s) => selectedKeys.includes(s.key));
235
109
 
236
- for (const server of MCP_SERVERS) {
237
- if (verbose) process.stdout.write(` ${dim(server.label + '...')} `);
238
-
110
+ for (const server of selected) {
239
111
  try {
240
112
  const available = checkPackageAvailable(server.package);
241
113
  if (available) {
242
- if (verbose) console.log(green('✓') + dim(` ${server.package}`));
243
- results.push({ ...server, ok: true, note: 'available via npx' });
114
+ results.push({ ...server, ok: true, note: "available via npx" });
244
115
  } else {
245
- // Try npm install -g as fallback
246
- const install = spawnSync('npm', ['install', '-g', server.package], {
116
+ const install = spawnSync("npm", ["install", "-g", server.package], {
247
117
  timeout: 60000,
248
- stdio: 'pipe',
249
- encoding: 'utf8',
118
+ stdio: "pipe",
119
+ encoding: "utf8",
250
120
  });
251
121
  if (install.status === 0) {
252
- if (verbose) console.log(green('✓') + dim(' installed globally'));
253
- results.push({ ...server, ok: true, note: 'installed globally' });
122
+ results.push({ ...server, ok: true, note: "installed globally" });
254
123
  } else {
255
- if (verbose) console.log(yellow('⚠') + dim(' npm check failed — will use npx at runtime'));
256
- results.push({ ...server, ok: true, note: 'npx at runtime (not pre-installed)' });
124
+ results.push({
125
+ ...server,
126
+ ok: true,
127
+ note: "npx at runtime (not pre-installed)",
128
+ });
257
129
  }
258
130
  }
259
131
  } catch (err) {
260
- if (verbose) console.log(red('✗') + ` ${err.message}`);
261
132
  results.push({ ...server, ok: false, error: err.message });
262
133
  }
263
134
  }
264
-
265
135
  return results;
266
136
  }
267
137
 
268
- // ─── Add MCP servers to settings.json ─────────────────────────────────────
269
- function addMcpToSettings(settingsPath, mcpResults) {
138
+ function addMcpToSettings(settingsPath, mcpResults, targetDir) {
270
139
  let settings = {};
271
140
  if (fs.existsSync(settingsPath)) {
272
141
  try {
273
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
142
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
274
143
  } catch (_) {}
275
144
  }
276
145
 
277
146
  if (!settings.mcpServers) settings.mcpServers = {};
278
147
 
279
- // Add each server that didn't hard-fail
280
148
  for (const result of mcpResults) {
281
149
  if (!result.ok) continue;
150
+ const config = result.configFn ? result.configFn(targetDir) : result.config;
282
151
  if (!settings.mcpServers[result.key]) {
283
- settings.mcpServers[result.key] = result.config;
152
+ settings.mcpServers[result.key] = config;
284
153
  }
285
154
  }
286
155
 
287
- // Also ensure mcp__playwright and mcp__sequential-thinking are in permissions.allow
288
156
  if (settings.permissions && Array.isArray(settings.permissions.allow)) {
289
- const toAdd = ['mcp__playwright', 'mcp__sequential-thinking', 'mcp__context7', 'mcp__filesystem'];
157
+ const toAdd = [
158
+ "mcp__playwright",
159
+ "mcp__sequential-thinking",
160
+ "mcp__context7",
161
+ "mcp__filesystem",
162
+ ];
290
163
  for (const perm of toAdd) {
291
164
  if (!settings.permissions.allow.includes(perm)) {
292
165
  settings.permissions.allow.push(perm);
@@ -294,47 +167,40 @@ function addMcpToSettings(settingsPath, mcpResults) {
294
167
  }
295
168
  }
296
169
 
297
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
170
+ fs.writeFileSync(
171
+ settingsPath,
172
+ JSON.stringify(settings, null, 2) + "\n",
173
+ "utf8",
174
+ );
298
175
  }
299
176
 
300
- // ─── Install Playwright browsers ──────────────────────────────────────────
301
- function installPlaywrightBrowsers(verbose = true) {
302
- if (verbose) process.stdout.write(` ${dim('Installing Playwright browsers...')} `);
177
+ // ─── Playwright install helpers ───────────────────────────────────────────
178
+
179
+ function installPlaywrightBrowsers() {
303
180
  try {
304
- const result = spawnSync('npx', ['playwright', 'install', '--with-deps', 'chromium'], {
181
+ const result = spawnSync(
182
+ "npx",
183
+ ["playwright", "install", "--with-deps", "chromium"],
184
+ { timeout: 120000, stdio: "pipe", encoding: "utf8" },
185
+ );
186
+ if (result.status === 0) return { ok: true };
187
+ const result2 = spawnSync("npx", ["playwright", "install", "chromium"], {
305
188
  timeout: 120000,
306
- stdio: 'pipe',
307
- encoding: 'utf8',
189
+ stdio: "pipe",
190
+ encoding: "utf8",
308
191
  });
309
- if (result.status === 0) {
310
- if (verbose) console.log(green('✓'));
311
- return { ok: true };
312
- } else {
313
- // Try without --with-deps (CI environments)
314
- const result2 = spawnSync('npx', ['playwright', 'install', 'chromium'], {
315
- timeout: 120000,
316
- stdio: 'pipe',
317
- encoding: 'utf8',
318
- });
319
- if (result2.status === 0) {
320
- if (verbose) console.log(green('✓') + dim(' (chromium only)'));
321
- return { ok: true };
322
- }
323
- if (verbose) console.log(yellow('⚠') + dim(' browser install failed — run: npx playwright install'));
324
- return { ok: false, error: result.stderr };
325
- }
192
+ if (result2.status === 0) return { ok: true };
193
+ return { ok: false, error: result.stderr };
326
194
  } catch (err) {
327
- if (verbose) console.log(yellow('⚠') + dim(` ${err.message}`));
328
195
  return { ok: false, error: err.message };
329
196
  }
330
197
  }
331
198
 
332
- // ─── Create playwright.config.ts if missing ───────────────────────────────
333
199
  function ensurePlaywrightConfig(targetDir) {
334
- const tsConfig = path.join(targetDir, 'playwright.config.ts');
335
- const jsConfig = path.join(targetDir, 'playwright.config.js');
200
+ const tsConfig = path.join(targetDir, "playwright.config.ts");
201
+ const jsConfig = path.join(targetDir, "playwright.config.js");
336
202
  if (fs.existsSync(tsConfig) || fs.existsSync(jsConfig)) {
337
- return { status: 'skipped', dest: tsConfig };
203
+ return { status: "skipped", dest: tsConfig };
338
204
  }
339
205
  const content = `import { defineConfig, devices } from '@playwright/test';
340
206
 
@@ -358,307 +224,488 @@ export default defineConfig({
358
224
  });
359
225
  `;
360
226
  ensureDir(path.dirname(tsConfig));
361
- fs.writeFileSync(tsConfig, content, 'utf8');
362
- return { status: 'created', dest: tsConfig };
227
+ fs.writeFileSync(tsConfig, content, "utf8");
228
+ return { status: "created", dest: tsConfig };
363
229
  }
364
230
 
365
- // ─── Verify ralph-loop command ────────────────────────────────────────────
366
- function verifyRalphLoop(commandsDir) {
367
- const ralphPath = path.join(commandsDir, 'ralph-loop.md');
368
- return fs.existsSync(ralphPath);
369
- }
231
+ // ─── Core install: copy commands, templates, stacks ───────────────────────
232
+
233
+ function installBaseFiles(targetDir, repoRoot, isGlobal) {
234
+ const claudeDir = isGlobal ? targetDir : path.join(targetDir, ".claude");
235
+ const commandsDir = path.join(claudeDir, "commands");
236
+ const steps = [];
370
237
 
371
- // ─── Install logic ─────────────────────────────────────────────────────────
372
- async function install(opts) {
373
- const { targetDir, repoRoot, isGlobal, runtime } = opts;
238
+ // 1. Commands
239
+ const srcCommands = path.join(repoRoot, "system", "commands");
240
+ steps.push(...copyDir(srcCommands, commandsDir, { skipExisting: false }));
241
+
242
+ // 2. AUTONOMOUS-WORKFLOW.md
243
+ const awSrc = path.join(
244
+ repoRoot,
245
+ "system",
246
+ "templates",
247
+ "AUTONOMOUS-WORKFLOW.md",
248
+ );
249
+ const awDest = isGlobal
250
+ ? path.join(os.homedir(), ".effectum", "AUTONOMOUS-WORKFLOW.md")
251
+ : path.join(targetDir, "AUTONOMOUS-WORKFLOW.md");
252
+ steps.push(copyFile(awSrc, awDest, { skipExisting: false }));
374
253
 
375
- // For global installs, .claude/ is the target dir itself
376
- const claudeDir = isGlobal ? targetDir : path.join(targetDir, '.claude');
377
- const commandsDir = path.join(claudeDir, 'commands');
254
+ // 3. Workshop
255
+ const workshopSrc = path.join(repoRoot, "workshop");
256
+ const workshopDest = isGlobal
257
+ ? path.join(os.homedir(), ".effectum", "workshop")
258
+ : path.join(targetDir, "workshop");
259
+ steps.push(...copyDir(workshopSrc, workshopDest, { skipExisting: true }));
260
+
261
+ // 4. Copy templates + stacks so reconfigure can find them later
262
+ const templatesSrc = path.join(repoRoot, "system", "templates");
263
+ const stacksSrc = path.join(repoRoot, "system", "stacks");
264
+ const effectumDir = isGlobal
265
+ ? path.join(os.homedir(), ".effectum")
266
+ : path.join(targetDir, ".effectum");
267
+ steps.push(
268
+ ...copyDir(templatesSrc, path.join(effectumDir, "templates"), {
269
+ skipExisting: false,
270
+ }),
271
+ );
272
+ steps.push(
273
+ ...copyDir(stacksSrc, path.join(effectumDir, "stacks"), {
274
+ skipExisting: false,
275
+ }),
276
+ );
378
277
 
278
+ return steps;
279
+ }
280
+
281
+ // ─── Generate configured files (CLAUDE.md, settings.json, guardrails.md) ─
282
+
283
+ function generateConfiguredFiles(config, targetDir, repoRoot, isGlobal) {
284
+ const claudeDir = isGlobal ? targetDir : path.join(targetDir, ".claude");
379
285
  const steps = [];
380
286
 
381
- // 1. system/commands/*.md → .claude/commands/ (always overwrite)
382
- const srcCommands = path.join(repoRoot, 'system', 'commands');
383
- const cmdResults = copyDir(srcCommands, commandsDir, { skipExisting: false });
384
- steps.push(...cmdResults);
287
+ // Load stack preset
288
+ const stackSections = loadStackPreset(config.stack, targetDir, repoRoot);
289
+ const vars = buildSubstitutionMap(config, stackSections);
290
+
291
+ // 1. CLAUDE.md
292
+ const claudeMdTmpl = findTemplatePath("CLAUDE.md.tmpl", targetDir, repoRoot);
293
+ const { content: claudeMdContent, remaining: claudeMdRemaining } =
294
+ renderTemplate(claudeMdTmpl, vars);
295
+ const claudeMdDest = isGlobal
296
+ ? path.join(targetDir, "CLAUDE.md")
297
+ : path.join(targetDir, "CLAUDE.md");
298
+ ensureDir(path.dirname(claudeMdDest));
299
+ fs.writeFileSync(claudeMdDest, claudeMdContent, "utf8");
300
+ steps.push({ status: "created", dest: claudeMdDest });
301
+
302
+ if (claudeMdRemaining.length > 0) {
303
+ p.log.warn(
304
+ `CLAUDE.md has remaining placeholders: ${claudeMdRemaining.join(", ")}`,
305
+ );
306
+ }
385
307
 
386
- // 2. AUTONOMOUS-WORKFLOW.md target/
387
- const awSrc = path.join(repoRoot, 'system', 'templates', 'AUTONOMOUS-WORKFLOW.md');
388
- const awDest = isGlobal
389
- ? path.join(os.homedir(), '.effectum', 'AUTONOMOUS-WORKFLOW.md')
390
- : path.join(targetDir, 'AUTONOMOUS-WORKFLOW.md');
391
- steps.push(copyFile(awSrc, awDest, { skipExisting: false }));
308
+ // 2. settings.json build from template with autonomy level applied
309
+ const settingsTmpl = findTemplatePath(
310
+ "settings.json.tmpl",
311
+ targetDir,
312
+ repoRoot,
313
+ );
314
+ let settingsObj;
315
+ try {
316
+ settingsObj = JSON.parse(fs.readFileSync(settingsTmpl, "utf8"));
317
+ } catch (e) {
318
+ throw new Error(`Could not parse settings template: ${e.message}`);
319
+ }
392
320
 
393
- // 3. settings.json — ALWAYS merge (template wins, existing keys preserved)
394
- const settingsSrc = path.join(repoRoot, 'system', 'templates', 'settings.json.tmpl');
395
- const settingsDest = path.join(claudeDir, 'settings.json');
396
- steps.push(mergeSettings(settingsSrc, settingsDest));
321
+ // Apply autonomy level
322
+ const autonomy = AUTONOMY_MAP[config.autonomyLevel] || AUTONOMY_MAP.standard;
323
+ settingsObj.permissions = {
324
+ ...settingsObj.permissions,
325
+ ...autonomy.permissions,
326
+ defaultMode: autonomy.defaultMode,
327
+ deny: settingsObj.permissions?.deny || [],
328
+ };
397
329
 
398
- // 4. guardrails.md ALWAYS overwrite
399
- const guardrailsSrc = path.join(repoRoot, 'system', 'templates', 'guardrails.md.tmpl');
400
- const guardrailsDest = path.join(claudeDir, 'guardrails.md');
401
- steps.push(copyFile(guardrailsSrc, guardrailsDest, { skipExisting: false }));
330
+ // Apply formatter in PostToolUse hook
331
+ const formatter = FORMATTER_MAP[config.stack] || FORMATTER_MAP.generic;
332
+ if (settingsObj.hooks?.PostToolUse) {
333
+ for (const group of settingsObj.hooks.PostToolUse) {
334
+ if (group.matcher === "Edit|Write") {
335
+ for (const hook of group.hooks) {
336
+ if (
337
+ hook.command &&
338
+ hook.command.includes("formatter-not-configured")
339
+ ) {
340
+ if (formatter.command === "echo no-formatter-configured") {
341
+ hook.command = "echo no-formatter-configured";
342
+ } else {
343
+ hook.command = `bash -c 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); if [[ "$FILE" =~ \\.(${formatter.glob})$ ]]; then ${formatter.command} "$FILE" 2>/dev/null; fi; exit 0'`;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
402
350
 
403
- // 5. workshop/ copy for BOTH local and global
404
- const workshopSrc = path.join(repoRoot, 'workshop');
405
- const workshopDest = isGlobal
406
- ? path.join(os.homedir(), '.effectum', 'workshop')
407
- : path.join(targetDir, 'workshop');
408
- const wResults = copyDir(workshopSrc, workshopDest, { skipExisting: true });
409
- steps.push(...wResults);
410
-
411
- // 6. Copy templates + stacks so /setup can find them after npx install
412
- const templatesSrc = path.join(repoRoot, 'system', 'templates');
413
- const stacksSrc = path.join(repoRoot, 'system', 'stacks');
414
- const effectumDir = isGlobal
415
- ? path.join(os.homedir(), '.effectum')
416
- : path.join(targetDir, '.effectum');
417
- const tResults = copyDir(templatesSrc, path.join(effectumDir, 'templates'), { skipExisting: false });
418
- const sResults = copyDir(stacksSrc, path.join(effectumDir, 'stacks'), { skipExisting: false });
419
- steps.push(...tResults, ...sResults);
351
+ // Merge with existing settings if present
352
+ const settingsDest = path.join(claudeDir, "settings.json");
353
+ let existing = {};
354
+ if (fs.existsSync(settingsDest)) {
355
+ try {
356
+ existing = JSON.parse(fs.readFileSync(settingsDest, "utf8"));
357
+ } catch (_) {}
358
+ }
359
+ const merged = deepMerge(existing, settingsObj);
360
+ ensureDir(path.dirname(settingsDest));
361
+ fs.writeFileSync(
362
+ settingsDest,
363
+ JSON.stringify(merged, null, 2) + "\n",
364
+ "utf8",
365
+ );
366
+ steps.push({ status: "created", dest: settingsDest });
367
+
368
+ // 3. guardrails.md — substitute stack-specific sections
369
+ const guardrailsTmpl = findTemplatePath(
370
+ "guardrails.md.tmpl",
371
+ targetDir,
372
+ repoRoot,
373
+ );
374
+ const guardrailsRaw = fs.readFileSync(guardrailsTmpl, "utf8");
375
+ let guardrailsContent = guardrailsRaw;
376
+
377
+ // Replace "No stack-specific guardrails..." with actual content
378
+ if (stackSections.STACK_SPECIFIC_GUARDRAILS) {
379
+ guardrailsContent = guardrailsContent.replace(
380
+ /No stack-specific guardrails configured yet\. Run \/setup to configure for your stack\./,
381
+ stackSections.STACK_SPECIFIC_GUARDRAILS,
382
+ );
383
+ }
384
+ if (stackSections.TOOL_SPECIFIC_GUARDRAILS) {
385
+ guardrailsContent = guardrailsContent.replace(
386
+ /No tool-specific guardrails configured yet\. Run \/setup to configure\./,
387
+ stackSections.TOOL_SPECIFIC_GUARDRAILS,
388
+ );
389
+ }
390
+
391
+ const guardrailsDest = path.join(claudeDir, "guardrails.md");
392
+ ensureDir(path.dirname(guardrailsDest));
393
+ fs.writeFileSync(guardrailsDest, guardrailsContent, "utf8");
394
+ steps.push({ status: "created", dest: guardrailsDest });
420
395
 
421
396
  return steps;
422
397
  }
423
398
 
424
- // ─── Status icon ──────────────────────────────────────────────────────────
425
- function statusIcon(status) {
426
- switch (status) {
427
- case 'created': return green('✓');
428
- case 'skipped': return dim('─');
429
- case 'error': return red('✗');
430
- default: return dim('·');
431
- }
399
+ // ─── Smart defaults for non-interactive mode ──────────────────────────────
400
+
401
+ function buildSmartDefaults(targetDir) {
402
+ const detected = detectAll(targetDir);
403
+ const formatter =
404
+ FORMATTER_MAP[detected.stack || "generic"] || FORMATTER_MAP.generic;
405
+ return {
406
+ projectName: detected.projectName,
407
+ stack: detected.stack || "generic",
408
+ language: "english",
409
+ autonomyLevel: "standard",
410
+ packageManager: detected.packageManager,
411
+ formatter: formatter.name,
412
+ mcpServers: MCP_SERVERS.map((s) => s.key),
413
+ playwrightBrowsers: true,
414
+ installScope: "local",
415
+ };
416
+ }
417
+
418
+ // ─── Git branch creation ──────────────────────────────────────────────────
419
+
420
+ function createGitBranch(name) {
421
+ const result = spawnSync("git", ["checkout", "-b", name], {
422
+ stdio: "pipe",
423
+ encoding: "utf8",
424
+ });
425
+ return result.status === 0;
432
426
  }
433
427
 
434
- // ─── Main ──────────────────────────────────────────────────────────────────
428
+ // ─── Main ─────────────────────────────────────────────────────────────────
429
+
435
430
  async function main() {
436
- const args = parseArgs(process.argv);
431
+ const args = parseArgs(process.argv);
437
432
  const repoRoot = findRepoRoot();
438
433
 
434
+ // Help
439
435
  if (args.help) {
440
436
  console.log(`
441
- ${bold('effectum')} — autonomous development system for Claude Code
442
-
443
- ${bold('Usage:')}
444
- npx effectum Interactive installer
445
- npx effectum --global Install to ~/.claude/ (no prompts)
446
- npx effectum --local Install to ./.claude/ (no prompts)
447
- npx effectum --global --claude Non-interactive, Claude Code runtime
448
- npx effectum --global --with-mcp Include MCP server setup
449
- npx effectum --global --with-playwright Include Playwright browser install
450
- npx effectum --global --claude --with-mcp --with-playwright Full install
451
-
452
- ${bold('Options:')}
437
+ effectum — autonomous development system for Claude Code
438
+
439
+ Usage:
440
+ npx effectum Interactive installer
441
+ npx effectum init Per-project init (after global install)
442
+ npx effectum reconfigure Re-apply config from .effectum.json
443
+ npx effectum --global Install globally (~/.claude/, no prompts)
444
+ npx effectum --local Install locally (./.claude/, no prompts)
445
+ npx effectum --dry-run Show planned files without writing
446
+ npx effectum --yes Non-interactive with smart defaults
447
+
448
+ Options:
453
449
  --global, -g Install globally for all projects (~/.claude/)
454
450
  --local, -l Install locally for this project (./.claude/)
455
451
  --claude Select Claude Code runtime (default)
456
- --with-mcp Install MCP servers (Context7, Playwright, Sequential Thinking, Filesystem)
452
+ --with-mcp Install MCP servers
457
453
  --with-playwright Install Playwright browsers
458
- --yes, -y Skip confirmation prompts
454
+ --yes, -y Skip interactive prompts, use smart defaults
455
+ --dry-run Show what would be created without writing
459
456
  --help, -h Show this help
460
457
  `);
461
458
  process.exit(0);
462
459
  }
463
460
 
464
- printBanner();
465
-
466
- // ── Check repo files exist ───────────────────────────────────────────────
467
- if (!fs.existsSync(path.join(repoRoot, 'system', 'commands'))) {
468
- console.log(red('✗ Error:') + ' Could not find Effectum system files.');
469
- console.log(dim(' Expected: ' + path.join(repoRoot, 'system', 'commands')));
470
- console.log(dim(' This is a bug — please report it at https://github.com/aslomon/effectum/issues'));
461
+ // Check repo files exist
462
+ if (!fs.existsSync(path.join(repoRoot, "system", "commands"))) {
463
+ console.error("Error: Could not find Effectum system files.");
464
+ console.error(" Expected: " + path.join(repoRoot, "system", "commands"));
471
465
  process.exit(1);
472
466
  }
473
467
 
474
- let isGlobal;
475
- let runtime = 'claude';
476
- let wantMcp = args.withMcp;
477
- let wantPlaywright = args.withPlaywright;
478
-
479
- // ── Non-interactive mode ─────────────────────────────────────────────────
480
- if (args.global || args.local) {
481
- isGlobal = args.global;
482
- if (args.claude) runtime = 'claude';
483
- // wantMcp / wantPlaywright already set from flags
484
- } else {
485
- // ── Interactive mode ───────────────────────────────────────────────────
486
- const rl = createRL();
487
-
488
- try {
489
- // Scope question
490
- const scopeIdx = await askChoice(rl,
491
- bold('Where do you want to install Effectum?'),
492
- [
493
- { label: 'Global', desc: 'all projects (~/.claude/)' },
494
- { label: 'Local', desc: 'this project only (./.claude/)' },
495
- ],
496
- 0
497
- );
498
- isGlobal = scopeIdx === 0;
499
- console.log();
500
-
501
- // Runtime question
502
- const runtimeIdx = await askChoice(rl,
503
- bold('Which AI coding runtime?'),
504
- [
505
- { label: 'Claude Code', desc: 'default — recommended' },
506
- { label: 'Codex / Gemini / OpenCode', desc: 'coming soon' },
507
- ],
508
- 0
509
- );
510
- runtime = runtimeIdx === 0 ? 'claude' : 'other';
511
- console.log();
512
-
513
- if (runtime === 'other') {
514
- console.log(yellow('⚠') + ' Only Claude Code is fully supported right now.');
515
- console.log(dim(' Other runtimes are on the roadmap. Proceeding with Claude Code configuration.'));
516
- runtime = 'claude';
517
- console.log();
518
- }
468
+ // Determine mode
469
+ const isNonInteractive =
470
+ args.yes ||
471
+ args.global ||
472
+ args.local ||
473
+ process.env.CI === "true" ||
474
+ !process.stdin.isTTY;
475
+
476
+ const isGlobal = args.global;
477
+ const homeClaudeDir = path.join(os.homedir(), ".claude");
478
+ const targetDir = isGlobal ? homeClaudeDir : process.cwd();
479
+
480
+ // ── Non-interactive mode ────────────────────────────────────────────────
481
+ if (isNonInteractive) {
482
+ const config = buildSmartDefaults(targetDir);
483
+ config.installScope = isGlobal ? "global" : "local";
484
+
485
+ if (args.dryRun) {
486
+ console.log("\n Dry run — no files will be written.\n");
487
+ console.log(" Config:", JSON.stringify(config, null, 2));
488
+ console.log("\n Files that would be created:");
489
+ console.log(" .claude/commands/*.md");
490
+ console.log(" .claude/settings.json");
491
+ console.log(" .claude/guardrails.md");
492
+ console.log(" CLAUDE.md");
493
+ console.log(" AUTONOMOUS-WORKFLOW.md");
494
+ console.log(" .effectum.json");
495
+ if (args.withMcp) console.log(" MCP servers in settings.json");
496
+ process.exit(0);
497
+ }
519
498
 
520
- // MCP question
521
- wantMcp = await confirm(rl,
522
- bold('Install MCP servers?') + dim(' (Context7, Playwright MCP, Sequential Thinking, Filesystem)'),
523
- true
524
- );
525
- console.log();
526
-
527
- // Playwright question
528
- if (wantMcp) {
529
- wantPlaywright = await confirm(rl,
530
- bold('Install Playwright browsers?') + dim(' (required for /e2e command)'),
531
- true
532
- );
533
- console.log();
534
- }
499
+ // Install base files
500
+ installBaseFiles(targetDir, repoRoot, isGlobal);
535
501
 
536
- } finally {
537
- rl.close();
502
+ // Generate configured files (only for local installs)
503
+ if (!isGlobal) {
504
+ generateConfiguredFiles(config, targetDir, repoRoot, isGlobal);
505
+ writeConfig(targetDir, config);
538
506
  }
539
- }
540
507
 
541
- // ── Determine target directory ───────────────────────────────────────────
542
- // For global: target is ~/.claude/ (so claudeDir = ~/.claude/)
543
- // For local: target is ./ (so claudeDir = ./.claude/)
544
- const homeClaudeDir = path.join(os.homedir(), '.claude');
545
- const targetDir = isGlobal ? homeClaudeDir : process.cwd();
546
- const displayTarget = isGlobal ? '~/.claude' : './.claude';
547
-
548
- console.log(` ${dim('Scope:')} ${cyan(isGlobal ? 'Global' : 'Local')}`);
549
- console.log(` ${dim('Target:')} ${cyan(displayTarget)}`);
550
- console.log(` ${dim('Runtime:')} ${cyan('Claude Code')}`);
551
- console.log();
552
-
553
- // ── Step 1: Workflow commands + files ────────────────────────────────────
554
- console.log(bold(' 1. Installing workflow commands...'));
555
- let steps;
556
- try {
557
- steps = await install({ targetDir, repoRoot, isGlobal, runtime });
558
- } catch (err) {
559
- console.log(red(' ✗ Installation failed:') + ' ' + err.message);
560
- process.exit(1);
561
- }
508
+ // MCP servers
509
+ if (args.withMcp) {
510
+ const mcpResults = installMcpServers(config.mcpServers);
511
+ const settingsPath = isGlobal
512
+ ? path.join(homeClaudeDir, "settings.json")
513
+ : path.join(targetDir, ".claude", "settings.json");
514
+ addMcpToSettings(settingsPath, mcpResults, targetDir);
515
+ }
562
516
 
563
- // Print file results
564
- for (const step of steps) {
565
- if (!step || !step.dest) continue;
566
- const homeDir = os.homedir();
567
- const rel = step.dest.startsWith(homeDir)
568
- ? '~/' + path.relative(homeDir, step.dest)
569
- : path.relative(process.cwd(), step.dest);
570
- const icon = statusIcon(step.status);
571
- if (step.status === 'error') {
572
- console.log(` ${icon} ${red(rel)} — ${step.error || ''}`);
573
- } else {
574
- console.log(` ${icon} ${step.status === 'skipped' ? dim(rel) : rel}`);
517
+ // Playwright
518
+ if (args.withPlaywright) {
519
+ installPlaywrightBrowsers();
520
+ if (!isGlobal) ensurePlaywrightConfig(process.cwd());
575
521
  }
522
+
523
+ console.log("\n Effectum installed successfully.\n");
524
+ process.exit(0);
576
525
  }
577
526
 
578
- // Verify ralph-loop
579
- const settingsPath = isGlobal
580
- ? path.join(homeClaudeDir, 'settings.json')
581
- : path.join(targetDir, '.claude', 'settings.json');
527
+ // ── Interactive mode ────────────────────────────────────────────────────
528
+ printBanner();
582
529
 
583
- const commandsInstallDir = isGlobal
584
- ? path.join(homeClaudeDir, 'commands')
585
- : path.join(targetDir, '.claude', 'commands');
530
+ const detected = detectAll(process.cwd());
586
531
 
587
- if (verifyRalphLoop(commandsInstallDir)) {
588
- console.log(` ${green('✓')} ralph-loop command ${dim('verified')}`);
589
- } else {
590
- console.log(` ${yellow('⚠')} ralph-loop command not found`);
532
+ if (detected.stack) {
533
+ p.log.info(
534
+ `Detected: ${detected.stack} project (${detected.packageManager})`,
535
+ );
591
536
  }
592
537
 
593
- console.log(` ${green('✅')} Done`);
594
- console.log();
595
-
596
- // ── Step 2: MCP servers ──────────────────────────────────────────────────
597
- if (wantMcp) {
598
- console.log(bold(' 2. Installing MCP servers...'));
599
- const mcpResults = installMcpServers(true);
538
+ // Scope
539
+ const scopeValue = await p.select({
540
+ message: "Install scope",
541
+ options: [
542
+ {
543
+ value: "local",
544
+ label: "Local",
545
+ hint: "This project only (./.claude/)",
546
+ },
547
+ {
548
+ value: "global",
549
+ label: "Global",
550
+ hint: "All projects (~/.claude/)",
551
+ },
552
+ ],
553
+ initialValue: "local",
554
+ });
555
+ if (p.isCancel(scopeValue)) {
556
+ p.cancel("Setup cancelled.");
557
+ process.exit(0);
558
+ }
559
+ const installGlobal = scopeValue === "global";
560
+ const installTargetDir = installGlobal ? homeClaudeDir : process.cwd();
561
+
562
+ // Project name
563
+ const projectName = await askProjectName(detected.projectName);
564
+
565
+ // Stack
566
+ const stack = await askStack(detected.stack);
567
+
568
+ // Language
569
+ const langResult = await askLanguage();
570
+
571
+ // Autonomy
572
+ const autonomyLevel = await askAutonomy();
573
+
574
+ // MCP servers
575
+ const mcpServerKeys = await askMcpServers();
576
+
577
+ // Playwright
578
+ const wantPlaywright = mcpServerKeys.includes("playwright")
579
+ ? await askPlaywright()
580
+ : false;
581
+
582
+ // Git branch
583
+ const gitBranch = await askGitBranch();
584
+
585
+ // Build config object
586
+ const formatter = FORMATTER_MAP[stack] || FORMATTER_MAP.generic;
587
+ const config = {
588
+ projectName,
589
+ stack,
590
+ language: langResult.language,
591
+ ...(langResult.customLanguage
592
+ ? { customLanguage: langResult.customLanguage }
593
+ : {}),
594
+ autonomyLevel,
595
+ packageManager: detected.packageManager,
596
+ formatter: formatter.name,
597
+ mcpServers: mcpServerKeys,
598
+ playwrightBrowsers: wantPlaywright,
599
+ installScope: installGlobal ? "global" : "local",
600
+ };
600
601
 
601
- // Inject into settings.json
602
- try {
603
- addMcpToSettings(settingsPath, mcpResults);
604
- console.log(` ${green('✓')} MCP servers added to settings.json`);
605
- } catch (err) {
606
- console.log(` ${red('✗')} Could not update settings.json: ${err.message}`);
602
+ // ── Dry run ─────────────────────────────────────────────────────────────
603
+ if (args.dryRun) {
604
+ p.log.info("Dry run — no files will be written.");
605
+ p.note(JSON.stringify(config, null, 2), "Planned Configuration");
606
+ const plannedFiles = [
607
+ ".claude/commands/*.md",
608
+ ".claude/settings.json",
609
+ ".claude/guardrails.md",
610
+ "CLAUDE.md",
611
+ "AUTONOMOUS-WORKFLOW.md",
612
+ ".effectum.json",
613
+ ];
614
+ if (mcpServerKeys.length > 0) {
615
+ plannedFiles.push("MCP servers in settings.json");
607
616
  }
617
+ p.note(plannedFiles.join("\n"), "Files to be created/updated");
618
+ p.outro("Dry run complete. No changes made.");
619
+ process.exit(0);
620
+ }
608
621
 
609
- console.log(` ${green('✅')} Done`);
610
- console.log();
622
+ // ── Create git branch ──────────────────────────────────────────────────
623
+ if (gitBranch.create) {
624
+ const s = p.spinner();
625
+ s.start("Creating git branch...");
626
+ const ok = createGitBranch(gitBranch.name);
627
+ if (ok) {
628
+ s.stop(`Branch "${gitBranch.name}" created`);
629
+ } else {
630
+ s.stop("Could not create branch (may already exist)");
631
+ }
611
632
  }
612
633
 
613
- // ── Step 3: Playwright browsers ──────────────────────────────────────────
614
- if (wantPlaywright) {
615
- console.log(bold(' 3. Setting up Playwright...'));
616
- installPlaywrightBrowsers(true);
634
+ // ── Step 1: Base files ─────────────────────────────────────────────────
635
+ const s1 = p.spinner();
636
+ s1.start("Installing workflow commands and templates...");
637
+ const baseSteps = installBaseFiles(installTargetDir, repoRoot, installGlobal);
638
+ s1.stop(
639
+ `Installed ${baseSteps.filter((s) => s.status === "created").length} files`,
640
+ );
641
+
642
+ // ── Step 2: Configure (local only) ────────────────────────────────────
643
+ const configSteps = [];
644
+ if (!installGlobal) {
645
+ const s2 = p.spinner();
646
+ s2.start(
647
+ "Generating configured files (CLAUDE.md, settings.json, guardrails.md)...",
648
+ );
649
+ const cSteps = generateConfiguredFiles(
650
+ config,
651
+ installTargetDir,
652
+ repoRoot,
653
+ installGlobal,
654
+ );
655
+ configSteps.push(...cSteps);
656
+ s2.stop("Configuration files generated");
657
+ }
617
658
 
618
- // Create playwright.config.ts in the current project (local installs only)
619
- if (!isGlobal) {
620
- const pcResult = ensurePlaywrightConfig(process.cwd());
621
- const icon = statusIcon(pcResult.status);
622
- const rel = path.relative(process.cwd(), pcResult.dest);
623
- console.log(` ${icon} ${pcResult.status === 'skipped' ? dim(rel) : rel}`);
624
- }
659
+ // ── Step 3: MCP servers ────────────────────────────────────────────────
660
+ if (mcpServerKeys.length > 0) {
661
+ const s3 = p.spinner();
662
+ s3.start("Setting up MCP servers...");
663
+ const mcpResults = installMcpServers(mcpServerKeys);
664
+ const settingsPath = installGlobal
665
+ ? path.join(homeClaudeDir, "settings.json")
666
+ : path.join(installTargetDir, ".claude", "settings.json");
667
+ addMcpToSettings(settingsPath, mcpResults, installTargetDir);
668
+ const okCount = mcpResults.filter((r) => r.ok).length;
669
+ s3.stop(`${okCount} MCP servers configured`);
670
+ }
625
671
 
626
- console.log(` ${green('✅')} Done`);
627
- console.log();
672
+ // ── Step 4: Playwright ─────────────────────────────────────────────────
673
+ if (wantPlaywright) {
674
+ const s4 = p.spinner();
675
+ s4.start("Installing Playwright browsers...");
676
+ const pwResult = installPlaywrightBrowsers();
677
+ if (!installGlobal) ensurePlaywrightConfig(process.cwd());
678
+ s4.stop(
679
+ pwResult.ok
680
+ ? "Playwright browsers installed"
681
+ : "Playwright install failed (run manually: npx playwright install)",
682
+ );
628
683
  }
629
684
 
630
- // ── Step 4: Summary ──────────────────────────────────────────────────────
631
- const createdCount = steps.filter(s => s && s.status === 'created').length;
632
- const skippedCount = steps.filter(s => s && s.status === 'skipped').length;
633
-
634
- console.log(green('⚡') + bold(' Effectum ready!'));
635
- console.log();
636
- console.log(` ${dim('Files installed:')} ${createdCount}`);
637
- if (skippedCount) console.log(` ${dim('Already existed:')} ${skippedCount} ${dim('(preserved)')}`);
638
- if (wantMcp) console.log(` ${dim('MCP servers:')} ${MCP_SERVERS.length} configured`);
639
- if (wantPlaywright) console.log(` ${dim('Playwright:')} browsers installed`);
640
- console.log();
641
-
642
- if (isGlobal) {
643
- console.log(' ' + bold('Next steps:'));
644
- console.log(` ${cyan('1.')} Open Claude Code in any project`);
645
- console.log(` ${cyan('2.')} Run ${bold('/setup ~/your-project')} to configure it`);
646
- console.log(` ${dim('↳ /setup substitutes placeholders in settings.json for your project')}`);
647
- console.log(` ${cyan('3.')} Write a spec with ${bold('/prd:new')}`);
648
- } else {
649
- console.log(' ' + bold('Next steps:'));
650
- console.log(` ${cyan('1.')} Open Claude Code here: ${dim('claude')}`);
651
- console.log(` ${cyan('2.')} Run ${bold('/setup .')} to configure this project`);
652
- console.log(` ${dim('↳ /setup substitutes placeholders in settings.json for your project')}`);
653
- console.log(` ${cyan('3.')} Write a spec with ${bold('/prd:new')}`);
685
+ // ── Step 5: Save config ────────────────────────────────────────────────
686
+ if (!installGlobal) {
687
+ const configPath = writeConfig(installTargetDir, config);
688
+ configSteps.push({ status: "created", dest: configPath });
654
689
  }
655
690
 
656
- console.log();
657
- console.log(dim(' Docs: https://github.com/aslomon/effectum'));
658
- console.log();
691
+ // ── Summary ─────────────────────────────────────────────────────────────
692
+ const allSteps = [...baseSteps, ...configSteps];
693
+ const allFiles = allSteps
694
+ .filter((s) => s && s.dest)
695
+ .map((s) => {
696
+ const homeDir = os.homedir();
697
+ return s.dest.startsWith(homeDir)
698
+ ? "~/" + path.relative(homeDir, s.dest)
699
+ : path.relative(process.cwd(), s.dest);
700
+ });
701
+
702
+ // Deduplicate for summary
703
+ const uniqueFiles = [...new Set(allFiles)].slice(0, 20);
704
+ showSummary(config, uniqueFiles);
705
+ showOutro(installGlobal);
659
706
  }
660
707
 
661
- main().catch(err => {
662
- console.error(red('Fatal error:'), err.message);
708
+ main().catch((err) => {
709
+ p.log.error(`Fatal error: ${err.message}`);
663
710
  process.exit(1);
664
711
  });