@pixelbyte-software/pixcode 1.31.0 → 1.31.2

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.
@@ -30,13 +30,21 @@
30
30
  * Jobs linger 10 minutes after completion so late subscribers still see
31
31
  * the outcome.
32
32
  */
33
- import { spawn } from 'node:child_process';
33
+ import { execFileSync } from 'node:child_process';
34
34
  import { EventEmitter } from 'node:events';
35
35
  import { randomUUID } from 'node:crypto';
36
36
  import fs from 'node:fs';
37
37
  import os from 'node:os';
38
38
  import path from 'node:path';
39
39
 
40
+ // Use cross-spawn instead of node:child_process.spawn. On Windows, node's
41
+ // spawn cannot invoke `.cmd` / `.bat` files without `shell: true`, and with
42
+ // `shell: true` it tokenises on spaces — so a valid npm path like
43
+ // `C:\Program Files\nodejs\npm.cmd` gets split into "C:\Program" + "Files...".
44
+ // cross-spawn shells out through cmd.exe with proper quoting transparently
45
+ // and is already a transitive dependency we can safely re-use.
46
+ import spawn from 'cross-spawn';
47
+
40
48
  const jobs = new Map();
41
49
  const FINISHED_TTL_MS = 10 * 60 * 1000;
42
50
  const HARD_TIMEOUT_MS = 10 * 60 * 1000;
@@ -107,8 +115,17 @@ export function primeCliBinPath(env = process.env) {
107
115
  * override detection.
108
116
  */
109
117
  export function resolveProviderExecutables(env = process.env) {
118
+ // Claude is intentionally omitted. The Claude Agent SDK ships a bundled
119
+ // native binary per platform (@anthropic-ai/claude-agent-sdk-<os>-<arch>)
120
+ // and resolves it automatically. Exporting CLAUDE_CLI_PATH here would
121
+ // override that and hand a `.cmd` shim to Node's spawn on Windows,
122
+ // which then throws EINVAL (spawn can't exec .cmd files directly).
123
+ //
124
+ // The other providers use cross-spawn in our own adapters, which
125
+ // handles .cmd/.bat resolution on Windows. Forcing an absolute path
126
+ // there is still helpful because cross-spawn.sync without quoting
127
+ // can hit edge cases when PATH contains spaces.
110
128
  const providers = [
111
- { name: 'claude', envKey: 'CLAUDE_CLI_PATH' },
112
129
  { name: 'codex', envKey: 'CODEX_CLI_PATH' },
113
130
  { name: 'gemini', envKey: 'GEMINI_CLI_PATH' },
114
131
  { name: 'qwen', envKey: 'QWEN_CLI_PATH' },
@@ -121,6 +138,146 @@ export function resolveProviderExecutables(env = process.env) {
121
138
  }
122
139
  }
123
140
 
141
+ /**
142
+ * Cross-platform lookup for the Claude Code CLI executable. The
143
+ * @anthropic-ai/claude-agent-sdk SDK spawns its target with plain
144
+ * `child_process.spawn(command, args)` — no shell, no cross-spawn — which
145
+ * means:
146
+ * - On Unix, `"claude"` resolves via PATH + shebang. Works out of the box.
147
+ * - On Windows, `"claude"` does NOT resolve (Node doesn't traverse PATHEXT
148
+ * for bare names), and spawning a `.cmd` shim directly throws EINVAL
149
+ * after Node 20.12's CVE-2024-27980 fix. We have to hand the SDK the
150
+ * real `.exe` target instead.
151
+ *
152
+ * We use the OS's own `where`/`which` so we stay consistent with whatever
153
+ * the user sees in their shell. When `where` yields a `.cmd` shim, we
154
+ * peek inside it (npm-generated shims quote the underlying `.exe` path)
155
+ * and return that real binary.
156
+ *
157
+ * Returns the absolute path, or `null` if nothing turned up — callers
158
+ * should leave `pathToClaudeCodeExecutable` unset so the SDK falls back
159
+ * to its own bundled native binary.
160
+ */
161
+ export function resolveClaudeExecutable() {
162
+ const isWindows = process.platform === 'win32';
163
+ try {
164
+ if (isWindows) {
165
+ // `where.exe` returns one path per line. Prefer `.exe` over any
166
+ // `.cmd` or `.ps1` shim because Node's spawn can exec .exe
167
+ // directly — .cmd needs shell:true which the SDK doesn't set.
168
+ const stdout = execFileSync('where', ['claude'], {
169
+ encoding: 'utf8',
170
+ stdio: ['ignore', 'pipe', 'ignore'],
171
+ }).trim();
172
+ const candidates = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
173
+ const exe = candidates.find((p) => p.toLowerCase().endsWith('.exe'));
174
+ if (exe && fs.existsSync(exe)) return exe;
175
+ // Only a `.cmd` shim found. Parse it for the real .exe target.
176
+ for (const candidate of candidates) {
177
+ if (candidate.toLowerCase().endsWith('.cmd')) {
178
+ const underlying = parseNpmCmdShim(candidate);
179
+ if (underlying) return underlying;
180
+ }
181
+ }
182
+ return candidates[0] || null;
183
+ }
184
+ const stdout = execFileSync('which', ['claude'], {
185
+ encoding: 'utf8',
186
+ stdio: ['ignore', 'pipe', 'ignore'],
187
+ }).trim();
188
+ return stdout || null;
189
+ } catch {
190
+ // `where`/`which` returns non-zero when nothing matches. Fall back
191
+ // to null so the SDK uses its own resolver.
192
+ return null;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Cross-platform lookup for a POSIX `bash` the Claude CLI can drive. On
198
+ * Windows, `claude.exe` hard-requires a `bash.exe` (typically from Git
199
+ * for Windows) and exits with code 1 + a guidance message if it can't
200
+ * find one. The CLI reads the path from `CLAUDE_CODE_GIT_BASH_PATH`
201
+ * when set, otherwise probes a short list of known install locations —
202
+ * which are exactly the ones we try below.
203
+ *
204
+ * Returns the absolute path or null. On non-Windows platforms we skip
205
+ * the probe entirely and rely on the system `bash` that Claude expects
206
+ * to already be on PATH.
207
+ */
208
+ export function resolveGitBashPath() {
209
+ if (process.platform !== 'win32') return null;
210
+
211
+ if (process.env.CLAUDE_CODE_GIT_BASH_PATH
212
+ && fs.existsSync(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
213
+ return process.env.CLAUDE_CODE_GIT_BASH_PATH;
214
+ }
215
+
216
+ // 1. `where.exe bash` first — the user already has it on PATH if any
217
+ // shell launcher (VS Code, etc.) set it up. Prefer this over our
218
+ // hard-coded list because it reflects their actual install.
219
+ try {
220
+ const stdout = execFileSync('where', ['bash'], {
221
+ encoding: 'utf8',
222
+ stdio: ['ignore', 'pipe', 'ignore'],
223
+ }).trim();
224
+ const first = stdout.split(/\r?\n/)[0]?.trim();
225
+ if (first && fs.existsSync(first)) return first;
226
+ } catch { /* fall through to hard-coded probes */ }
227
+
228
+ // 2. Known Git-for-Windows install locations. Covers system-wide,
229
+ // per-user, scoop, and chocolatey defaults.
230
+ const home = os.homedir();
231
+ const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
232
+ const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
233
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
234
+
235
+ const candidates = [
236
+ path.join(programFiles, 'Git', 'bin', 'bash.exe'),
237
+ path.join(programFiles, 'Git', 'usr', 'bin', 'bash.exe'),
238
+ path.join(programFilesX86, 'Git', 'bin', 'bash.exe'),
239
+ path.join(programFilesX86, 'Git', 'usr', 'bin', 'bash.exe'),
240
+ path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
241
+ path.join(localAppData, 'Programs', 'Git', 'usr', 'bin', 'bash.exe'),
242
+ // Scoop's default install path for the git package
243
+ path.join(home, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
244
+ ];
245
+
246
+ for (const candidate of candidates) {
247
+ try {
248
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
249
+ return candidate;
250
+ }
251
+ } catch { /* ignore */ }
252
+ }
253
+
254
+ return null;
255
+ }
256
+
257
+ /**
258
+ * Extract the real .exe target from an npm-generated Windows .cmd shim.
259
+ *
260
+ * The shim looks like:
261
+ * @"%_prog%" "%dp0%\node_modules\@anthropic-ai\claude-code\bin\claude.exe" %*
262
+ * We capture the first quoted `.exe` path, then expand `%~dp0` / `%dp0`
263
+ * to the shim's own directory so the returned path is absolute.
264
+ */
265
+ function parseNpmCmdShim(cmdPath) {
266
+ try {
267
+ const content = fs.readFileSync(cmdPath, 'utf8');
268
+ const match = content.match(/"([^"]+\.exe)"/i);
269
+ if (!match) return null;
270
+ const rel = match[1];
271
+ const dir = path.dirname(cmdPath);
272
+ const resolved = rel
273
+ .replace(/%~?dp0%?\\?/gi, `${dir}${path.sep}`)
274
+ .replace(/%~dp0/gi, dir);
275
+ return fs.existsSync(resolved) ? resolved : null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
124
281
  /**
125
282
  * Search PATH for an executable, including the Windows extension variants.
126
283
  * Returns the absolute path or null. Plain Node has no cross-platform
@@ -263,8 +420,10 @@ export function createInstallJob({ provider, installCmd, packageName }) {
263
420
  env: { ...process.env, npm_config_yes: 'true' },
264
421
  stdio: ['ignore', 'pipe', 'pipe'],
265
422
  windowsHide: true,
266
- // On Windows, .cmd files need a shell to be invokable by spawn().
267
- shell: process.platform === 'win32' && !useNodeRunner,
423
+ // cross-spawn handles .cmd/.bat resolution itself no shell
424
+ // needed. Passing `shell: true` here would re-introduce the
425
+ // space-in-path tokenisation bug that caused "'C:\Program' is
426
+ // not recognized" on Windows installs of Node.
268
427
  });
269
428
  } catch (err) {
270
429
  const message = err?.message || String(err);