claudeboard 2.15.2 → 2.15.4

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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * claude-resolver.js — Cross-platform Claude Code CLI detection
3
+ *
4
+ * Problem: On Windows, Claude Desktop installs a `claude.exe` that shadows
5
+ * the Claude Code CLI installed via `npm install -g @anthropic-ai/claude-code`.
6
+ * `which`/`where` may return the Desktop binary instead of the CLI.
7
+ *
8
+ * Strategy:
9
+ * 1. Honor CLAUDE_CODE_PATH env var (explicit override)
10
+ * 2. npm-based detection (most reliable — finds the npm-installed binary directly)
11
+ * 3. which/where — but validate it's not the Desktop app
12
+ * 4. Hardcoded platform-specific fallback paths
13
+ */
14
+
15
+ import { execSync } from "child_process";
16
+ import path from "path";
17
+ import fs from "fs";
18
+
19
+ const isWin = process.platform === "win32";
20
+ const pathSep = isWin ? ";" : ":";
21
+
22
+ /**
23
+ * Returns true if the candidate path looks like the Claude Code CLI
24
+ * and NOT the Claude Desktop app binary.
25
+ */
26
+ function isClaudeCodePath(p) {
27
+ if (!p) return false;
28
+ const lower = p.toLowerCase().replace(/\\/g, "/");
29
+ // Desktop app on Windows: usually under AppData/Local/AnthropicClaude/
30
+ if (lower.includes("anthropicclaude")) return false;
31
+ if (lower.includes("anthropic claude")) return false;
32
+ // Desktop app on macOS: /Applications/Claude.app/...
33
+ if (lower.includes("claude.app/")) return false;
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * Resolves the path to the Claude Code CLI binary.
39
+ * Handles Windows / macOS / Linux, and the Desktop-vs-CLI shadowing conflict.
40
+ */
41
+ export function resolveClaudePath() {
42
+ // 1. Explicit override always wins
43
+ if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
44
+
45
+ // 2. npm global bin detection — bypasses PATH shadowing entirely
46
+ try {
47
+ const npmRoot = execSync("npm root -g", { stdio: "pipe", timeout: 5000 })
48
+ .toString().trim();
49
+ // npmRoot: /usr/local/lib/node_modules or C:\Users\<user>\AppData\Roaming\npm\node_modules
50
+ // The global bin dir is one level up from node_modules
51
+ const npmBinDir = path.dirname(npmRoot);
52
+ const candidates = isWin
53
+ ? [
54
+ path.join(npmBinDir, "claude.cmd"),
55
+ path.join(npmBinDir, "claude"),
56
+ ]
57
+ : [path.join(npmBinDir, "claude")];
58
+ for (const c of candidates) {
59
+ if (fs.existsSync(c)) return c;
60
+ }
61
+ } catch {}
62
+
63
+ // 3. which / where — validate it's not the Desktop app
64
+ try {
65
+ const raw = execSync(isWin ? "where claude" : "which claude", {
66
+ stdio: "pipe",
67
+ timeout: 5000,
68
+ }).toString().trim();
69
+ // `where` on Windows may return multiple lines; take the first valid one
70
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
71
+ for (const candidate of lines) {
72
+ if (isClaudeCodePath(candidate)) return candidate;
73
+ }
74
+ } catch {}
75
+
76
+ // 4. Hardcoded platform-specific fallback paths
77
+ if (isWin) {
78
+ const appData = process.env.APPDATA || "";
79
+ for (const p of [
80
+ path.join(appData, "npm", "claude.cmd"),
81
+ path.join(appData, "npm", "claude"),
82
+ ]) {
83
+ if (fs.existsSync(p)) return p;
84
+ }
85
+ } else {
86
+ for (const p of [
87
+ "/opt/homebrew/bin/claude",
88
+ "/usr/local/bin/claude",
89
+ `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
90
+ `${process.env.HOME}/.npm-global/bin/claude`,
91
+ ]) {
92
+ try {
93
+ execSync(`test -f "${p}"`, { stdio: "pipe" });
94
+ return p;
95
+ } catch {}
96
+ }
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Builds a cross-platform environment for subprocesses so that node/npm/npx
104
+ * are always resolvable inside Claude Code's shell.
105
+ * Also strips ANTHROPIC_API_KEY so Claude uses the user's subscription.
106
+ */
107
+ export function buildEnv() {
108
+ // Use the current process's node binary dir — reliable on all platforms
109
+ const nodeBinDir = path.dirname(process.execPath);
110
+
111
+ const extraPaths = isWin
112
+ ? [
113
+ process.env.APPDATA ? path.join(process.env.APPDATA, "npm") : "",
114
+ process.env.LOCALAPPDATA
115
+ ? path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps")
116
+ : "",
117
+ ]
118
+ : [
119
+ "/opt/homebrew/bin",
120
+ "/opt/homebrew/sbin",
121
+ "/usr/local/bin",
122
+ "/usr/bin",
123
+ "/bin",
124
+ `${process.env.HOME}/.npm-global/bin`,
125
+ `${process.env.HOME}/.nvm/versions/node/current/bin`,
126
+ ];
127
+
128
+ const pathParts = [
129
+ process.env.PATH || "",
130
+ nodeBinDir,
131
+ ...extraPaths,
132
+ ].filter(Boolean);
133
+
134
+ const fullPath = [
135
+ ...new Set(pathParts.join(pathSep).split(pathSep).filter(Boolean)),
136
+ ].join(pathSep);
137
+
138
+ const env = { ...process.env, PATH: fullPath };
139
+ if (!isWin) env.HOME = process.env.HOME;
140
+ // Remove API key so Claude Code uses the Claude subscription (not API credits)
141
+ delete env.ANTHROPIC_API_KEY;
142
+ return env;
143
+ }
144
+
145
+ /**
146
+ * Returns a human-readable installation hint for the current platform.
147
+ */
148
+ export function installHint() {
149
+ if (isWin) {
150
+ return [
151
+ "Claude Code CLI not found or shadowed by Claude Desktop.",
152
+ "",
153
+ "Install the CLI: npm install -g @anthropic-ai/claude-code",
154
+ "Then run: claude",
155
+ "",
156
+ "If already installed, set the explicit path to avoid conflicts:",
157
+ " set CLAUDE_CODE_PATH=C:\\Users\\<you>\\AppData\\Roaming\\npm\\claude.cmd",
158
+ " claudeboard run ...",
159
+ ].join("\n");
160
+ }
161
+ return [
162
+ "Claude Code CLI not found.",
163
+ "",
164
+ "Install: npm install -g @anthropic-ai/claude-code",
165
+ "Then run: claude",
166
+ ].join("\n");
167
+ }
@@ -8,47 +8,7 @@
8
8
 
9
9
  import { query } from "@anthropic-ai/claude-agent-sdk";
10
10
  import { startTask, completeTask, failTask, addLog } from "./board-client.js";
11
- import { execSync } from "child_process";
12
-
13
- // ── Resolve the global `claude` binary path at startup ────────────────────────
14
- function resolveClaudePath() {
15
- if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
16
- try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
17
- for (const p of [
18
- "/opt/homebrew/bin/claude",
19
- "/usr/local/bin/claude",
20
- `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
21
- `${process.env.HOME}/.npm-global/bin/claude`,
22
- ]) {
23
- try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
24
- }
25
- return null;
26
- }
27
-
28
- // ── Build a full PATH so node/npm/npx are found inside the subprocess ─────────
29
- // Exit code 127 = "command not found" — happens when PATH is stripped for global pkgs
30
- function buildEnv() {
31
- let nodeBinDir = "";
32
- try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
33
-
34
- const pathParts = [
35
- process.env.PATH || "",
36
- nodeBinDir,
37
- "/opt/homebrew/bin",
38
- "/opt/homebrew/sbin",
39
- "/usr/local/bin",
40
- "/usr/bin",
41
- "/bin",
42
- `${process.env.HOME}/.npm-global/bin`,
43
- `${process.env.HOME}/.nvm/versions/node/current/bin`,
44
- ].filter(Boolean);
45
-
46
- const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
47
- const env = { ...process.env, PATH: fullPath, HOME: process.env.HOME };
48
- // Remove API key so Claude Code uses the Claude subscription (not API credits)
49
- delete env.ANTHROPIC_API_KEY;
50
- return env;
51
- }
11
+ import { resolveClaudePath, buildEnv, installHint } from "./claude-resolver.js";
52
12
 
53
13
  const CLAUDE_PATH = resolveClaudePath();
54
14
 
@@ -79,7 +39,7 @@ export async function runDeveloperAgent(task, projectPath, techStack, allTasks =
79
39
  console.log(` 🤖 Claude Code working on: ${task.title}`);
80
40
 
81
41
  if (!CLAUDE_PATH) {
82
- const hint = "Claude Code CLI not found. Run: npm install -g @anthropic-ai/claude-code";
42
+ const hint = installHint();
83
43
  await startTask(task.id, hint);
84
44
  await failTask(task.id, hint);
85
45
  console.error(`\n ✗ ${hint}\n`);
@@ -10,6 +10,9 @@ import { createConnection } from "net";
10
10
  import { spawn as _spawn, execSync } from "child_process";
11
11
 
12
12
  const require = createRequire(import.meta.url);
13
+ // On Windows, npm-installed binaries have a .cmd wrapper — use it for spawn()
14
+ const isWin = process.platform === "win32";
15
+ const NPX = isWin ? "npx.cmd" : "npx";
13
16
  const MAX_FIX_ATTEMPTS = 5;
14
17
  const BUILD_HASH_FILE = ".claudeboard-build-hash.txt";
15
18
  const BUILD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min max for expo run:ios
@@ -244,7 +247,7 @@ export async function ensureDevBuild(projectPath) {
244
247
  }, BUILD_TIMEOUT_MS);
245
248
 
246
249
  try {
247
- const proc = _spawn("npx", ["expo", "run:ios", "--simulator"], {
250
+ const proc = _spawn(NPX, ["expo", "run:ios", "--simulator"], {
248
251
  cwd: projectPath,
249
252
  env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1" },
250
253
  stdio: "pipe",
@@ -386,7 +389,7 @@ async function tryStartExpo(projectPath, port) {
386
389
  ? ["expo", "start", "--ios", "--port", String(port)]
387
390
  : ["expo", "start", "--web", "--port", String(port)];
388
391
 
389
- proc = _spawn("npx", expoArgs, {
392
+ proc = _spawn(NPX, expoArgs, {
390
393
  cwd: projectPath,
391
394
  env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
392
395
  stdio: "pipe",
package/agents/qa.js CHANGED
@@ -5,44 +5,11 @@ import { screenshotExpoWeb } from "../tools/screenshot.js";
5
5
  import { screenshotSimulator } from "./expo-health.js";
6
6
  import { callClaudeWithImage } from "./claude-api.js";
7
7
  import { listFiles, readFile } from "../tools/filesystem.js";
8
- import { execSync } from "child_process";
9
8
  import chalk from "chalk";
10
9
  import path from "path";
11
10
  import fs from "fs";
12
11
  import { createConnection } from "net";
13
-
14
- // ── Reuse Claude Code path + env from developer ───────────────────────────────
15
- function resolveClaudePath() {
16
- if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
17
- try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
18
- for (const p of [
19
- "/opt/homebrew/bin/claude",
20
- "/usr/local/bin/claude",
21
- `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
22
- `${process.env.HOME}/.npm-global/bin/claude`,
23
- ]) {
24
- try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
25
- }
26
- return null;
27
- }
28
-
29
- function buildEnv() {
30
- let nodeBinDir = "";
31
- try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
32
- const pathParts = [
33
- process.env.PATH || "",
34
- nodeBinDir,
35
- "/opt/homebrew/bin", "/opt/homebrew/sbin",
36
- "/usr/local/bin", "/usr/bin", "/bin",
37
- `${process.env.HOME}/.npm-global/bin`,
38
- `${process.env.HOME}/.nvm/versions/node/current/bin`,
39
- ].filter(Boolean);
40
- const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
41
- const env = { ...process.env, PATH: fullPath, HOME: process.env.HOME };
42
- // Remove API key so Claude Code uses the Claude subscription (not API credits)
43
- delete env.ANTHROPIC_API_KEY;
44
- return env;
45
- }
12
+ import { resolveClaudePath, buildEnv } from "./claude-resolver.js";
46
13
 
47
14
  const CLAUDE_PATH = resolveClaudePath();
48
15
 
package/bin/cli.js CHANGED
@@ -16,9 +16,9 @@ const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"),
16
16
 
17
17
 
18
18
  const LOGO = `
19
- ${chalk.cyan("╔══════════════════════════════════════╗")}
20
- ${chalk.cyan("║")} ${chalk.bold.white("●")} ${chalk.bold.cyan("CLAUDEBOARD")} ${chalk.dim("ai engineering highvalue team")} ${chalk.cyan("║")}
21
- ${chalk.cyan("╚══════════════════════════════════════╝")}
19
+ ${chalk.cyan("╔════════════════════════════════════════════╗")}
20
+ ${chalk.cyan("║")} ${chalk.bold.white("●")} ${chalk.bold.cyan("CLAUDEBOARD")} ${chalk.dim("powered by High Value, LLC")} ${chalk.cyan("║")}
21
+ ${chalk.cyan("╚════════════════════════════════════════════╝")}
22
22
  `;
23
23
 
24
24
  function loadConfig() {
@@ -48,7 +48,7 @@ program
48
48
  console.log(LOGO);
49
49
  const { default: Enquirer } = await import("enquirer");
50
50
  const enquirer = new Enquirer();
51
- console.log(chalk.bold("Let's set up your AI engineering team.\n"));
51
+ console.log(chalk.bold("Let's set up your AI engineering team — powered by High Value, LLC.\n"));
52
52
 
53
53
  const answers = await enquirer.prompt([
54
54
  { type: "input", name: "projectName", message: "Project name:", initial: path.basename(process.cwd()) },
@@ -158,18 +158,22 @@ program
158
158
 
159
159
  // ── Verify Claude Code CLI is installed ───────────────────────────────────
160
160
  const { execSync } = await import("child_process");
161
- try {
162
- execSync("claude --version", { stdio: "pipe" });
163
- const version = execSync("claude --version", { stdio: "pipe" }).toString().trim();
164
- console.log(chalk.dim(` Claude Code → ${version}\n`));
165
- } catch {
161
+ const { resolveClaudePath, installHint } = await import("../agents/claude-resolver.js");
162
+ const claudePath = resolveClaudePath();
163
+ if (!claudePath) {
166
164
  console.log(chalk.red("\n ✗ Claude Code CLI not found!\n"));
167
- console.log(chalk.yellow(" The developer agent requires Claude Code to be installed:"));
168
- console.log(chalk.bold(" npm install -g @anthropic-ai/claude-code\n"));
169
- console.log(chalk.dim(" Then authenticate:"));
170
- console.log(chalk.bold(" claude\n"));
165
+ for (const line of installHint().split("\n")) {
166
+ console.log(chalk.yellow(` ${line}`));
167
+ }
168
+ console.log();
171
169
  process.exit(1);
172
170
  }
171
+ try {
172
+ const version = execSync(`"${claudePath}" --version`, { stdio: "pipe" }).toString().trim();
173
+ console.log(chalk.dim(` Claude Code → ${version} (${claudePath})\n`));
174
+ } catch {
175
+ console.log(chalk.dim(` Claude Code → ${claudePath}\n`));
176
+ }
173
177
 
174
178
  const resolvedProject = path.resolve(opts.project);
175
179
 
@@ -241,7 +245,7 @@ program
241
245
 
242
246
  program
243
247
  .name("claudeboard")
244
- .description("AI engineering team — from PRD to working app, autonomously")
248
+ .description("AI engineering team — from PRD to working app, autonomously — powered by High Value, LLC")
245
249
  .version(_pkg.version);
246
250
 
247
251
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.15.2",
3
+ "version": "2.15.4",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {
package/tools/terminal.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { exec, spawn } from "child_process";
2
2
  import { promisify } from "util";
3
+ import { createConnection } from "net";
3
4
 
4
5
  const execAsync = promisify(exec);
6
+ const isWin = process.platform === "win32";
5
7
 
6
8
  /**
7
9
  * Run a shell command in a given directory, return { stdout, stderr, exitCode }
@@ -12,6 +14,7 @@ export async function runCommand(cmd, cwd, timeoutMs = 60000) {
12
14
  cwd,
13
15
  timeout: timeoutMs,
14
16
  env: { ...process.env, CI: "true", FORCE_COLOR: "0" },
17
+ shell: isWin ? "cmd.exe" : "/bin/sh",
15
18
  });
16
19
  return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
17
20
  } catch (err) {
@@ -43,15 +46,17 @@ export function startProcess(cmd, args, cwd, onLog) {
43
46
  }
44
47
 
45
48
  /**
46
- * Check if a port is in use
49
+ * Check if a port is in use — uses a TCP connection probe (cross-platform, no curl needed)
47
50
  */
48
51
  export async function waitForPort(port, timeoutMs = 30000) {
49
52
  const start = Date.now();
50
53
  while (Date.now() - start < timeoutMs) {
51
- try {
52
- const result = await execAsync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}`);
53
- if (result.stdout !== "000") return true;
54
- } catch {}
54
+ const open = await new Promise((resolve) => {
55
+ const sock = createConnection({ port, host: "127.0.0.1" });
56
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
57
+ sock.once("error", () => { sock.destroy(); resolve(false); });
58
+ });
59
+ if (open) return true;
55
60
  await new Promise((r) => setTimeout(r, 1000));
56
61
  }
57
62
  return false;