@aslomon/effectum 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lib/ui.js CHANGED
@@ -1,21 +1,53 @@
1
1
  /**
2
2
  * Shared @clack/prompts helpers and display utilities.
3
+ * Uses dynamic import() because @clack/prompts is ESM-only.
4
+ *
5
+ * Supports the 9-step intelligent setup flow:
6
+ * 1. Install Scope 2. Project Basics 3. App Type 4. Description
7
+ * 5. Language 6. Autonomy 7. Recommendation Preview 8. Decision 9. Install
3
8
  */
4
9
  "use strict";
5
10
 
6
- const p = require("@clack/prompts");
11
+ const { STACK_CHOICES, AUTONOMY_CHOICES, MCP_SERVERS } = require("./constants");
12
+ const { LANGUAGE_CHOICES } = require("./languages");
13
+ const { APP_TYPE_CHOICES } = require("./app-types");
14
+ const { FOUNDATION_HOOKS } = require("./foundation");
7
15
  const {
8
- STACK_CHOICES,
9
- LANGUAGE_CHOICES,
10
- AUTONOMY_CHOICES,
11
- MCP_SERVERS,
12
- } = require("./constants");
16
+ getAllCommands,
17
+ getAllHooks,
18
+ getAllSkills,
19
+ getAllMcps,
20
+ getAllSubagents,
21
+ } = require("./recommendation");
22
+
23
+ /** @type {import("@clack/prompts")} */
24
+ let p;
25
+
26
+ /**
27
+ * Initialize @clack/prompts (ESM module). Must be called once before using prompts.
28
+ * @returns {Promise<import("@clack/prompts")>}
29
+ */
30
+ async function initClack() {
31
+ if (!p) {
32
+ p = await import("@clack/prompts");
33
+ }
34
+ return p;
35
+ }
36
+
37
+ /**
38
+ * Get the loaded clack instance.
39
+ * @returns {import("@clack/prompts")}
40
+ */
41
+ function getClack() {
42
+ if (!p) throw new Error("Call initClack() before using prompts");
43
+ return p;
44
+ }
13
45
 
14
46
  /**
15
47
  * Print the Effectum banner via clack intro.
16
48
  */
17
49
  function printBanner() {
18
- p.intro("EFFECTUM — Autonomous development system for Claude Code");
50
+ p.intro("EFFECTUM — Intelligent Setup for Claude Code");
19
51
  }
20
52
 
21
53
  /**
@@ -29,6 +61,35 @@ function handleCancel(value) {
29
61
  }
30
62
  }
31
63
 
64
+ // ─── Step 1: Install Scope ──────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Ask for install scope.
68
+ * @returns {Promise<string>} "local" or "global"
69
+ */
70
+ async function askScope() {
71
+ const value = await p.select({
72
+ message: "Install scope",
73
+ options: [
74
+ {
75
+ value: "local",
76
+ label: "Local",
77
+ hint: "This project only (./.claude/)",
78
+ },
79
+ {
80
+ value: "global",
81
+ label: "Global",
82
+ hint: "All projects (~/.claude/)",
83
+ },
84
+ ],
85
+ initialValue: "local",
86
+ });
87
+ handleCancel(value);
88
+ return value;
89
+ }
90
+
91
+ // ─── Step 2: Project Basics ─────────────────────────────────────────────────
92
+
32
93
  /**
33
94
  * Ask for project name with a detected default.
34
95
  * @param {string} detected
@@ -66,6 +127,47 @@ async function askStack(detected) {
66
127
  return value;
67
128
  }
68
129
 
130
+ // ─── Step 3: App Type ───────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Ask for app type selection.
134
+ * @returns {Promise<string>}
135
+ */
136
+ async function askAppType() {
137
+ const value = await p.select({
138
+ message: "What are you building?",
139
+ options: APP_TYPE_CHOICES.map((c) => ({
140
+ value: c.value,
141
+ label: c.label,
142
+ hint: c.hint,
143
+ })),
144
+ initialValue: "web-app",
145
+ });
146
+ handleCancel(value);
147
+ return value;
148
+ }
149
+
150
+ // ─── Step 4: Description ────────────────────────────────────────────────────
151
+
152
+ /**
153
+ * Ask for a free-text project description.
154
+ * @returns {Promise<string>}
155
+ */
156
+ async function askDescription() {
157
+ const value = await p.text({
158
+ message: "Describe what you want to build (one sentence)",
159
+ placeholder: "e.g. An internal CRM dashboard with auth and analytics",
160
+ validate: (v) => {
161
+ if (!v.trim())
162
+ return "A short description helps generate better recommendations";
163
+ },
164
+ });
165
+ handleCancel(value);
166
+ return value;
167
+ }
168
+
169
+ // ─── Step 5: Language ───────────────────────────────────────────────────────
170
+
69
171
  /**
70
172
  * Ask for communication language.
71
173
  * @returns {Promise<{ language: string, customLanguage?: string }>}
@@ -97,13 +199,15 @@ async function askLanguage() {
97
199
  return { language: value };
98
200
  }
99
201
 
202
+ // ─── Step 6: Autonomy ───────────────────────────────────────────────────────
203
+
100
204
  /**
101
205
  * Ask for autonomy level.
102
206
  * @returns {Promise<string>}
103
207
  */
104
208
  async function askAutonomy() {
105
209
  const value = await p.select({
106
- message: "Autonomy level",
210
+ message: "How should Claude work?",
107
211
  options: AUTONOMY_CHOICES.map((c) => ({
108
212
  value: c.value,
109
213
  label: c.label,
@@ -115,6 +219,232 @@ async function askAutonomy() {
115
219
  return value;
116
220
  }
117
221
 
222
+ // ─── Step 7: Recommendation Preview ─────────────────────────────────────────
223
+
224
+ /**
225
+ * Display the recommendation preview.
226
+ * @param {object} rec - recommendation from recommend()
227
+ */
228
+ function showRecommendation(rec) {
229
+ const lines = [];
230
+
231
+ lines.push("FOUNDATION (always active)");
232
+ for (const h of FOUNDATION_HOOKS) {
233
+ lines.push(` + ${h.label}`);
234
+ }
235
+
236
+ lines.push("");
237
+ lines.push("RECOMMENDED COMMANDS");
238
+ for (const key of rec.commands) {
239
+ lines.push(` + /${key}`);
240
+ }
241
+
242
+ lines.push("");
243
+ lines.push("RECOMMENDED HOOKS");
244
+ for (const key of rec.hooks) {
245
+ lines.push(` + ${key}`);
246
+ }
247
+
248
+ if (rec.skills.length > 0) {
249
+ lines.push("");
250
+ lines.push("RECOMMENDED SKILLS");
251
+ for (const key of rec.skills) {
252
+ lines.push(` + ${key}`);
253
+ }
254
+ }
255
+
256
+ lines.push("");
257
+ lines.push("RECOMMENDED MCP SERVERS");
258
+ for (const key of rec.mcps) {
259
+ const server = MCP_SERVERS.find((s) => s.key === key);
260
+ lines.push(` + ${server ? server.label : key}`);
261
+ }
262
+
263
+ lines.push("");
264
+ lines.push("RECOMMENDED SUBAGENT SPECIALIZATIONS");
265
+ for (const key of rec.subagents) {
266
+ lines.push(` + ${key}`);
267
+ }
268
+
269
+ lines.push("");
270
+ lines.push("AGENT TEAMS: disabled (experimental, enable manually)");
271
+
272
+ p.note(lines.join("\n"), "Recommended Setup");
273
+ }
274
+
275
+ // ─── Step 8: Decision ───────────────────────────────────────────────────────
276
+
277
+ /**
278
+ * Ask user how to proceed with the recommendation.
279
+ * @returns {Promise<string>} "recommended" | "customize" | "manual"
280
+ */
281
+ async function askSetupMode() {
282
+ const value = await p.select({
283
+ message: "How do you want to proceed?",
284
+ options: [
285
+ {
286
+ value: "recommended",
287
+ label: "Use Recommended",
288
+ hint: "Install the recommended setup as-is",
289
+ },
290
+ {
291
+ value: "customize",
292
+ label: "Customize",
293
+ hint: "Start from recommendations, toggle items on/off",
294
+ },
295
+ {
296
+ value: "manual",
297
+ label: "Manual Selection",
298
+ hint: "Choose everything yourself from scratch",
299
+ },
300
+ ],
301
+ initialValue: "recommended",
302
+ });
303
+ handleCancel(value);
304
+ return value;
305
+ }
306
+
307
+ /**
308
+ * Let user customize the recommended setup by toggling items.
309
+ * @param {object} rec - current recommendation
310
+ * @returns {Promise<object>} modified recommendation
311
+ */
312
+ async function askCustomize(rec) {
313
+ const result = { ...rec };
314
+
315
+ // Commands
316
+ const commands = await p.multiselect({
317
+ message: "Commands (space to toggle)",
318
+ options: getAllCommands().map((c) => ({
319
+ value: c.key,
320
+ label: c.label,
321
+ })),
322
+ initialValues: rec.commands,
323
+ required: false,
324
+ });
325
+ handleCancel(commands);
326
+ result.commands = commands;
327
+
328
+ // Skills
329
+ const skills = await p.multiselect({
330
+ message: "Skills (space to toggle)",
331
+ options: getAllSkills().map((s) => ({
332
+ value: s.key,
333
+ label: s.label,
334
+ })),
335
+ initialValues: rec.skills,
336
+ required: false,
337
+ });
338
+ handleCancel(skills);
339
+ result.skills = skills;
340
+
341
+ // MCP servers
342
+ const mcps = await p.multiselect({
343
+ message: "MCP servers (space to toggle)",
344
+ options: getAllMcps().map((m) => ({
345
+ value: m.key,
346
+ label: m.label,
347
+ hint: m.desc,
348
+ })),
349
+ initialValues: rec.mcps,
350
+ required: false,
351
+ });
352
+ handleCancel(mcps);
353
+ result.mcps = mcps;
354
+
355
+ // Subagent specializations
356
+ const subagents = await p.multiselect({
357
+ message: "Subagent specializations (space to toggle)",
358
+ options: getAllSubagents().map((s) => ({
359
+ value: s.key,
360
+ label: s.label,
361
+ })),
362
+ initialValues: rec.subagents,
363
+ required: false,
364
+ });
365
+ handleCancel(subagents);
366
+ result.subagents = subagents;
367
+
368
+ // Agent Teams (experimental)
369
+ const agentTeams = await p.confirm({
370
+ message: "Enable Agent Teams? (experimental, advanced)",
371
+ initialValue: false,
372
+ });
373
+ handleCancel(agentTeams);
374
+ result.agentTeams = agentTeams;
375
+
376
+ return result;
377
+ }
378
+
379
+ /**
380
+ * Full manual selection — nothing pre-selected.
381
+ * @returns {Promise<object>} user-selected setup
382
+ */
383
+ async function askManual() {
384
+ const commands = await p.multiselect({
385
+ message: "Select commands",
386
+ options: getAllCommands().map((c) => ({
387
+ value: c.key,
388
+ label: c.label,
389
+ })),
390
+ initialValues: [],
391
+ required: false,
392
+ });
393
+ handleCancel(commands);
394
+
395
+ const skills = await p.multiselect({
396
+ message: "Select skills",
397
+ options: getAllSkills().map((s) => ({
398
+ value: s.key,
399
+ label: s.label,
400
+ })),
401
+ initialValues: [],
402
+ required: false,
403
+ });
404
+ handleCancel(skills);
405
+
406
+ const mcps = await p.multiselect({
407
+ message: "Select MCP servers",
408
+ options: getAllMcps().map((m) => ({
409
+ value: m.key,
410
+ label: m.label,
411
+ hint: m.desc,
412
+ })),
413
+ initialValues: [],
414
+ required: false,
415
+ });
416
+ handleCancel(mcps);
417
+
418
+ const subagents = await p.multiselect({
419
+ message: "Select subagent specializations",
420
+ options: getAllSubagents().map((s) => ({
421
+ value: s.key,
422
+ label: s.label,
423
+ })),
424
+ initialValues: [],
425
+ required: false,
426
+ });
427
+ handleCancel(subagents);
428
+
429
+ const agentTeams = await p.confirm({
430
+ message: "Enable Agent Teams? (experimental, advanced)",
431
+ initialValue: false,
432
+ });
433
+ handleCancel(agentTeams);
434
+
435
+ return {
436
+ commands,
437
+ hooks: getAllHooks().map((h) => h.key),
438
+ skills,
439
+ mcps,
440
+ subagents,
441
+ agentTeams,
442
+ tags: [],
443
+ };
444
+ }
445
+
446
+ // ─── Legacy prompts (kept for backward compat) ──────────────────────────────
447
+
118
448
  /**
119
449
  * Ask which MCP servers to install via multi-select.
120
450
  * @returns {Promise<string[]>}
@@ -169,6 +499,8 @@ async function askGitBranch() {
169
499
  return { create: true, name };
170
500
  }
171
501
 
502
+ // ─── Display helpers ────────────────────────────────────────────────────────
503
+
172
504
  /**
173
505
  * Display a summary note.
174
506
  * @param {object} config
@@ -176,17 +508,29 @@ async function askGitBranch() {
176
508
  */
177
509
  function showSummary(config, files) {
178
510
  const lines = [
179
- `Project: ${config.projectName}`,
180
- `Stack: ${config.stack}`,
181
- `Language: ${config.language}`,
182
- `Autonomy: ${config.autonomyLevel}`,
183
- `Pkg Mgr: ${config.packageManager}`,
184
- `Formatter: ${config.formatter}`,
185
- `MCP: ${(config.mcpServers || []).join(", ") || "none"}`,
186
- "",
187
- `Files created/updated:`,
188
- ...files.map((f) => ` ${f}`),
511
+ `Project: ${config.projectName}`,
512
+ `Stack: ${config.stack}`,
513
+ `App Type: ${config.appType || "n/a"}`,
514
+ `Language: ${config.language}`,
515
+ `Autonomy: ${config.autonomyLevel}`,
516
+ `Pkg Manager: ${config.packageManager}`,
517
+ `Formatter: ${config.formatter}`,
518
+ `Mode: ${config.mode || "recommended"}`,
189
519
  ];
520
+
521
+ if (config.recommended) {
522
+ lines.push(
523
+ `Commands: ${config.recommended.commands.length}`,
524
+ `Skills: ${config.recommended.skills.length}`,
525
+ `MCPs: ${config.recommended.mcps.length}`,
526
+ `Subagents: ${config.recommended.subagents.length}`,
527
+ `Agent Teams: ${config.recommended.agentTeams ? "enabled" : "disabled"}`,
528
+ );
529
+ }
530
+
531
+ lines.push("", `Files created/updated:`);
532
+ lines.push(...files.map((f) => ` ${f}`));
533
+
190
534
  p.note(lines.join("\n"), "Configuration Summary");
191
535
  }
192
536
 
@@ -196,9 +540,7 @@ function showSummary(config, files) {
196
540
  */
197
541
  function showOutro(isGlobal) {
198
542
  if (isGlobal) {
199
- p.outro(
200
- "Effectum ready! In any project, run: npx @aslomon/effectum init",
201
- );
543
+ p.outro("Effectum ready! In any project, run: npx @aslomon/effectum init");
202
544
  } else {
203
545
  p.outro(
204
546
  "Effectum ready! Open Claude Code here and start building. Try /plan or /prd:new",
@@ -207,15 +549,27 @@ function showOutro(isGlobal) {
207
549
  }
208
550
 
209
551
  module.exports = {
552
+ initClack,
553
+ getClack,
210
554
  printBanner,
211
555
  handleCancel,
556
+ // Step prompts
557
+ askScope,
212
558
  askProjectName,
213
559
  askStack,
560
+ askAppType,
561
+ askDescription,
214
562
  askLanguage,
215
563
  askAutonomy,
564
+ showRecommendation,
565
+ askSetupMode,
566
+ askCustomize,
567
+ askManual,
568
+ // Legacy / utility prompts
216
569
  askMcpServers,
217
570
  askPlaywright,
218
571
  askGitBranch,
572
+ // Display
219
573
  showSummary,
220
574
  showOutro,
221
575
  };
@@ -2,24 +2,24 @@
2
2
  /**
3
3
  * Reconfigure — re-apply settings from .effectum.json.
4
4
  * Reads the saved config and regenerates CLAUDE.md, settings.json, guardrails.md.
5
+ * Supports v0.4.0 config schema with appType, description, recommended setup.
5
6
  */
6
7
  "use strict";
7
8
 
8
9
  const fs = require("fs");
9
10
  const path = require("path");
10
- const p = require("@clack/prompts");
11
-
12
11
  const { readConfig } = require("./lib/config");
13
12
  const { loadStackPreset } = require("./lib/stack-parser");
14
13
  const {
15
14
  buildSubstitutionMap,
16
15
  renderTemplate,
17
16
  findTemplatePath,
18
- findRemainingPlaceholders,
19
- substituteAll,
20
17
  } = require("./lib/template");
21
18
  const { AUTONOMY_MAP, FORMATTER_MAP } = require("./lib/constants");
22
19
  const { ensureDir, deepMerge, findRepoRoot } = require("./lib/utils");
20
+ const { initClack } = require("./lib/ui");
21
+ const { recommend } = require("./lib/recommendation");
22
+ const { writeConfig } = require("./lib/config");
23
23
 
24
24
  // ─── Main ─────────────────────────────────────────────────────────────────
25
25
 
@@ -29,6 +29,7 @@ async function main() {
29
29
  const targetDir = process.cwd();
30
30
  const repoRoot = findRepoRoot();
31
31
 
32
+ const p = await initClack();
32
33
  p.intro("EFFECTUM — Reconfigure");
33
34
 
34
35
  // Read existing config
@@ -40,7 +41,24 @@ async function main() {
40
41
  process.exit(1);
41
42
  }
42
43
 
43
- p.log.info(`Reconfiguring "${config.projectName}" (${config.stack})`);
44
+ // Re-run recommendation engine with current config values
45
+ if (config.appType || config.description) {
46
+ const rec = recommend({
47
+ stack: config.stack,
48
+ appType: config.appType || "web-app",
49
+ description: config.description || "",
50
+ autonomyLevel: config.autonomyLevel || "standard",
51
+ language: config.language || "english",
52
+ });
53
+ config.recommended = rec;
54
+ // Persist updated recommendations
55
+ writeConfig(targetDir, config);
56
+ }
57
+
58
+ const configInfo = [`"${config.projectName}" (${config.stack})`];
59
+ if (config.appType) configInfo.push(`type: ${config.appType}`);
60
+ if (config.mode) configInfo.push(`mode: ${config.mode}`);
61
+ p.log.info(`Reconfiguring ${configInfo.join(", ")}`);
44
62
 
45
63
  if (dryRun) {
46
64
  p.note(JSON.stringify(config, null, 2), "Current Configuration");
@@ -94,6 +112,14 @@ async function main() {
94
112
  deny: settingsObj.permissions?.deny || [],
95
113
  };
96
114
 
115
+ // Apply Agent Teams env var from saved config
116
+ if (settingsObj.env && config.recommended) {
117
+ settingsObj.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = config.recommended
118
+ .agentTeams
119
+ ? "1"
120
+ : "0";
121
+ }
122
+
97
123
  const formatter = FORMATTER_MAP[config.stack] || FORMATTER_MAP.generic;
98
124
  if (settingsObj.hooks?.PostToolUse) {
99
125
  for (const group of settingsObj.hooks.PostToolUse) {
@@ -165,6 +191,6 @@ async function main() {
165
191
  }
166
192
 
167
193
  main().catch((err) => {
168
- p.log.error(`Fatal error: ${err.message}`);
194
+ console.error(`Fatal error: ${err.message}`);
169
195
  process.exit(1);
170
196
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aslomon/effectum",
3
- "version": "0.2.0",
4
- "description": "Autonomous development system for Claude Code describe what you want, get production-ready code",
3
+ "version": "0.3.0",
4
+ "description": "Autonomous development system for Claude Code \u2014 describe what you want, get production-ready code",
5
5
  "bin": {
6
6
  "effectum": "bin/effectum.js"
7
7
  },