@aslomon/effectum 0.3.4 → 0.5.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/install.js CHANGED
@@ -53,6 +53,9 @@ const {
53
53
  askLanguage,
54
54
  askAutonomy,
55
55
  showRecommendation,
56
+ showSystemCheck,
57
+ showInstallPlan,
58
+ showAuthCheck,
56
59
  askSetupMode,
57
60
  askCustomize,
58
61
  askManual,
@@ -61,6 +64,15 @@ const {
61
64
  showSummary,
62
65
  showOutro,
63
66
  } = require("./lib/ui");
67
+ const {
68
+ checkAllTools,
69
+ checkSystemBasics,
70
+ formatToolStatus,
71
+ categorizeForInstall,
72
+ formatInstallPlan,
73
+ checkAllAuth,
74
+ formatAuthStatus,
75
+ } = require("./lib/cli-tools");
64
76
 
65
77
  // ─── File helpers ─────────────────────────────────────────────────────────────
66
78
 
@@ -343,7 +355,11 @@ function installRecommendedAgents(targetDir, repoRoot, recommendedAgents) {
343
355
  const agentsDest = path.join(targetDir, ".claude", "agents");
344
356
  const steps = [];
345
357
 
346
- if (!fs.existsSync(agentsSrc) || !recommendedAgents || recommendedAgents.length === 0) {
358
+ if (
359
+ !fs.existsSync(agentsSrc) ||
360
+ !recommendedAgents ||
361
+ recommendedAgents.length === 0
362
+ ) {
347
363
  return steps;
348
364
  }
349
365
 
@@ -592,6 +608,45 @@ Options:
592
608
  process.exit(0);
593
609
  }
594
610
 
611
+ // System basics check (report only in non-interactive mode)
612
+ const sysCheck = checkSystemBasics();
613
+ if (sysCheck.missing.length > 0) {
614
+ console.log("\n System Basics:");
615
+ console.log(formatToolStatus(sysCheck.tools));
616
+ console.log(
617
+ `\n ${sysCheck.missing.length} system tool(s) not installed. Run installer interactively to install.`,
618
+ );
619
+ }
620
+
621
+ // CLI tool check (report only, no install in non-interactive mode)
622
+ const toolCheck = checkAllTools(config.stack);
623
+ if (toolCheck.missing.length > 0) {
624
+ const plan = categorizeForInstall(toolCheck.tools);
625
+ console.log("\n Stack Tools:");
626
+ console.log(formatToolStatus(toolCheck.tools));
627
+ if (plan.autoInstall.length > 0 || plan.manual.length > 0) {
628
+ console.log("\n Installation Plan:");
629
+ console.log(formatInstallPlan(plan));
630
+ }
631
+ console.log(
632
+ `\n ${toolCheck.missing.length} tool(s) not installed. Run installer interactively to install.`,
633
+ );
634
+ }
635
+
636
+ // Auth check (report only in non-interactive mode)
637
+ const authResults = checkAllAuth(toolCheck.tools);
638
+ const unauthenticated = authResults.filter((t) => !t.authenticated);
639
+ if (unauthenticated.length > 0) {
640
+ console.log("\n Auth Status:");
641
+ console.log(formatAuthStatus(authResults));
642
+ }
643
+
644
+ // Store tool status in config
645
+ config.detectedTools = [...sysCheck.tools, ...toolCheck.tools].map((t) => ({
646
+ key: t.key,
647
+ installed: t.installed,
648
+ }));
649
+
595
650
  // Install base files
596
651
  installBaseFiles(targetDir, repoRoot, isGlobal);
597
652
 
@@ -609,9 +664,13 @@ Options:
609
664
  }
610
665
 
611
666
  // MCP servers — always install recommended MCPs (or explicit --with-mcp)
612
- const mcpKeys = config.mcpServers || (config.recommended ? config.recommended.mcps : []) || [];
667
+ const mcpKeys =
668
+ config.mcpServers ||
669
+ (config.recommended ? config.recommended.mcps : []) ||
670
+ [];
613
671
  if (mcpKeys.length > 0 || args.withMcp) {
614
- const keysToInstall = mcpKeys.length > 0 ? mcpKeys : MCP_SERVERS.map((s) => s.key);
672
+ const keysToInstall =
673
+ mcpKeys.length > 0 ? mcpKeys : MCP_SERVERS.map((s) => s.key);
615
674
  const mcpResults = installMcpServers(keysToInstall);
616
675
  const settingsPath = isGlobal
617
676
  ? path.join(homeClaudeDir, "settings.json")
@@ -673,6 +732,9 @@ Options:
673
732
  process.exit(0);
674
733
  }
675
734
 
735
+ // ── Phase 1: System Basics Check ──────────────────────────────────────────
736
+ await showSystemCheck();
737
+
676
738
  // ── Step 2: Project Basics ────────────────────────────────────────────────
677
739
  const projectName = await askProjectName(detected.projectName);
678
740
  const stack = await askStack(detected.stack);
@@ -700,6 +762,9 @@ Options:
700
762
 
701
763
  showRecommendation(rec);
702
764
 
765
+ // ── Phase 3: Consolidated Tool Plan ───────────────────────────────────────
766
+ const cliToolResult = await showInstallPlan(stack, installTargetDir);
767
+
703
768
  // ── Step 8: Decision ──────────────────────────────────────────────────────
704
769
  const setupMode = await askSetupMode();
705
770
 
@@ -733,6 +798,10 @@ Options:
733
798
  packageManager: detected.packageManager,
734
799
  formatter: formatterDef.name,
735
800
  mcpServers: finalSetup.mcps,
801
+ detectedTools: cliToolResult.tools.map((t) => ({
802
+ key: t.key,
803
+ installed: t.installed,
804
+ })),
736
805
  playwrightBrowsers: wantPlaywright,
737
806
  installScope: "local",
738
807
  recommended: {
@@ -804,7 +873,11 @@ Options:
804
873
  if (recAgents.length > 0) {
805
874
  const sAgents = p.spinner();
806
875
  sAgents.start("Installing agent specializations...");
807
- const agentSteps = installRecommendedAgents(installTargetDir, repoRoot, recAgents);
876
+ const agentSteps = installRecommendedAgents(
877
+ installTargetDir,
878
+ repoRoot,
879
+ recAgents,
880
+ );
808
881
  const agentCount = agentSteps.filter((s) => s.status === "created").length;
809
882
  sAgents.stop(`${agentCount} agent specializations installed`);
810
883
  configSteps.push(...agentSteps);
@@ -838,7 +911,10 @@ Options:
838
911
  );
839
912
  }
840
913
 
841
- // 9e: Save config
914
+ // 9e: Auth check
915
+ await showAuthCheck(cliToolResult.tools);
916
+
917
+ // 9f: Save config
842
918
  const configPath = writeConfig(installTargetDir, config);
843
919
  configSteps.push({ status: "created", dest: configPath });
844
920
 
@@ -0,0 +1,371 @@
1
+ /**
2
+ * CLI tool detection, installation, and auth checking.
3
+ *
4
+ * Tool definitions are loaded dynamically from JSON files in system/tools/
5
+ * via the tool-loader module. This module provides the runtime operations:
6
+ * check, install, auth, and formatting.
7
+ */
8
+ "use strict";
9
+
10
+ const { spawnSync } = require("child_process");
11
+ const os = require("os");
12
+ const { loadToolDefinitions, getSystemBasics } = require("./tool-loader");
13
+
14
+ // ─── Platform ────────────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Get the platform key for install commands.
18
+ * @returns {"darwin"|"linux"}
19
+ */
20
+ function getPlatform() {
21
+ const p = os.platform();
22
+ return p === "darwin" ? "darwin" : "linux";
23
+ }
24
+
25
+ // ─── Tool check ──────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Check if a CLI tool is installed by looking up its binary.
29
+ * Uses the tool's `check` command if available, otherwise falls back to `which`.
30
+ * @param {object|string} toolOrBin - tool object or binary name
31
+ * @returns {boolean}
32
+ */
33
+ function checkTool(toolOrBin) {
34
+ const bin = typeof toolOrBin === "string" ? toolOrBin : toolOrBin.bin;
35
+ const checkCmd =
36
+ typeof toolOrBin === "object" && toolOrBin.check ? toolOrBin.check : null;
37
+
38
+ try {
39
+ if (checkCmd) {
40
+ const result = spawnSync("bash", ["-c", checkCmd], {
41
+ timeout: 5000,
42
+ stdio: "pipe",
43
+ encoding: "utf8",
44
+ });
45
+ return result.status === 0;
46
+ }
47
+ const result = spawnSync("which", [bin], {
48
+ timeout: 5000,
49
+ stdio: "pipe",
50
+ encoding: "utf8",
51
+ });
52
+ return result.status === 0 && result.stdout.trim().length > 0;
53
+ } catch (_) {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ // ─── Tool retrieval ──────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Get the relevant tools for a given stack, loaded from JSON definitions.
62
+ * Foundation tools are always included.
63
+ * @param {string} stack - stack key (e.g., "nextjs-supabase")
64
+ * @param {string} [targetDir] - project directory for community overrides
65
+ * @returns {Array<object>}
66
+ */
67
+ function getToolsForStack(stack, targetDir) {
68
+ return loadToolDefinitions(stack, targetDir);
69
+ }
70
+
71
+ /**
72
+ * Check all relevant tools for a stack and return status.
73
+ * @param {string} stack
74
+ * @param {string} [targetDir] - project directory for community overrides
75
+ * @returns {{ tools: Array<object>, missing: Array<object>, installed: Array<object> }}
76
+ */
77
+ function checkAllTools(stack, targetDir) {
78
+ const relevant = getToolsForStack(stack, targetDir);
79
+ const results = relevant.map((tool) => ({
80
+ ...tool,
81
+ installed: checkTool(tool),
82
+ }));
83
+
84
+ return {
85
+ tools: results,
86
+ missing: results.filter((t) => !t.installed),
87
+ installed: results.filter((t) => t.installed),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Check system basics (Homebrew, Git, Node.js, Claude Code).
93
+ * @returns {{ tools: Array<object>, missing: Array<object>, installed: Array<object> }}
94
+ */
95
+ function checkSystemBasics() {
96
+ const basics = getSystemBasics();
97
+ const results = basics.map((tool) => ({
98
+ ...tool,
99
+ installed: checkTool(tool),
100
+ }));
101
+
102
+ return {
103
+ tools: results,
104
+ missing: results.filter((t) => !t.installed),
105
+ installed: results.filter((t) => t.installed),
106
+ };
107
+ }
108
+
109
+ // ─── Tool installation ───────────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Get the install command for a tool on the current platform.
113
+ * @param {object} tool
114
+ * @returns {string|null}
115
+ */
116
+ function getInstallCommand(tool) {
117
+ if (!tool.install) return null;
118
+ const platform = getPlatform();
119
+ return tool.install[platform] || tool.install.all || null;
120
+ }
121
+
122
+ /**
123
+ * Install a single tool using its platform-appropriate command.
124
+ * @param {object} tool
125
+ * @returns {{ ok: boolean, command: string|null, error?: string }}
126
+ */
127
+ function installTool(tool) {
128
+ const command = getInstallCommand(tool);
129
+ if (!command) {
130
+ return {
131
+ ok: false,
132
+ command: null,
133
+ error: "No install command for this platform",
134
+ };
135
+ }
136
+
137
+ try {
138
+ const result = spawnSync("bash", ["-c", command], {
139
+ timeout: 120000,
140
+ stdio: "pipe",
141
+ encoding: "utf8",
142
+ });
143
+ if (result.status === 0) {
144
+ return { ok: true, command };
145
+ }
146
+ return { ok: false, command, error: result.stderr || "Install failed" };
147
+ } catch (err) {
148
+ return { ok: false, command, error: err.message };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Categorize tools into auto-installable and manual-only groups.
154
+ * @param {Array<object>} tools - tools with `installed` status
155
+ * @returns {{ autoInstall: Array<object>, manual: Array<object> }}
156
+ */
157
+ function categorizeForInstall(tools) {
158
+ const missing = tools.filter((t) => !t.installed);
159
+ const platform = getPlatform();
160
+
161
+ return {
162
+ autoInstall: missing.filter((t) => {
163
+ if (t.autoInstall === false) return false;
164
+ const cmd = t.install ? t.install[platform] || t.install.all : null;
165
+ return !!cmd;
166
+ }),
167
+ manual: missing.filter((t) => {
168
+ if (t.autoInstall === false) return true;
169
+ const cmd = t.install ? t.install[platform] || t.install.all : null;
170
+ return !cmd;
171
+ }),
172
+ };
173
+ }
174
+
175
+ // ─── Auth checking ───────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * Check if a tool is authenticated.
179
+ * Supports both legacy format (auth as string) and new format (auth as object).
180
+ * @param {object} tool
181
+ * @returns {{ authenticated: boolean, needsAuth: boolean }}
182
+ */
183
+ function checkAuth(tool) {
184
+ const authCheck =
185
+ typeof tool.auth === "object" && tool.auth !== null
186
+ ? tool.auth.check
187
+ : typeof tool.auth === "string"
188
+ ? tool.auth
189
+ : null;
190
+
191
+ if (!authCheck) {
192
+ return { authenticated: true, needsAuth: false };
193
+ }
194
+
195
+ try {
196
+ const result = spawnSync("bash", ["-c", authCheck], {
197
+ timeout: 10000,
198
+ stdio: "pipe",
199
+ encoding: "utf8",
200
+ });
201
+ return {
202
+ authenticated: result.status === 0,
203
+ needsAuth: true,
204
+ };
205
+ } catch (_) {
206
+ return { authenticated: false, needsAuth: true };
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Get the auth setup command for a tool.
212
+ * @param {object} tool
213
+ * @returns {string|null}
214
+ */
215
+ function getAuthSetup(tool) {
216
+ if (typeof tool.auth === "object" && tool.auth !== null) {
217
+ return tool.auth.setup || null;
218
+ }
219
+ return tool.authSetup || null;
220
+ }
221
+
222
+ /**
223
+ * Get the auth URL for a tool (for creating tokens).
224
+ * @param {object} tool
225
+ * @returns {string|null}
226
+ */
227
+ function getAuthUrl(tool) {
228
+ if (typeof tool.auth === "object" && tool.auth !== null) {
229
+ return tool.auth.url || null;
230
+ }
231
+ return null;
232
+ }
233
+
234
+ /**
235
+ * Check auth status for all installed tools that need auth.
236
+ * @param {Array<object>} tools - tools with `installed` status
237
+ * @returns {Array<object>} - tools with auth status added
238
+ */
239
+ function checkAllAuth(tools) {
240
+ return tools
241
+ .filter((t) => t.installed)
242
+ .map((tool) => {
243
+ const authResult = checkAuth(tool);
244
+ return {
245
+ ...tool,
246
+ ...authResult,
247
+ authSetupCmd: getAuthSetup(tool),
248
+ authUrl: getAuthUrl(tool),
249
+ };
250
+ })
251
+ .filter((t) => t.needsAuth);
252
+ }
253
+
254
+ // ─── Formatting helpers ──────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Generate a human-readable tools status summary.
258
+ * @param {Array<object>} tools - tool check results
259
+ * @returns {string}
260
+ */
261
+ function formatToolStatus(tools) {
262
+ return tools
263
+ .map((t) => {
264
+ const icon = t.installed ? "\u2705" : "\u274C";
265
+ const name = t.displayName || t.key;
266
+ return ` ${icon} ${name} — ${t.why}`;
267
+ })
268
+ .join("\n");
269
+ }
270
+
271
+ /**
272
+ * Generate install instructions for missing tools.
273
+ * @param {Array<object>} missing - missing tools
274
+ * @returns {string}
275
+ */
276
+ function formatInstallInstructions(missing) {
277
+ const platform = getPlatform();
278
+ return missing
279
+ .map((t) => {
280
+ const cmd = t.install
281
+ ? t.install[platform] || t.install.all || "N/A"
282
+ : "N/A";
283
+ const name = t.displayName || t.key;
284
+ return ` ${name}: ${cmd}`;
285
+ })
286
+ .join("\n");
287
+ }
288
+
289
+ /**
290
+ * Format a consolidated installation plan.
291
+ * @param {{ autoInstall: Array<object>, manual: Array<object> }} plan
292
+ * @returns {string}
293
+ */
294
+ function formatInstallPlan(plan) {
295
+ const lines = [];
296
+
297
+ if (plan.autoInstall.length > 0) {
298
+ lines.push("Will install:");
299
+ for (const tool of plan.autoInstall) {
300
+ const name = tool.displayName || tool.key;
301
+ const cmd = getInstallCommand(tool);
302
+ lines.push(` ${name} — ${cmd}`);
303
+ }
304
+ }
305
+
306
+ if (plan.manual.length > 0) {
307
+ if (lines.length > 0) lines.push("");
308
+ lines.push("Manual setup needed:");
309
+ for (const tool of plan.manual) {
310
+ const name = tool.displayName || tool.key;
311
+ const url = tool.manualUrl || getInstallCommand(tool) || "see docs";
312
+ lines.push(` ${name} — ${url}`);
313
+ }
314
+ }
315
+
316
+ return lines.join("\n");
317
+ }
318
+
319
+ /**
320
+ * Format auth status for display.
321
+ * @param {Array<object>} authResults
322
+ * @returns {string}
323
+ */
324
+ function formatAuthStatus(authResults) {
325
+ return authResults
326
+ .map((t) => {
327
+ const icon = t.authenticated ? "\u2705" : "\u274C";
328
+ const name = t.displayName || t.key;
329
+ let line = ` ${icon} ${name}`;
330
+ if (!t.authenticated && t.authSetupCmd) {
331
+ line += ` — run: ${t.authSetupCmd}`;
332
+ if (t.authUrl) line += ` (${t.authUrl})`;
333
+ }
334
+ return line;
335
+ })
336
+ .join("\n");
337
+ }
338
+
339
+ /**
340
+ * Build the AVAILABLE_TOOLS section content for CLAUDE.md.
341
+ * @param {Array<object>} tools - tool check results
342
+ * @returns {string}
343
+ */
344
+ function buildAvailableToolsSection(tools) {
345
+ const lines = tools.map((t) => {
346
+ const status = t.installed ? "installed" : "not installed";
347
+ const name = t.displayName || t.key;
348
+ return `- **${name}** (${status}): ${t.why}`;
349
+ });
350
+ return lines.join("\n");
351
+ }
352
+
353
+ module.exports = {
354
+ checkTool,
355
+ getToolsForStack,
356
+ checkAllTools,
357
+ checkSystemBasics,
358
+ getPlatform,
359
+ getInstallCommand,
360
+ installTool,
361
+ categorizeForInstall,
362
+ checkAuth,
363
+ getAuthSetup,
364
+ getAuthUrl,
365
+ checkAllAuth,
366
+ formatToolStatus,
367
+ formatInstallInstructions,
368
+ formatInstallPlan,
369
+ formatAuthStatus,
370
+ buildAvailableToolsSection,
371
+ };
@@ -94,6 +94,16 @@ const SUBAGENT_SPECS = [
94
94
  label: "Code Reviewer",
95
95
  tags: ["testing-heavy", "docs-needed"],
96
96
  },
97
+ {
98
+ key: "mobile-developer",
99
+ label: "Mobile Developer",
100
+ tags: ["native-ui", "frontend-heavy", "swift"],
101
+ },
102
+ {
103
+ key: "data-engineer",
104
+ label: "Data Engineer",
105
+ tags: ["data-pipeline", "compute-heavy", "analytics"],
106
+ },
97
107
  ];
98
108
 
99
109
  /**
@@ -116,7 +126,7 @@ const STACK_SUBAGENTS = {
116
126
  "test-automator",
117
127
  "api-designer",
118
128
  ],
119
- "swift-ios": ["ui-designer", "test-automator"],
129
+ "swift-ios": ["ui-designer", "test-automator", "mobile-developer"],
120
130
  generic: ["debugger", "test-automator"],
121
131
  };
122
132
 
@@ -9,6 +9,7 @@ const fs = require("fs");
9
9
  const path = require("path");
10
10
  const { FORMATTER_MAP } = require("./constants");
11
11
  const { LANGUAGE_INSTRUCTIONS } = require("./languages");
12
+ const { getToolsForStack, checkTool } = require("./cli-tools");
12
13
 
13
14
  /**
14
15
  * Build a substitution map from user config and parsed stack sections.
@@ -23,6 +24,18 @@ function buildSubstitutionMap(config, stackSections) {
23
24
  config.customLanguage ||
24
25
  LANGUAGE_INSTRUCTIONS.english;
25
26
 
27
+ // Build AVAILABLE_TOOLS section from detected CLI tools
28
+ const tools = getToolsForStack(config.stack);
29
+ const toolLines = tools.map((t) => {
30
+ const installed = checkTool(t.bin);
31
+ const status = installed ? "installed" : "not installed";
32
+ return `- **${t.key}** (${status}): ${t.why}`;
33
+ });
34
+ const availableTools =
35
+ toolLines.length > 0
36
+ ? toolLines.join("\n")
37
+ : "No CLI tools configured. Run the installer to detect and configure tools.";
38
+
26
39
  return {
27
40
  PROJECT_NAME: config.projectName,
28
41
  LANGUAGE: langInstruction,
@@ -39,6 +52,7 @@ function buildSubstitutionMap(config, stackSections) {
39
52
  PACKAGE_MANAGER: config.packageManager,
40
53
  TOOL_SPECIFIC_GUARDRAILS:
41
54
  stackSections.TOOL_SPECIFIC_GUARDRAILS || "[Not configured]",
55
+ AVAILABLE_TOOLS: availableTools,
42
56
  };
43
57
  }
44
58