@aslomon/effectum 0.1.6 → 0.2.1
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/effectum.js +50 -0
- package/bin/init.js +30 -0
- package/bin/install.js +532 -484
- package/bin/lib/config.js +55 -0
- package/bin/lib/constants.js +197 -0
- package/bin/lib/detect.js +98 -0
- package/bin/lib/stack-parser.js +56 -0
- package/bin/lib/template.js +108 -0
- package/bin/lib/ui.js +246 -0
- package/bin/lib/utils.js +56 -0
- package/bin/reconfigure.js +170 -0
- package/package.json +7 -4
package/bin/install.js
CHANGED
|
@@ -1,98 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 { detectAll } = require("./lib/detect");
|
|
20
|
+
const { loadStackPreset } = require("./lib/stack-parser");
|
|
21
|
+
const {
|
|
22
|
+
buildSubstitutionMap,
|
|
23
|
+
renderTemplate,
|
|
24
|
+
findTemplatePath,
|
|
25
|
+
findRemainingPlaceholders,
|
|
26
|
+
} = require("./lib/template");
|
|
27
|
+
const { writeConfig } = require("./lib/config");
|
|
28
|
+
const { AUTONOMY_MAP, FORMATTER_MAP, MCP_SERVERS } = require("./lib/constants");
|
|
29
|
+
const { ensureDir, deepMerge, findRepoRoot: findRepoRootShared } = require("./lib/utils");
|
|
30
|
+
const {
|
|
31
|
+
initClack,
|
|
32
|
+
getClack,
|
|
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
|
-
|
|
81
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
163
|
-
local:
|
|
164
|
-
claude:
|
|
165
|
-
withMcp:
|
|
166
|
-
withPlaywright: args.includes(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
help:
|
|
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
|
|
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
|
-
|
|
221
|
-
const result = spawnSync('npm', ['view', pkg, 'version'], {
|
|
95
|
+
const result = spawnSync("npm", ["view", pkg, "version"], {
|
|
222
96
|
timeout: 8000,
|
|
223
|
-
stdio:
|
|
224
|
-
encoding:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
results.push({ ...server, ok: true, note: 'available via npx' });
|
|
114
|
+
results.push({ ...server, ok: true, note: "available via npx" });
|
|
244
115
|
} else {
|
|
245
|
-
|
|
246
|
-
const install = spawnSync('npm', ['install', '-g', server.package], {
|
|
116
|
+
const install = spawnSync("npm", ["install", "-g", server.package], {
|
|
247
117
|
timeout: 60000,
|
|
248
|
-
stdio:
|
|
249
|
-
encoding:
|
|
118
|
+
stdio: "pipe",
|
|
119
|
+
encoding: "utf8",
|
|
250
120
|
});
|
|
251
121
|
if (install.status === 0) {
|
|
252
|
-
|
|
253
|
-
results.push({ ...server, ok: true, note: 'installed globally' });
|
|
122
|
+
results.push({ ...server, ok: true, note: "installed globally" });
|
|
254
123
|
} else {
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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,
|
|
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] =
|
|
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 = [
|
|
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(
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
settingsPath,
|
|
172
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
173
|
+
"utf8",
|
|
174
|
+
);
|
|
298
175
|
}
|
|
299
176
|
|
|
300
|
-
// ───
|
|
301
|
-
|
|
302
|
-
|
|
177
|
+
// ─── Playwright install helpers ───────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function installPlaywrightBrowsers() {
|
|
303
180
|
try {
|
|
304
|
-
const result = spawnSync(
|
|
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:
|
|
307
|
-
encoding:
|
|
189
|
+
stdio: "pipe",
|
|
190
|
+
encoding: "utf8",
|
|
308
191
|
});
|
|
309
|
-
if (
|
|
310
|
-
|
|
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
|
|
335
|
-
const jsConfig
|
|
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:
|
|
203
|
+
return { status: "skipped", dest: tsConfig };
|
|
338
204
|
}
|
|
339
205
|
const content = `import { defineConfig, devices } from '@playwright/test';
|
|
340
206
|
|
|
@@ -358,307 +224,489 @@ export default defineConfig({
|
|
|
358
224
|
});
|
|
359
225
|
`;
|
|
360
226
|
ensureDir(path.dirname(tsConfig));
|
|
361
|
-
fs.writeFileSync(tsConfig, content,
|
|
362
|
-
return { status:
|
|
227
|
+
fs.writeFileSync(tsConfig, content, "utf8");
|
|
228
|
+
return { status: "created", dest: tsConfig };
|
|
363
229
|
}
|
|
364
230
|
|
|
365
|
-
// ───
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
//
|
|
376
|
-
const
|
|
377
|
-
const
|
|
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
|
-
//
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
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
|
+
console.warn(
|
|
304
|
+
`⚠ CLAUDE.md has remaining placeholders: ${claudeMdRemaining.join(", ")}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
385
307
|
|
|
386
|
-
// 2.
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
//
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
//
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
//
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
// ───
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
npx effectum
|
|
445
|
-
npx effectum
|
|
446
|
-
npx effectum
|
|
447
|
-
npx effectum --global
|
|
448
|
-
npx effectum --
|
|
449
|
-
npx effectum --
|
|
450
|
-
npx effectum --
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
452
|
+
--with-mcp Install MCP servers
|
|
457
453
|
--with-playwright Install Playwright browsers
|
|
458
|
-
--yes, -y Skip
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
console.log();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
//
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
: path.join(targetDir, '.claude', 'settings.json');
|
|
527
|
+
// ── Interactive mode ────────────────────────────────────────────────────
|
|
528
|
+
const p = await initClack();
|
|
529
|
+
printBanner();
|
|
582
530
|
|
|
583
|
-
const
|
|
584
|
-
? path.join(homeClaudeDir, 'commands')
|
|
585
|
-
: path.join(targetDir, '.claude', 'commands');
|
|
531
|
+
const detected = detectAll(process.cwd());
|
|
586
532
|
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
533
|
+
if (detected.stack) {
|
|
534
|
+
p.log.info(
|
|
535
|
+
`Detected: ${detected.stack} project (${detected.packageManager})`,
|
|
536
|
+
);
|
|
591
537
|
}
|
|
592
538
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
539
|
+
// Scope
|
|
540
|
+
const scopeValue = await p.select({
|
|
541
|
+
message: "Install scope",
|
|
542
|
+
options: [
|
|
543
|
+
{
|
|
544
|
+
value: "local",
|
|
545
|
+
label: "Local",
|
|
546
|
+
hint: "This project only (./.claude/)",
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
value: "global",
|
|
550
|
+
label: "Global",
|
|
551
|
+
hint: "All projects (~/.claude/)",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
initialValue: "local",
|
|
555
|
+
});
|
|
556
|
+
if (p.isCancel(scopeValue)) {
|
|
557
|
+
p.cancel("Setup cancelled.");
|
|
558
|
+
process.exit(0);
|
|
559
|
+
}
|
|
560
|
+
const installGlobal = scopeValue === "global";
|
|
561
|
+
const installTargetDir = installGlobal ? homeClaudeDir : process.cwd();
|
|
562
|
+
|
|
563
|
+
// Project name
|
|
564
|
+
const projectName = await askProjectName(detected.projectName);
|
|
565
|
+
|
|
566
|
+
// Stack
|
|
567
|
+
const stack = await askStack(detected.stack);
|
|
568
|
+
|
|
569
|
+
// Language
|
|
570
|
+
const langResult = await askLanguage();
|
|
571
|
+
|
|
572
|
+
// Autonomy
|
|
573
|
+
const autonomyLevel = await askAutonomy();
|
|
574
|
+
|
|
575
|
+
// MCP servers
|
|
576
|
+
const mcpServerKeys = await askMcpServers();
|
|
577
|
+
|
|
578
|
+
// Playwright
|
|
579
|
+
const wantPlaywright = mcpServerKeys.includes("playwright")
|
|
580
|
+
? await askPlaywright()
|
|
581
|
+
: false;
|
|
582
|
+
|
|
583
|
+
// Git branch
|
|
584
|
+
const gitBranch = await askGitBranch();
|
|
585
|
+
|
|
586
|
+
// Build config object
|
|
587
|
+
const formatter = FORMATTER_MAP[stack] || FORMATTER_MAP.generic;
|
|
588
|
+
const config = {
|
|
589
|
+
projectName,
|
|
590
|
+
stack,
|
|
591
|
+
language: langResult.language,
|
|
592
|
+
...(langResult.customLanguage
|
|
593
|
+
? { customLanguage: langResult.customLanguage }
|
|
594
|
+
: {}),
|
|
595
|
+
autonomyLevel,
|
|
596
|
+
packageManager: detected.packageManager,
|
|
597
|
+
formatter: formatter.name,
|
|
598
|
+
mcpServers: mcpServerKeys,
|
|
599
|
+
playwrightBrowsers: wantPlaywright,
|
|
600
|
+
installScope: installGlobal ? "global" : "local",
|
|
601
|
+
};
|
|
600
602
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
603
|
+
// ── Dry run ─────────────────────────────────────────────────────────────
|
|
604
|
+
if (args.dryRun) {
|
|
605
|
+
p.log.info("Dry run — no files will be written.");
|
|
606
|
+
p.note(JSON.stringify(config, null, 2), "Planned Configuration");
|
|
607
|
+
const plannedFiles = [
|
|
608
|
+
".claude/commands/*.md",
|
|
609
|
+
".claude/settings.json",
|
|
610
|
+
".claude/guardrails.md",
|
|
611
|
+
"CLAUDE.md",
|
|
612
|
+
"AUTONOMOUS-WORKFLOW.md",
|
|
613
|
+
".effectum.json",
|
|
614
|
+
];
|
|
615
|
+
if (mcpServerKeys.length > 0) {
|
|
616
|
+
plannedFiles.push("MCP servers in settings.json");
|
|
607
617
|
}
|
|
618
|
+
p.note(plannedFiles.join("\n"), "Files to be created/updated");
|
|
619
|
+
p.outro("Dry run complete. No changes made.");
|
|
620
|
+
process.exit(0);
|
|
621
|
+
}
|
|
608
622
|
|
|
609
|
-
|
|
610
|
-
|
|
623
|
+
// ── Create git branch ──────────────────────────────────────────────────
|
|
624
|
+
if (gitBranch.create) {
|
|
625
|
+
const s = p.spinner();
|
|
626
|
+
s.start("Creating git branch...");
|
|
627
|
+
const ok = createGitBranch(gitBranch.name);
|
|
628
|
+
if (ok) {
|
|
629
|
+
s.stop(`Branch "${gitBranch.name}" created`);
|
|
630
|
+
} else {
|
|
631
|
+
s.stop("Could not create branch (may already exist)");
|
|
632
|
+
}
|
|
611
633
|
}
|
|
612
634
|
|
|
613
|
-
// ── Step
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
635
|
+
// ── Step 1: Base files ─────────────────────────────────────────────────
|
|
636
|
+
const s1 = p.spinner();
|
|
637
|
+
s1.start("Installing workflow commands and templates...");
|
|
638
|
+
const baseSteps = installBaseFiles(installTargetDir, repoRoot, installGlobal);
|
|
639
|
+
s1.stop(
|
|
640
|
+
`Installed ${baseSteps.filter((s) => s.status === "created").length} files`,
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// ── Step 2: Configure (local only) ────────────────────────────────────
|
|
644
|
+
const configSteps = [];
|
|
645
|
+
if (!installGlobal) {
|
|
646
|
+
const s2 = p.spinner();
|
|
647
|
+
s2.start(
|
|
648
|
+
"Generating configured files (CLAUDE.md, settings.json, guardrails.md)...",
|
|
649
|
+
);
|
|
650
|
+
const cSteps = generateConfiguredFiles(
|
|
651
|
+
config,
|
|
652
|
+
installTargetDir,
|
|
653
|
+
repoRoot,
|
|
654
|
+
installGlobal,
|
|
655
|
+
);
|
|
656
|
+
configSteps.push(...cSteps);
|
|
657
|
+
s2.stop("Configuration files generated");
|
|
658
|
+
}
|
|
617
659
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
660
|
+
// ── Step 3: MCP servers ────────────────────────────────────────────────
|
|
661
|
+
if (mcpServerKeys.length > 0) {
|
|
662
|
+
const s3 = p.spinner();
|
|
663
|
+
s3.start("Setting up MCP servers...");
|
|
664
|
+
const mcpResults = installMcpServers(mcpServerKeys);
|
|
665
|
+
const settingsPath = installGlobal
|
|
666
|
+
? path.join(homeClaudeDir, "settings.json")
|
|
667
|
+
: path.join(installTargetDir, ".claude", "settings.json");
|
|
668
|
+
addMcpToSettings(settingsPath, mcpResults, installTargetDir);
|
|
669
|
+
const okCount = mcpResults.filter((r) => r.ok).length;
|
|
670
|
+
s3.stop(`${okCount} MCP servers configured`);
|
|
671
|
+
}
|
|
625
672
|
|
|
626
|
-
|
|
627
|
-
|
|
673
|
+
// ── Step 4: Playwright ─────────────────────────────────────────────────
|
|
674
|
+
if (wantPlaywright) {
|
|
675
|
+
const s4 = p.spinner();
|
|
676
|
+
s4.start("Installing Playwright browsers...");
|
|
677
|
+
const pwResult = installPlaywrightBrowsers();
|
|
678
|
+
if (!installGlobal) ensurePlaywrightConfig(process.cwd());
|
|
679
|
+
s4.stop(
|
|
680
|
+
pwResult.ok
|
|
681
|
+
? "Playwright browsers installed"
|
|
682
|
+
: "Playwright install failed (run manually: npx playwright install)",
|
|
683
|
+
);
|
|
628
684
|
}
|
|
629
685
|
|
|
630
|
-
// ── Step
|
|
631
|
-
|
|
632
|
-
|
|
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')}`);
|
|
686
|
+
// ── Step 5: Save config ────────────────────────────────────────────────
|
|
687
|
+
if (!installGlobal) {
|
|
688
|
+
const configPath = writeConfig(installTargetDir, config);
|
|
689
|
+
configSteps.push({ status: "created", dest: configPath });
|
|
654
690
|
}
|
|
655
691
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
692
|
+
// ── Summary ─────────────────────────────────────────────────────────────
|
|
693
|
+
const allSteps = [...baseSteps, ...configSteps];
|
|
694
|
+
const allFiles = allSteps
|
|
695
|
+
.filter((s) => s && s.dest)
|
|
696
|
+
.map((s) => {
|
|
697
|
+
const homeDir = os.homedir();
|
|
698
|
+
return s.dest.startsWith(homeDir)
|
|
699
|
+
? "~/" + path.relative(homeDir, s.dest)
|
|
700
|
+
: path.relative(process.cwd(), s.dest);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Deduplicate for summary
|
|
704
|
+
const uniqueFiles = [...new Set(allFiles)].slice(0, 20);
|
|
705
|
+
showSummary(config, uniqueFiles);
|
|
706
|
+
showOutro(installGlobal);
|
|
659
707
|
}
|
|
660
708
|
|
|
661
|
-
main().catch(err => {
|
|
662
|
-
console.error(
|
|
709
|
+
main().catch((err) => {
|
|
710
|
+
console.error(`Fatal error: ${err.message}`);
|
|
663
711
|
process.exit(1);
|
|
664
712
|
});
|