@venturewild/workspace 0.6.27 → 0.6.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venturewild/workspace",
3
- "version": "0.6.27",
3
+ "version": "0.6.28",
4
4
  "description": "Claude Code Web — Replit/Lovable-style chat-first browser UI that wraps the AI agent already installed on your machine.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -46,13 +46,32 @@ export function ensureToolPath(env = process.env, { platform = process.platform,
46
46
  return env.PATH;
47
47
  }
48
48
 
49
+ // On Windows, `where.exe <binary>` lists the extensionless POSIX npm shim
50
+ // (e.g. `…\AppData\Roaming\npm\claude`) BEFORE claude.cmd. spawn() with
51
+ // shell:false can't run that shim — it's a sh script — so a npm-installed
52
+ // Claude resolved to ENOENT (caught live 2026-06-26). Prefer a real Windows
53
+ // launcher (.cmd > .exe > .bat); fall back to the first hit if none. Exported
54
+ // for unit testing.
55
+ export function pickWindowsBinary(lines) {
56
+ const list = Array.isArray(lines) ? lines : [];
57
+ for (const ext of ['.exe', '.cmd', '.bat']) {
58
+ const re = new RegExp(ext.replace('.', '\\.') + '$', 'i');
59
+ const hit = list.find((l) => re.test(l.trim()));
60
+ if (hit) return hit.trim();
61
+ }
62
+ return list[0]?.trim() || null;
63
+ }
64
+
49
65
  async function isOnPath(binary) {
50
66
  ensureToolPath(); // make sure the tool dirs are on PATH before we look
51
67
  const probe = process.platform === 'win32' ? 'where.exe' : 'which';
52
68
  try {
53
69
  const { stdout } = await execFile(probe, [binary], { timeout: PATH_LOOKUP_TIMEOUT_MS });
54
70
  const lines = stdout.split(/\r?\n/).filter(Boolean);
55
- if (lines.length > 0) return lines[0].trim();
71
+ if (lines.length > 0) {
72
+ // Skip the extensionless npm shim on Windows (see pickWindowsBinary).
73
+ return process.platform === 'win32' ? pickWindowsBinary(lines) : lines[0].trim();
74
+ }
56
75
  } catch { /* fall through to a direct probe */ }
57
76
  // which/where can still miss a freshly-installed binary in a stripped launchd
58
77
  // environment — probe the known install dirs directly and return an ABSOLUTE
@@ -180,12 +199,19 @@ export class AgentSession extends EventEmitter {
180
199
  ? buildClaudeArgs(this.agent.args, ctx)
181
200
  : [...this.agent.args];
182
201
  // Prefer the resolved absolute path from detection; fall back to the bare
183
- // name. claude ships a native binary, so shell:false spawns cleanly.
202
+ // name. On Windows a npm-installed Claude resolves to claude.cmd — which
203
+ // Node refuses to spawn with shell:false (CVE-2024-27980) — so a .cmd/.bat
204
+ // shim (or the extensionless npm shim, which cmd rescues via PATHEXT) must
205
+ // go through cmd.exe. A real .exe (node, native claude.exe) spawns
206
+ // directly with shell:false, so tests + native installs are untouched.
207
+ // POSIX keeps shell:false (the native binary there spawns directly).
184
208
  const command = this.agent.resolvedPath || this.agent.binary;
209
+ const isWin = process.platform === 'win32';
210
+ const needsShell = isWin && !/\.exe$/i.test(command);
185
211
  this.proc = spawn(command, args, {
186
212
  cwd: ctx.cwd || this.opts.cwd || process.cwd(),
187
213
  env: { ...process.env, ...(ctx.env || {}) },
188
- shell: false,
214
+ shell: needsShell,
189
215
  stdio: ['pipe', 'pipe', 'pipe'],
190
216
  windowsHide: true,
191
217
  });