@aslomon/effectum 0.1.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.
Files changed (39) hide show
  1. package/README.md +633 -0
  2. package/bin/install.js +652 -0
  3. package/package.json +29 -0
  4. package/system/README.md +118 -0
  5. package/system/commands/build-fix.md +89 -0
  6. package/system/commands/cancel-ralph.md +90 -0
  7. package/system/commands/checkpoint.md +63 -0
  8. package/system/commands/code-review.md +120 -0
  9. package/system/commands/e2e.md +92 -0
  10. package/system/commands/plan.md +111 -0
  11. package/system/commands/ralph-loop.md +163 -0
  12. package/system/commands/refactor-clean.md +104 -0
  13. package/system/commands/tdd.md +84 -0
  14. package/system/commands/verify.md +71 -0
  15. package/system/stacks/generic.md +96 -0
  16. package/system/stacks/nextjs-supabase.md +114 -0
  17. package/system/stacks/python-fastapi.md +140 -0
  18. package/system/stacks/swift-ios.md +136 -0
  19. package/system/templates/AUTONOMOUS-WORKFLOW.md +1368 -0
  20. package/system/templates/CLAUDE.md.tmpl +141 -0
  21. package/system/templates/guardrails.md.tmpl +39 -0
  22. package/system/templates/settings.json.tmpl +201 -0
  23. package/workshop/knowledge/01-prd-template.md +275 -0
  24. package/workshop/knowledge/02-questioning-framework.md +209 -0
  25. package/workshop/knowledge/03-decomposition-guide.md +234 -0
  26. package/workshop/knowledge/04-examples.md +435 -0
  27. package/workshop/knowledge/05-quality-checklist.md +166 -0
  28. package/workshop/knowledge/06-network-map-guide.md +413 -0
  29. package/workshop/knowledge/07-prompt-templates.md +315 -0
  30. package/workshop/knowledge/08-workflow-modes.md +198 -0
  31. package/workshop/projects/_example-project/PROJECT.md +33 -0
  32. package/workshop/projects/_example-project/notes/decisions.md +15 -0
  33. package/workshop/projects/_example-project/notes/discovery-log.md +9 -0
  34. package/workshop/templates/PROJECT.md +25 -0
  35. package/workshop/templates/network-map.mmd +13 -0
  36. package/workshop/templates/prd.md +133 -0
  37. package/workshop/templates/requirements-map.md +48 -0
  38. package/workshop/templates/shared-contracts.md +89 -0
  39. package/workshop/templates/vision.md +66 -0
package/bin/install.js ADDED
@@ -0,0 +1,652 @@
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
+ }
78
+
79
+ function copyFile(src, dest, opts = {}) {
80
+ const { skipExisting = false } = opts;
81
+ if (fs.existsSync(dest) && skipExisting) {
82
+ return { status: 'skipped', dest };
83
+ }
84
+ ensureDir(path.dirname(dest));
85
+ fs.copyFileSync(src, dest);
86
+ return { status: 'created', dest };
87
+ }
88
+
89
+ function copyDir(srcDir, destDir, opts = {}) {
90
+ const results = [];
91
+ if (!fs.existsSync(srcDir)) return results;
92
+
93
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ const srcPath = path.join(srcDir, entry.name);
96
+ const destPath = path.join(destDir, entry.name);
97
+ if (entry.isDirectory()) {
98
+ results.push(...copyDir(srcPath, destPath, opts));
99
+ } else {
100
+ results.push(copyFile(srcPath, destPath, opts));
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+
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
+ 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;
156
+ }
157
+
158
+ // ─── Parse CLI args ────────────────────────────────────────────────────────
159
+ function parseArgs(argv) {
160
+ const args = argv.slice(2);
161
+ 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'),
170
+ };
171
+ }
172
+
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
+ ];
216
+
217
+ // ─── Check if package is available via npx (quick dry-run) ────────────────
218
+ function checkPackageAvailable(pkg) {
219
+ try {
220
+ // Try `npm view` — fast, no install, works offline cache check
221
+ const result = spawnSync('npm', ['view', pkg, 'version'], {
222
+ timeout: 8000,
223
+ stdio: 'pipe',
224
+ encoding: 'utf8',
225
+ });
226
+ return result.status === 0 && result.stdout.trim().length > 0;
227
+ } catch (_) {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ // ─── Install / verify MCP servers ─────────────────────────────────────────
233
+ function installMcpServers(verbose = true) {
234
+ const results = [];
235
+
236
+ for (const server of MCP_SERVERS) {
237
+ if (verbose) process.stdout.write(` ${dim(server.label + '...')} `);
238
+
239
+ try {
240
+ const available = checkPackageAvailable(server.package);
241
+ if (available) {
242
+ if (verbose) console.log(green('✓') + dim(` ${server.package}`));
243
+ results.push({ ...server, ok: true, note: 'available via npx' });
244
+ } else {
245
+ // Try npm install -g as fallback
246
+ const install = spawnSync('npm', ['install', '-g', server.package], {
247
+ timeout: 60000,
248
+ stdio: 'pipe',
249
+ encoding: 'utf8',
250
+ });
251
+ if (install.status === 0) {
252
+ if (verbose) console.log(green('✓') + dim(' installed globally'));
253
+ results.push({ ...server, ok: true, note: 'installed globally' });
254
+ } 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)' });
257
+ }
258
+ }
259
+ } catch (err) {
260
+ if (verbose) console.log(red('✗') + ` ${err.message}`);
261
+ results.push({ ...server, ok: false, error: err.message });
262
+ }
263
+ }
264
+
265
+ return results;
266
+ }
267
+
268
+ // ─── Add MCP servers to settings.json ─────────────────────────────────────
269
+ function addMcpToSettings(settingsPath, mcpResults) {
270
+ let settings = {};
271
+ if (fs.existsSync(settingsPath)) {
272
+ try {
273
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
274
+ } catch (_) {}
275
+ }
276
+
277
+ if (!settings.mcpServers) settings.mcpServers = {};
278
+
279
+ // Add each server that didn't hard-fail
280
+ for (const result of mcpResults) {
281
+ if (!result.ok) continue;
282
+ if (!settings.mcpServers[result.key]) {
283
+ settings.mcpServers[result.key] = result.config;
284
+ }
285
+ }
286
+
287
+ // Also ensure mcp__playwright and mcp__sequential-thinking are in permissions.allow
288
+ if (settings.permissions && Array.isArray(settings.permissions.allow)) {
289
+ const toAdd = ['mcp__playwright', 'mcp__sequential-thinking', 'mcp__context7', 'mcp__filesystem'];
290
+ for (const perm of toAdd) {
291
+ if (!settings.permissions.allow.includes(perm)) {
292
+ settings.permissions.allow.push(perm);
293
+ }
294
+ }
295
+ }
296
+
297
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
298
+ }
299
+
300
+ // ─── Install Playwright browsers ──────────────────────────────────────────
301
+ function installPlaywrightBrowsers(verbose = true) {
302
+ if (verbose) process.stdout.write(` ${dim('Installing Playwright browsers...')} `);
303
+ try {
304
+ const result = spawnSync('npx', ['playwright', 'install', '--with-deps', 'chromium'], {
305
+ timeout: 120000,
306
+ stdio: 'pipe',
307
+ encoding: 'utf8',
308
+ });
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
+ }
326
+ } catch (err) {
327
+ if (verbose) console.log(yellow('⚠') + dim(` ${err.message}`));
328
+ return { ok: false, error: err.message };
329
+ }
330
+ }
331
+
332
+ // ─── Create playwright.config.ts if missing ───────────────────────────────
333
+ function ensurePlaywrightConfig(targetDir) {
334
+ const tsConfig = path.join(targetDir, 'playwright.config.ts');
335
+ const jsConfig = path.join(targetDir, 'playwright.config.js');
336
+ if (fs.existsSync(tsConfig) || fs.existsSync(jsConfig)) {
337
+ return { status: 'skipped', dest: tsConfig };
338
+ }
339
+ const content = `import { defineConfig, devices } from '@playwright/test';
340
+
341
+ export default defineConfig({
342
+ testDir: './e2e',
343
+ fullyParallel: true,
344
+ forbidOnly: !!process.env.CI,
345
+ retries: process.env.CI ? 2 : 0,
346
+ workers: process.env.CI ? 1 : undefined,
347
+ reporter: 'html',
348
+ use: {
349
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
350
+ trace: 'on-first-retry',
351
+ },
352
+ projects: [
353
+ {
354
+ name: 'chromium',
355
+ use: { ...devices['Desktop Chrome'] },
356
+ },
357
+ ],
358
+ });
359
+ `;
360
+ ensureDir(path.dirname(tsConfig));
361
+ fs.writeFileSync(tsConfig, content, 'utf8');
362
+ return { status: 'created', dest: tsConfig };
363
+ }
364
+
365
+ // ─── Verify ralph-loop command ────────────────────────────────────────────
366
+ function verifyRalphLoop(commandsDir) {
367
+ const ralphPath = path.join(commandsDir, 'ralph-loop.md');
368
+ return fs.existsSync(ralphPath);
369
+ }
370
+
371
+ // ─── Install logic ─────────────────────────────────────────────────────────
372
+ async function install(opts) {
373
+ const { targetDir, repoRoot, isGlobal, runtime } = opts;
374
+
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');
378
+
379
+ const steps = [];
380
+
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);
385
+
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 }));
392
+
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));
397
+
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 }));
402
+
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
+ return steps;
412
+ }
413
+
414
+ // ─── Status icon ──────────────────────────────────────────────────────────
415
+ function statusIcon(status) {
416
+ switch (status) {
417
+ case 'created': return green('✓');
418
+ case 'skipped': return dim('─');
419
+ case 'error': return red('✗');
420
+ default: return dim('·');
421
+ }
422
+ }
423
+
424
+ // ─── Main ──────────────────────────────────────────────────────────────────
425
+ async function main() {
426
+ const args = parseArgs(process.argv);
427
+ const repoRoot = findRepoRoot();
428
+
429
+ if (args.help) {
430
+ console.log(`
431
+ ${bold('effectum')} — autonomous development system for Claude Code
432
+
433
+ ${bold('Usage:')}
434
+ npx effectum Interactive installer
435
+ npx effectum --global Install to ~/.claude/ (no prompts)
436
+ npx effectum --local Install to ./.claude/ (no prompts)
437
+ npx effectum --global --claude Non-interactive, Claude Code runtime
438
+ npx effectum --global --with-mcp Include MCP server setup
439
+ npx effectum --global --with-playwright Include Playwright browser install
440
+ npx effectum --global --claude --with-mcp --with-playwright Full install
441
+
442
+ ${bold('Options:')}
443
+ --global, -g Install globally for all projects (~/.claude/)
444
+ --local, -l Install locally for this project (./.claude/)
445
+ --claude Select Claude Code runtime (default)
446
+ --with-mcp Install MCP servers (Context7, Playwright, Sequential Thinking, Filesystem)
447
+ --with-playwright Install Playwright browsers
448
+ --yes, -y Skip confirmation prompts
449
+ --help, -h Show this help
450
+ `);
451
+ process.exit(0);
452
+ }
453
+
454
+ printBanner();
455
+
456
+ // ── Check repo files exist ───────────────────────────────────────────────
457
+ if (!fs.existsSync(path.join(repoRoot, 'system', 'commands'))) {
458
+ console.log(red('✗ Error:') + ' Could not find Effectum system files.');
459
+ console.log(dim(' Expected: ' + path.join(repoRoot, 'system', 'commands')));
460
+ console.log(dim(' This is a bug — please report it at https://github.com/aslomon/effectum/issues'));
461
+ process.exit(1);
462
+ }
463
+
464
+ let isGlobal;
465
+ let runtime = 'claude';
466
+ let wantMcp = args.withMcp;
467
+ let wantPlaywright = args.withPlaywright;
468
+
469
+ // ── Non-interactive mode ─────────────────────────────────────────────────
470
+ if (args.global || args.local) {
471
+ isGlobal = args.global;
472
+ if (args.claude) runtime = 'claude';
473
+ // wantMcp / wantPlaywright already set from flags
474
+ } else {
475
+ // ── Interactive mode ───────────────────────────────────────────────────
476
+ const rl = createRL();
477
+
478
+ try {
479
+ // Scope question
480
+ const scopeIdx = await askChoice(rl,
481
+ bold('Where do you want to install Effectum?'),
482
+ [
483
+ { label: 'Global', desc: 'all projects (~/.claude/)' },
484
+ { label: 'Local', desc: 'this project only (./.claude/)' },
485
+ ],
486
+ 0
487
+ );
488
+ isGlobal = scopeIdx === 0;
489
+ console.log();
490
+
491
+ // Runtime question
492
+ const runtimeIdx = await askChoice(rl,
493
+ bold('Which AI coding runtime?'),
494
+ [
495
+ { label: 'Claude Code', desc: 'default — recommended' },
496
+ { label: 'Codex / Gemini / OpenCode', desc: 'coming soon' },
497
+ ],
498
+ 0
499
+ );
500
+ runtime = runtimeIdx === 0 ? 'claude' : 'other';
501
+ console.log();
502
+
503
+ if (runtime === 'other') {
504
+ console.log(yellow('⚠') + ' Only Claude Code is fully supported right now.');
505
+ console.log(dim(' Other runtimes are on the roadmap. Proceeding with Claude Code configuration.'));
506
+ runtime = 'claude';
507
+ console.log();
508
+ }
509
+
510
+ // MCP question
511
+ wantMcp = await confirm(rl,
512
+ bold('Install MCP servers?') + dim(' (Context7, Playwright MCP, Sequential Thinking, Filesystem)'),
513
+ true
514
+ );
515
+ console.log();
516
+
517
+ // Playwright question
518
+ if (wantMcp) {
519
+ wantPlaywright = await confirm(rl,
520
+ bold('Install Playwright browsers?') + dim(' (required for /e2e command)'),
521
+ true
522
+ );
523
+ console.log();
524
+ }
525
+
526
+ } finally {
527
+ rl.close();
528
+ }
529
+ }
530
+
531
+ // ── Determine target directory ───────────────────────────────────────────
532
+ // For global: target is ~/.claude/ (so claudeDir = ~/.claude/)
533
+ // For local: target is ./ (so claudeDir = ./.claude/)
534
+ const homeClaudeDir = path.join(os.homedir(), '.claude');
535
+ const targetDir = isGlobal ? homeClaudeDir : process.cwd();
536
+ const displayTarget = isGlobal ? '~/.claude' : './.claude';
537
+
538
+ console.log(` ${dim('Scope:')} ${cyan(isGlobal ? 'Global' : 'Local')}`);
539
+ console.log(` ${dim('Target:')} ${cyan(displayTarget)}`);
540
+ console.log(` ${dim('Runtime:')} ${cyan('Claude Code')}`);
541
+ console.log();
542
+
543
+ // ── Step 1: Workflow commands + files ────────────────────────────────────
544
+ console.log(bold(' 1. Installing workflow commands...'));
545
+ let steps;
546
+ try {
547
+ steps = await install({ targetDir, repoRoot, isGlobal, runtime });
548
+ } catch (err) {
549
+ console.log(red(' ✗ Installation failed:') + ' ' + err.message);
550
+ process.exit(1);
551
+ }
552
+
553
+ // Print file results
554
+ for (const step of steps) {
555
+ if (!step || !step.dest) continue;
556
+ const homeDir = os.homedir();
557
+ const rel = step.dest.startsWith(homeDir)
558
+ ? '~/' + path.relative(homeDir, step.dest)
559
+ : path.relative(process.cwd(), step.dest);
560
+ const icon = statusIcon(step.status);
561
+ if (step.status === 'error') {
562
+ console.log(` ${icon} ${red(rel)} — ${step.error || ''}`);
563
+ } else {
564
+ console.log(` ${icon} ${step.status === 'skipped' ? dim(rel) : rel}`);
565
+ }
566
+ }
567
+
568
+ // Verify ralph-loop
569
+ const settingsPath = isGlobal
570
+ ? path.join(homeClaudeDir, 'settings.json')
571
+ : path.join(targetDir, '.claude', 'settings.json');
572
+
573
+ const commandsInstallDir = isGlobal
574
+ ? path.join(homeClaudeDir, 'commands')
575
+ : path.join(targetDir, '.claude', 'commands');
576
+
577
+ if (verifyRalphLoop(commandsInstallDir)) {
578
+ console.log(` ${green('✓')} ralph-loop command ${dim('verified')}`);
579
+ } else {
580
+ console.log(` ${yellow('⚠')} ralph-loop command not found`);
581
+ }
582
+
583
+ console.log(` ${green('✅')} Done`);
584
+ console.log();
585
+
586
+ // ── Step 2: MCP servers ──────────────────────────────────────────────────
587
+ if (wantMcp) {
588
+ console.log(bold(' 2. Installing MCP servers...'));
589
+ const mcpResults = installMcpServers(true);
590
+
591
+ // Inject into settings.json
592
+ try {
593
+ addMcpToSettings(settingsPath, mcpResults);
594
+ console.log(` ${green('✓')} MCP servers added to settings.json`);
595
+ } catch (err) {
596
+ console.log(` ${red('✗')} Could not update settings.json: ${err.message}`);
597
+ }
598
+
599
+ console.log(` ${green('✅')} Done`);
600
+ console.log();
601
+ }
602
+
603
+ // ── Step 3: Playwright browsers ──────────────────────────────────────────
604
+ if (wantPlaywright) {
605
+ console.log(bold(' 3. Setting up Playwright...'));
606
+ installPlaywrightBrowsers(true);
607
+
608
+ // Create playwright.config.ts in the current project (local installs only)
609
+ if (!isGlobal) {
610
+ const pcResult = ensurePlaywrightConfig(process.cwd());
611
+ const icon = statusIcon(pcResult.status);
612
+ const rel = path.relative(process.cwd(), pcResult.dest);
613
+ console.log(` ${icon} ${pcResult.status === 'skipped' ? dim(rel) : rel}`);
614
+ }
615
+
616
+ console.log(` ${green('✅')} Done`);
617
+ console.log();
618
+ }
619
+
620
+ // ── Step 4: Summary ──────────────────────────────────────────────────────
621
+ const createdCount = steps.filter(s => s && s.status === 'created').length;
622
+ const skippedCount = steps.filter(s => s && s.status === 'skipped').length;
623
+
624
+ console.log(green('⚡') + bold(' Effectum ready!'));
625
+ console.log();
626
+ console.log(` ${dim('Files installed:')} ${createdCount}`);
627
+ if (skippedCount) console.log(` ${dim('Already existed:')} ${skippedCount} ${dim('(preserved)')}`);
628
+ if (wantMcp) console.log(` ${dim('MCP servers:')} ${MCP_SERVERS.length} configured`);
629
+ if (wantPlaywright) console.log(` ${dim('Playwright:')} browsers installed`);
630
+ console.log();
631
+
632
+ if (isGlobal) {
633
+ console.log(' ' + bold('Next steps:'));
634
+ console.log(` ${cyan('1.')} Open Claude Code in any project`);
635
+ console.log(` ${cyan('2.')} Run ${bold('/setup ~/your-project')} to configure it`);
636
+ console.log(` ${cyan('3.')} Write a spec with ${bold('/prd:new')}`);
637
+ } else {
638
+ console.log(' ' + bold('Next steps:'));
639
+ console.log(` ${cyan('1.')} Open Claude Code here: ${dim('claude')}`);
640
+ console.log(` ${cyan('2.')} Run ${bold('/setup .')} to configure this project`);
641
+ console.log(` ${cyan('3.')} Write a spec with ${bold('/prd:new')}`);
642
+ }
643
+
644
+ console.log();
645
+ console.log(dim(' Docs: https://github.com/aslomon/effectum'));
646
+ console.log();
647
+ }
648
+
649
+ main().catch(err => {
650
+ console.error(red('Fatal error:'), err.message);
651
+ process.exit(1);
652
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@aslomon/effectum",
3
+ "version": "0.1.0",
4
+ "description": "Autonomous development system for Claude Code — describe what you want, get production-ready code",
5
+ "bin": {
6
+ "effectum": "bin/install.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "system/",
11
+ "workshop/"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/aslomon/effectum.git"
17
+ },
18
+ "keywords": [
19
+ "claude-code",
20
+ "autonomous",
21
+ "development",
22
+ "spec-driven",
23
+ "tdd",
24
+ "prd"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }