@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.
- package/README.md +633 -0
- package/bin/install.js +652 -0
- package/package.json +29 -0
- package/system/README.md +118 -0
- package/system/commands/build-fix.md +89 -0
- package/system/commands/cancel-ralph.md +90 -0
- package/system/commands/checkpoint.md +63 -0
- package/system/commands/code-review.md +120 -0
- package/system/commands/e2e.md +92 -0
- package/system/commands/plan.md +111 -0
- package/system/commands/ralph-loop.md +163 -0
- package/system/commands/refactor-clean.md +104 -0
- package/system/commands/tdd.md +84 -0
- package/system/commands/verify.md +71 -0
- package/system/stacks/generic.md +96 -0
- package/system/stacks/nextjs-supabase.md +114 -0
- package/system/stacks/python-fastapi.md +140 -0
- package/system/stacks/swift-ios.md +136 -0
- package/system/templates/AUTONOMOUS-WORKFLOW.md +1368 -0
- package/system/templates/CLAUDE.md.tmpl +141 -0
- package/system/templates/guardrails.md.tmpl +39 -0
- package/system/templates/settings.json.tmpl +201 -0
- package/workshop/knowledge/01-prd-template.md +275 -0
- package/workshop/knowledge/02-questioning-framework.md +209 -0
- package/workshop/knowledge/03-decomposition-guide.md +234 -0
- package/workshop/knowledge/04-examples.md +435 -0
- package/workshop/knowledge/05-quality-checklist.md +166 -0
- package/workshop/knowledge/06-network-map-guide.md +413 -0
- package/workshop/knowledge/07-prompt-templates.md +315 -0
- package/workshop/knowledge/08-workflow-modes.md +198 -0
- package/workshop/projects/_example-project/PROJECT.md +33 -0
- package/workshop/projects/_example-project/notes/decisions.md +15 -0
- package/workshop/projects/_example-project/notes/discovery-log.md +9 -0
- package/workshop/templates/PROJECT.md +25 -0
- package/workshop/templates/network-map.mmd +13 -0
- package/workshop/templates/prd.md +133 -0
- package/workshop/templates/requirements-map.md +48 -0
- package/workshop/templates/shared-contracts.md +89 -0
- 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
|
+
}
|