@aslomon/effectum 0.4.0 → 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,7 +53,9 @@ const {
53
53
  askLanguage,
54
54
  askAutonomy,
55
55
  showRecommendation,
56
- showCliToolCheck,
56
+ showSystemCheck,
57
+ showInstallPlan,
58
+ showAuthCheck,
57
59
  askSetupMode,
58
60
  askCustomize,
59
61
  askManual,
@@ -62,7 +64,15 @@ const {
62
64
  showSummary,
63
65
  showOutro,
64
66
  } = require("./lib/ui");
65
- const { checkAllTools, formatToolStatus } = require("./lib/cli-tools");
67
+ const {
68
+ checkAllTools,
69
+ checkSystemBasics,
70
+ formatToolStatus,
71
+ categorizeForInstall,
72
+ formatInstallPlan,
73
+ checkAllAuth,
74
+ formatAuthStatus,
75
+ } = require("./lib/cli-tools");
66
76
 
67
77
  // ─── File helpers ─────────────────────────────────────────────────────────────
68
78
 
@@ -598,18 +608,41 @@ Options:
598
608
  process.exit(0);
599
609
  }
600
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
+
601
621
  // CLI tool check (report only, no install in non-interactive mode)
602
622
  const toolCheck = checkAllTools(config.stack);
603
623
  if (toolCheck.missing.length > 0) {
604
- console.log("\n CLI Tool Check:");
624
+ const plan = categorizeForInstall(toolCheck.tools);
625
+ console.log("\n Stack Tools:");
605
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
+ }
606
631
  console.log(
607
632
  `\n ${toolCheck.missing.length} tool(s) not installed. Run installer interactively to install.`,
608
633
  );
609
634
  }
610
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
+
611
644
  // Store tool status in config
612
- config.detectedTools = toolCheck.tools.map((t) => ({
645
+ config.detectedTools = [...sysCheck.tools, ...toolCheck.tools].map((t) => ({
613
646
  key: t.key,
614
647
  installed: t.installed,
615
648
  }));
@@ -699,6 +732,9 @@ Options:
699
732
  process.exit(0);
700
733
  }
701
734
 
735
+ // ── Phase 1: System Basics Check ──────────────────────────────────────────
736
+ await showSystemCheck();
737
+
702
738
  // ── Step 2: Project Basics ────────────────────────────────────────────────
703
739
  const projectName = await askProjectName(detected.projectName);
704
740
  const stack = await askStack(detected.stack);
@@ -726,8 +762,8 @@ Options:
726
762
 
727
763
  showRecommendation(rec);
728
764
 
729
- // ── CLI Tool Check ────────────────────────────────────────────────────────
730
- const cliToolResult = await showCliToolCheck(stack);
765
+ // ── Phase 3: Consolidated Tool Plan ───────────────────────────────────────
766
+ const cliToolResult = await showInstallPlan(stack, installTargetDir);
731
767
 
732
768
  // ── Step 8: Decision ──────────────────────────────────────────────────────
733
769
  const setupMode = await askSetupMode();
@@ -875,7 +911,10 @@ Options:
875
911
  );
876
912
  }
877
913
 
878
- // 9e: Save config
914
+ // 9e: Auth check
915
+ await showAuthCheck(cliToolResult.tools);
916
+
917
+ // 9f: Save config
879
918
  const configPath = writeConfig(installTargetDir, config);
880
919
  configSteps.push({ status: "created", dest: configPath });
881
920
 
@@ -1,113 +1,49 @@
1
1
  /**
2
- * CLI tool definitions, detection, installation, and auth checking.
2
+ * CLI tool detection, installation, and auth checking.
3
3
  *
4
- * Each tool specifies:
5
- * - key/bin: identifier and binary name
6
- * - install: platform-specific install commands (darwin/linux/all)
7
- * - auth/authSetup: commands to check and configure authentication
8
- * - why: human-readable reason for the tool
9
- * - foundation: true if always recommended regardless of stack
10
- * - stacks: array of stack keys where this tool is relevant
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.
11
7
  */
12
8
  "use strict";
13
9
 
14
10
  const { spawnSync } = require("child_process");
15
11
  const os = require("os");
12
+ const { loadToolDefinitions, getSystemBasics } = require("./tool-loader");
16
13
 
17
- // ─── Tool definitions ────────────────────────────────────────────────────────
18
-
19
- const CLI_TOOLS = [
20
- // Foundation (always recommended)
21
- {
22
- key: "git",
23
- bin: "git",
24
- install: {
25
- darwin: "xcode-select --install",
26
- linux: "sudo apt install -y git",
27
- },
28
- auth: "git config user.name && git config user.email",
29
- authSetup:
30
- 'git config --global user.name "Your Name" && git config --global user.email "you@example.com"',
31
- why: "Version control — required for all projects",
32
- foundation: true,
33
- },
34
- {
35
- key: "gh",
36
- bin: "gh",
37
- install: { darwin: "brew install gh", linux: "sudo apt install -y gh" },
38
- auth: "gh auth status",
39
- authSetup: "gh auth login",
40
- why: "GitHub: Issues, PRs, Code Search, CI status",
41
- foundation: true,
42
- },
43
- // Stack-specific
44
- {
45
- key: "supabase",
46
- bin: "supabase",
47
- install: {
48
- darwin: "brew install supabase/tap/supabase",
49
- linux: "npm i -g supabase",
50
- },
51
- auth: "supabase projects list",
52
- authSetup: "supabase login",
53
- why: "Database migrations, type generation, edge functions",
54
- stacks: ["nextjs-supabase"],
55
- },
56
- {
57
- key: "vercel",
58
- bin: "vercel",
59
- install: { all: "npm i -g vercel" },
60
- auth: "vercel whoami",
61
- authSetup: "vercel login",
62
- why: "Deployment to Vercel",
63
- stacks: ["nextjs-supabase"],
64
- },
65
- {
66
- key: "docker",
67
- bin: "docker",
68
- install: {
69
- darwin: "brew install --cask docker",
70
- linux: "sudo apt install -y docker.io",
71
- },
72
- auth: null,
73
- why: "Container management, local dev environment",
74
- stacks: ["python-fastapi", "generic"],
75
- },
76
- {
77
- key: "uv",
78
- bin: "uv",
79
- install: { all: "curl -LsSf https://astral.sh/uv/install.sh | sh" },
80
- auth: null,
81
- why: "Fast Python package management",
82
- stacks: ["python-fastapi"],
83
- },
84
- {
85
- key: "ruff",
86
- bin: "ruff",
87
- install: { all: "pip install ruff" },
88
- auth: null,
89
- why: "Python linting and formatting",
90
- stacks: ["python-fastapi"],
91
- },
92
- {
93
- key: "xcodebuild",
94
- bin: "xcodebuild",
95
- install: { darwin: "xcode-select --install" },
96
- auth: null,
97
- why: "iOS/macOS build toolchain",
98
- stacks: ["swift-ios"],
99
- },
100
- ];
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
+ }
101
24
 
102
25
  // ─── Tool check ──────────────────────────────────────────────────────────────
103
26
 
104
27
  /**
105
28
  * Check if a CLI tool is installed by looking up its binary.
106
- * @param {string} bin - binary name (e.g. "git", "gh")
29
+ * Uses the tool's `check` command if available, otherwise falls back to `which`.
30
+ * @param {object|string} toolOrBin - tool object or binary name
107
31
  * @returns {boolean}
108
32
  */
109
- function checkTool(bin) {
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
+
110
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
+ }
111
47
  const result = spawnSync("which", [bin], {
112
48
  timeout: 5000,
113
49
  stdio: "pipe",
@@ -119,28 +55,30 @@ function checkTool(bin) {
119
55
  }
120
56
  }
121
57
 
58
+ // ─── Tool retrieval ──────────────────────────────────────────────────────────
59
+
122
60
  /**
123
- * Get the relevant tools for a given stack.
61
+ * Get the relevant tools for a given stack, loaded from JSON definitions.
124
62
  * Foundation tools are always included.
125
- * @param {string} stack - stack key (e.g. "nextjs-supabase")
63
+ * @param {string} stack - stack key (e.g., "nextjs-supabase")
64
+ * @param {string} [targetDir] - project directory for community overrides
126
65
  * @returns {Array<object>}
127
66
  */
128
- function getToolsForStack(stack) {
129
- return CLI_TOOLS.filter(
130
- (tool) => tool.foundation || (tool.stacks && tool.stacks.includes(stack)),
131
- );
67
+ function getToolsForStack(stack, targetDir) {
68
+ return loadToolDefinitions(stack, targetDir);
132
69
  }
133
70
 
134
71
  /**
135
72
  * Check all relevant tools for a stack and return status.
136
73
  * @param {string} stack
137
- * @returns {{ tools: Array<{ key: string, bin: string, installed: boolean, why: string, install: object, auth: string|null, authSetup: string|null }>, missing: Array<object>, installed: Array<object> }}
74
+ * @param {string} [targetDir] - project directory for community overrides
75
+ * @returns {{ tools: Array<object>, missing: Array<object>, installed: Array<object> }}
138
76
  */
139
- function checkAllTools(stack) {
140
- const relevant = getToolsForStack(stack);
77
+ function checkAllTools(stack, targetDir) {
78
+ const relevant = getToolsForStack(stack, targetDir);
141
79
  const results = relevant.map((tool) => ({
142
80
  ...tool,
143
- installed: checkTool(tool.bin),
81
+ installed: checkTool(tool),
144
82
  }));
145
83
 
146
84
  return {
@@ -150,17 +88,26 @@ function checkAllTools(stack) {
150
88
  };
151
89
  }
152
90
 
153
- // ─── Tool installation ───────────────────────────────────────────────────────
154
-
155
91
  /**
156
- * Get the platform key for install commands.
157
- * @returns {"darwin"|"linux"}
92
+ * Check system basics (Homebrew, Git, Node.js, Claude Code).
93
+ * @returns {{ tools: Array<object>, missing: Array<object>, installed: Array<object> }}
158
94
  */
159
- function getPlatform() {
160
- const p = os.platform();
161
- return p === "darwin" ? "darwin" : "linux";
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
+ };
162
107
  }
163
108
 
109
+ // ─── Tool installation ───────────────────────────────────────────────────────
110
+
164
111
  /**
165
112
  * Get the install command for a tool on the current platform.
166
113
  * @param {object} tool
@@ -202,20 +149,51 @@ function installTool(tool) {
202
149
  }
203
150
  }
204
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
+
205
175
  // ─── Auth checking ───────────────────────────────────────────────────────────
206
176
 
207
177
  /**
208
178
  * Check if a tool is authenticated.
179
+ * Supports both legacy format (auth as string) and new format (auth as object).
209
180
  * @param {object} tool
210
181
  * @returns {{ authenticated: boolean, needsAuth: boolean }}
211
182
  */
212
183
  function checkAuth(tool) {
213
- if (!tool.auth) {
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) {
214
192
  return { authenticated: true, needsAuth: false };
215
193
  }
216
194
 
217
195
  try {
218
- const result = spawnSync("bash", ["-c", tool.auth], {
196
+ const result = spawnSync("bash", ["-c", authCheck], {
219
197
  timeout: 10000,
220
198
  stdio: "pipe",
221
199
  encoding: "utf8",
@@ -229,6 +207,50 @@ function checkAuth(tool) {
229
207
  }
230
208
  }
231
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
+
232
254
  // ─── Formatting helpers ──────────────────────────────────────────────────────
233
255
 
234
256
  /**
@@ -240,7 +262,8 @@ function formatToolStatus(tools) {
240
262
  return tools
241
263
  .map((t) => {
242
264
  const icon = t.installed ? "\u2705" : "\u274C";
243
- return ` ${icon} ${t.key} ${t.why}`;
265
+ const name = t.displayName || t.key;
266
+ return ` ${icon} ${name} — ${t.why}`;
244
267
  })
245
268
  .join("\n");
246
269
  }
@@ -254,8 +277,61 @@ function formatInstallInstructions(missing) {
254
277
  const platform = getPlatform();
255
278
  return missing
256
279
  .map((t) => {
257
- const cmd = t.install[platform] || t.install.all || "N/A";
258
- return ` ${t.key}: ${cmd}`;
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;
259
335
  })
260
336
  .join("\n");
261
337
  }
@@ -268,21 +344,28 @@ function formatInstallInstructions(missing) {
268
344
  function buildAvailableToolsSection(tools) {
269
345
  const lines = tools.map((t) => {
270
346
  const status = t.installed ? "installed" : "not installed";
271
- return `- **${t.key}** (${status}): ${t.why}`;
347
+ const name = t.displayName || t.key;
348
+ return `- **${name}** (${status}): ${t.why}`;
272
349
  });
273
350
  return lines.join("\n");
274
351
  }
275
352
 
276
353
  module.exports = {
277
- CLI_TOOLS,
278
354
  checkTool,
279
355
  getToolsForStack,
280
356
  checkAllTools,
357
+ checkSystemBasics,
281
358
  getPlatform,
282
359
  getInstallCommand,
283
360
  installTool,
361
+ categorizeForInstall,
284
362
  checkAuth,
363
+ getAuthSetup,
364
+ getAuthUrl,
365
+ checkAllAuth,
285
366
  formatToolStatus,
286
367
  formatInstallInstructions,
368
+ formatInstallPlan,
369
+ formatAuthStatus,
287
370
  buildAvailableToolsSection,
288
371
  };