Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion hooks/session-start-profiler-platform.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, test } from "bun:test";
import { detectSessionStartPlatform } from "./src/session-start-profiler.mts";
import {
buildWindowsShellCommand,
detectSessionStartPlatform,
getBinaryPathCandidates,
} from "./src/session-start-profiler.mts";

describe("session-start-profiler platform detection", () => {
test("test_session_start_profiler_does_not_infer_cursor_from_cursor_project_dir_alone", () => {
Expand All @@ -26,3 +30,24 @@ describe("session-start-profiler platform detection", () => {
).toBe("claude-code");
});
});

describe("session-start-profiler Windows CLI resolution", () => {
test("test_windows_prefers_pathext_launchers_over_extensionless_npm_shims", () => {
const candidates = getBinaryPathCandidates("vercel", "win32");
const uppercaseCandidates = candidates.map((candidate: string) => candidate.toUpperCase());

expect(uppercaseCandidates).toContain("VERCEL.CMD");
expect(candidates).not.toContain("vercel");
expect(candidates[0]).toMatch(/^vercel\./i);
});

test("test_windows_cmd_shims_are_quoted_for_the_command_shell", () => {
expect(
buildWindowsShellCommand(
"C:\\Users\\Adam\\AppData\\Roaming\\npm\\vercel.cmd",
["--version"],
"win32",
),
).toBe('"C:\\Users\\Adam\\AppData\\Roaming\\npm\\vercel.cmd" "--version"');
});
});
44 changes: 30 additions & 14 deletions hooks/session-start-profiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
readdirSync
} from "fs";
import { delimiter, join, resolve } from "path";
import { execFileSync } from "child_process";
import { execFileSync, execSync } from "child_process";
import { fileURLToPath } from "url";
import {
formatOutput,
Expand Down Expand Up @@ -200,12 +200,12 @@ var SPAWN_STDIO = "ignore pipe ignore".split(" ");
var EXEC_SYNC_TIMEOUT_MS = 3e3;
var NUMERIC_VERSION_RE = /\d+(?:\.\d+)*/;
var WINDOWS_EXECUTABLE_EXTENSIONS = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
function getBinaryPathCandidates(binaryName) {
if (process.platform !== "win32") {
function getBinaryPathCandidates(binaryName, platform = process.platform) {
if (platform !== "win32") {
return [binaryName];
}
const hasExecutableExtension = /\.[^./\\]+$/.test(binaryName);
const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
const suffixes = hasExecutableExtension ? [""] : WINDOWS_EXECUTABLE_EXTENSIONS;
return suffixes.map((suffix) => `${binaryName}${suffix}`);
}
function resolveBinaryFromPath(binaryName) {
Expand Down Expand Up @@ -234,6 +234,28 @@ function resolveBinaryFromPath(binaryName) {
});
return null;
}
function buildWindowsShellCommand(binaryPath, args, platform = process.platform) {
if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(binaryPath)) {
return null;
}
const quote = (value) => `"${value.replace(/"/g, '""')}"`;
return [quote(binaryPath), ...args.map(quote)].join(" ");
}
function execResolvedBinarySync(binaryPath, args) {
const windowsShellCommand = buildWindowsShellCommand(binaryPath, args);
const options = {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO
};
if (windowsShellCommand) {
return execSync(windowsShellCommand, {
...options,
shell: process.env.ComSpec || "cmd.exe"
}).trim();
}
return execFileSync(binaryPath, args, options).trim();
}
function parseVersionSegments(version) {
const matchedVersion = version.match(NUMERIC_VERSION_RE)?.[0];
if (!matchedVersion) {
Expand Down Expand Up @@ -264,11 +286,7 @@ function checkVercelCli() {
}
let currentVersion;
try {
const raw = execFileSync(vercelBinary, VERCEL_VERSION_ARGS, {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO
}).trim();
const raw = execResolvedBinarySync(vercelBinary, VERCEL_VERSION_ARGS);
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
currentVersion = lines[lines.length - 1];
} catch (error) {
Expand All @@ -284,11 +302,7 @@ function checkVercelCli() {
}
let latestVersion;
try {
const raw = execFileSync(npmBinary, NPM_VIEW_ARGS, {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO
}).trim();
const raw = execResolvedBinarySync(npmBinary, NPM_VIEW_ARGS);
latestVersion = raw;
} catch (error) {
logCaughtError(log, "session-start-profiler:npm-latest-version-check-failed", error, {
Expand Down Expand Up @@ -487,9 +501,11 @@ if (isSessionStartProfilerEntrypoint) {
export {
buildSessionStartProfilerEnvVars,
buildSessionStartProfilerUserMessages,
buildWindowsShellCommand,
checkGreenfield,
detectSessionStartPlatform,
formatSessionStartProfilerCursorOutput,
getBinaryPathCandidates,
logBrokenSkillFrontmatterSummary,
normalizeSessionStartSessionId,
parseSessionStartInput,
Expand Down
56 changes: 42 additions & 14 deletions hooks/src/session-start-profiler.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from "node:fs";
import { homedir } from "node:os";
import { delimiter, join, resolve } from "node:path";
import { execFileSync } from "node:child_process";
import { execFileSync, execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
formatOutput,
Expand Down Expand Up @@ -330,13 +330,18 @@ const WINDOWS_EXECUTABLE_EXTENSIONS = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.C
.split(";")
.filter(Boolean);

function getBinaryPathCandidates(binaryName: string): string[] {
if (process.platform !== "win32") {
export function getBinaryPathCandidates(
binaryName: string,
platform: NodeJS.Platform = process.platform,
): string[] {
if (platform !== "win32") {
return [binaryName];
}

const hasExecutableExtension = /\.[^./\\]+$/.test(binaryName);
const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
// npm installs an extensionless POSIX shim beside PATHEXT launchers on
// Windows. Node's execFileSync cannot execute that shim or a .cmd directly.
const suffixes = hasExecutableExtension ? [""] : WINDOWS_EXECUTABLE_EXTENSIONS;
return suffixes.map((suffix: string) => `${binaryName}${suffix}`);
}

Expand Down Expand Up @@ -368,6 +373,37 @@ function resolveBinaryFromPath(binaryName: string): string | null {
return null;
}

export function buildWindowsShellCommand(
binaryPath: string,
args: string[],
platform: NodeJS.Platform = process.platform,
): string | null {
if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(binaryPath)) {
return null;
}

const quote = (value: string): string => `"${value.replace(/"/g, '""')}"`;
return [quote(binaryPath), ...args.map(quote)].join(" ");
}

function execResolvedBinarySync(binaryPath: string, args: string[]): string {
const windowsShellCommand = buildWindowsShellCommand(binaryPath, args);
const options = {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8" as const,
stdio: SPAWN_STDIO,
};

if (windowsShellCommand) {
return execSync(windowsShellCommand, {
...options,
shell: process.env.ComSpec || "cmd.exe",
}).trim();
}

return execFileSync(binaryPath, args, options).trim();
}

function parseVersionSegments(version: string): number[] | null {
const matchedVersion = version.match(NUMERIC_VERSION_RE)?.[0];
if (!matchedVersion) {
Expand Down Expand Up @@ -413,11 +449,7 @@ function checkVercelCli(): VercelCliStatus {
// 1. Check if vercel is installed
let currentVersion: string | undefined;
try {
const raw: string = execFileSync(vercelBinary, VERCEL_VERSION_ARGS, {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO,
}).trim();
const raw: string = execResolvedBinarySync(vercelBinary, VERCEL_VERSION_ARGS);
// Output may include extra lines; version is typically last non-empty line
const lines: string[] = raw.split("\n").map((l: string) => l.trim()).filter(Boolean);
currentVersion = lines[lines.length - 1];
Expand All @@ -437,11 +469,7 @@ function checkVercelCli(): VercelCliStatus {
// 2. Fetch latest version from npm registry
let latestVersion: string | undefined;
try {
const raw: string = execFileSync(npmBinary, NPM_VIEW_ARGS, {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO,
}).trim();
const raw: string = execResolvedBinarySync(npmBinary, NPM_VIEW_ARGS);
latestVersion = raw;
} catch (error) {
logCaughtError(log, "session-start-profiler:npm-latest-version-check-failed", error, {
Expand Down