@aslomon/effectum 0.2.1 → 0.3.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/lib/ui.js CHANGED
@@ -1,15 +1,24 @@
1
1
  /**
2
2
  * Shared @clack/prompts helpers and display utilities.
3
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
4
8
  */
5
9
  "use strict";
6
10
 
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");
13
22
 
14
23
  /** @type {import("@clack/prompts")} */
15
24
  let p;
@@ -26,7 +35,7 @@ async function initClack() {
26
35
  }
27
36
 
28
37
  /**
29
- * Get the loaded clack instance (for use in install.js / reconfigure.js).
38
+ * Get the loaded clack instance.
30
39
  * @returns {import("@clack/prompts")}
31
40
  */
32
41
  function getClack() {
@@ -38,7 +47,7 @@ function getClack() {
38
47
  * Print the Effectum banner via clack intro.
39
48
  */
40
49
  function printBanner() {
41
- p.intro("EFFECTUM — Autonomous development system for Claude Code");
50
+ p.intro("EFFECTUM — Intelligent Setup for Claude Code");
42
51
  }
43
52
 
44
53
  /**
@@ -52,6 +61,35 @@ function handleCancel(value) {
52
61
  }
53
62
  }
54
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
+
55
93
  /**
56
94
  * Ask for project name with a detected default.
57
95
  * @param {string} detected
@@ -89,6 +127,47 @@ async function askStack(detected) {
89
127
  return value;
90
128
  }
91
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
+
92
171
  /**
93
172
  * Ask for communication language.
94
173
  * @returns {Promise<{ language: string, customLanguage?: string }>}
@@ -120,13 +199,15 @@ async function askLanguage() {
120
199
  return { language: value };
121
200
  }
122
201
 
202
+ // ─── Step 6: Autonomy ───────────────────────────────────────────────────────
203
+
123
204
  /**
124
205
  * Ask for autonomy level.
125
206
  * @returns {Promise<string>}
126
207
  */
127
208
  async function askAutonomy() {
128
209
  const value = await p.select({
129
- message: "Autonomy level",
210
+ message: "How should Claude work?",
130
211
  options: AUTONOMY_CHOICES.map((c) => ({
131
212
  value: c.value,
132
213
  label: c.label,
@@ -138,6 +219,232 @@ async function askAutonomy() {
138
219
  return value;
139
220
  }
140
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
+
141
448
  /**
142
449
  * Ask which MCP servers to install via multi-select.
143
450
  * @returns {Promise<string[]>}
@@ -192,6 +499,8 @@ async function askGitBranch() {
192
499
  return { create: true, name };
193
500
  }
194
501
 
502
+ // ─── Display helpers ────────────────────────────────────────────────────────
503
+
195
504
  /**
196
505
  * Display a summary note.
197
506
  * @param {object} config
@@ -199,17 +508,29 @@ async function askGitBranch() {
199
508
  */
200
509
  function showSummary(config, files) {
201
510
  const lines = [
202
- `Project: ${config.projectName}`,
203
- `Stack: ${config.stack}`,
204
- `Language: ${config.language}`,
205
- `Autonomy: ${config.autonomyLevel}`,
206
- `Pkg Mgr: ${config.packageManager}`,
207
- `Formatter: ${config.formatter}`,
208
- `MCP: ${(config.mcpServers || []).join(", ") || "none"}`,
209
- "",
210
- `Files created/updated:`,
211
- ...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"}`,
212
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
+
213
534
  p.note(lines.join("\n"), "Configuration Summary");
214
535
  }
215
536
 
@@ -219,9 +540,7 @@ function showSummary(config, files) {
219
540
  */
220
541
  function showOutro(isGlobal) {
221
542
  if (isGlobal) {
222
- p.outro(
223
- "Effectum ready! In any project, run: npx @aslomon/effectum init",
224
- );
543
+ p.outro("Effectum ready! In any project, run: npx @aslomon/effectum init");
225
544
  } else {
226
545
  p.outro(
227
546
  "Effectum ready! Open Claude Code here and start building. Try /plan or /prd:new",
@@ -234,13 +553,23 @@ module.exports = {
234
553
  getClack,
235
554
  printBanner,
236
555
  handleCancel,
556
+ // Step prompts
557
+ askScope,
237
558
  askProjectName,
238
559
  askStack,
560
+ askAppType,
561
+ askDescription,
239
562
  askLanguage,
240
563
  askAutonomy,
564
+ showRecommendation,
565
+ askSetupMode,
566
+ askCustomize,
567
+ askManual,
568
+ // Legacy / utility prompts
241
569
  askMcpServers,
242
570
  askPlaywright,
243
571
  askGitBranch,
572
+ // Display
244
573
  showSummary,
245
574
  showOutro,
246
575
  };
@@ -2,6 +2,7 @@
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
 
@@ -13,12 +14,12 @@ const {
13
14
  buildSubstitutionMap,
14
15
  renderTemplate,
15
16
  findTemplatePath,
16
- findRemainingPlaceholders,
17
- substituteAll,
18
17
  } = require("./lib/template");
19
18
  const { AUTONOMY_MAP, FORMATTER_MAP } = require("./lib/constants");
20
19
  const { ensureDir, deepMerge, findRepoRoot } = require("./lib/utils");
21
20
  const { initClack } = require("./lib/ui");
21
+ const { recommend } = require("./lib/recommendation");
22
+ const { writeConfig } = require("./lib/config");
22
23
 
23
24
  // ─── Main ─────────────────────────────────────────────────────────────────
24
25
 
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aslomon/effectum",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
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"