From 33b825bb7ae65657193f5df20c618b7ea4738663 Mon Sep 17 00:00:00 2001 From: Cody Moore Date: Fri, 10 Apr 2026 12:05:51 -0400 Subject: [PATCH 1/2] fix: remove shell-specific commands for Windows compatibility --- src/constants.ts | 5 +++-- src/index.ts | 2 +- src/oauth.ts | 33 +++++++++++++++++++++++---------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 5db6d6f..3545b29 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -6,9 +6,10 @@ import { join } from "node:path"; function detectClaudeVersion(): string { // Try to read version from installed Claude CLI try { - const version = execSync("claude --version 2>/dev/null", { + const version = execFileSync("claude", ["--version"], { encoding: "utf8", timeout: 3000, + stdio: ["ignore", "pipe", "ignore"], }).trim(); const match = version.match(/^(\d+\.\d+\.\d+)/); if (match) return match[1]; diff --git a/src/index.ts b/src/index.ts index 03a5222..467737f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -230,7 +230,7 @@ function openBrowser(url: string): void { try { attempt(); break; } catch {} } } else if (process.platform === "win32") { - try { execFileSync("cmd", ["/c", "start", url], { timeout: 3000 }); } catch {} + try { execFileSync("cmd", ["/c", "start", "", url], { timeout: 3000 }); } catch {} } else { const attempts: Array<() => void> = [ () => execFileSync("/usr/bin/xdg-open", [url], { timeout: 3000 }), diff --git a/src/oauth.ts b/src/oauth.ts index c7cfcfc..9c3651f 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -1,6 +1,6 @@ import { randomBytes, createHash } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import { CLIENT_ID, TOKEN_URL, @@ -38,7 +38,7 @@ function base64url(buf: Buffer): string { } function sleep(ms: number): void { - execSync(`sleep ${(ms / 1000).toFixed(3)}`, { timeout: 60000 }); + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } /** @@ -50,17 +50,30 @@ function curlPost( retries = 3, ): { status: number; body: string } { const payload = JSON.stringify(body); - const escaped = payload.replace(/'/g, "'\\''"); for (let attempt = 0; attempt < retries; attempt++) { try { - const result = execSync( - `curl -s -w '\\n__HTTP_STATUS__%{http_code}' ` + - `-X POST '${TOKEN_URL}' ` + - `-H 'Content-Type: application/json' ` + - `-H 'User-Agent: ${USER_AGENT}' ` + - `-d '${escaped}'`, - { timeout: 30000, encoding: "utf8" }, + const result = execFileSync( + "curl", + [ + "-s", + "-w", + "\n__HTTP_STATUS__%{http_code}", + "-X", + "POST", + TOKEN_URL, + "-H", + "Content-Type: application/json", + "-H", + `User-Agent: ${USER_AGENT}`, + "-d", + payload, + ], + { + timeout: 30000, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }, ); const parts = result.split("\n__HTTP_STATUS__"); From 5faeb295336142f67d52992486e1ebbc5bdb78d6 Mon Sep 17 00:00:00 2001 From: Cody Moore Date: Fri, 10 Apr 2026 12:09:42 -0400 Subject: [PATCH 2/2] test: cover shell-free curl arg construction --- src/index.test.ts | 14 ++++++++++++++ src/oauth.ts | 32 ++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 647a3a1..b8d6757 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -8,6 +8,7 @@ import { describe, it, before } from "node:test"; import assert from "node:assert/strict"; +import { buildTokenCurlArgs } from "./oauth.js"; // ── Helpers (extracted / reimplemented from index.ts for unit testing) ──────── @@ -119,3 +120,16 @@ describe("temperature coercion", () => { assert.equal(out.temperature, undefined); }); }); + +describe("windows compatibility regressions", () => { + it("builds curl args without shell escaping requirements", () => { + const payload = JSON.stringify({ note: "O'Reilly" }); + const args = buildTokenCurlArgs(payload); + + assert.equal(args[0], "-s"); + assert.equal(args[1], "-w"); + assert.equal(args[2], "\n__HTTP_STATUS__%{http_code}"); + assert.equal(args[args.length - 2], "-d"); + assert.equal(args[args.length - 1], payload); + }); +}); diff --git a/src/oauth.ts b/src/oauth.ts index 9c3651f..4d4fe3b 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -41,6 +41,23 @@ function sleep(ms: number): void { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } +export function buildTokenCurlArgs(payload: string): string[] { + return [ + "-s", + "-w", + "\n__HTTP_STATUS__%{http_code}", + "-X", + "POST", + TOKEN_URL, + "-H", + "Content-Type: application/json", + "-H", + `User-Agent: ${USER_AGENT}`, + "-d", + payload, + ]; +} + /** * curl-based token exchange to avoid Bun/runtime fetch injecting * forbidden headers (Origin, Referer, Sec-Fetch-*) that trigger 429s. @@ -55,20 +72,7 @@ function curlPost( try { const result = execFileSync( "curl", - [ - "-s", - "-w", - "\n__HTTP_STATUS__%{http_code}", - "-X", - "POST", - TOKEN_URL, - "-H", - "Content-Type: application/json", - "-H", - `User-Agent: ${USER_AGENT}`, - "-d", - payload, - ], + buildTokenCurlArgs(payload), { timeout: 30000, encoding: "utf8",