feat(S-202): Test-drive and implement src/detect.ts

This commit is contained in:
2026-05-18 17:51:07 -05:00
parent 8340933f8a
commit 2642c280a2
2 changed files with 217 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
import { spawnSync as defaultSpawnSync } from "node:child_process";
import { existsSync as defaultExistsSync } from "node:fs";
import { join } from "node:path";
import { type ClientName, type ClientInfo } from "./types.js";
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
export interface DetectOptions {
pathEnv?: string;
spawnSync?: typeof defaultSpawnSync;
existsSync?: typeof defaultExistsSync;
}
function parseVersion(stdout: string): string | undefined {
const match = stdout.match(/v?\d+\.\d+\.\d+(?:[-+.]\w+)*/);
return match ? match[0] : undefined;
}
function findBinary(
name: string,
pathEnv: string,
spawnSyncImpl: typeof defaultSpawnSync,
existsSyncImpl: typeof defaultExistsSync
): string | undefined {
const isWin = process.platform === "win32";
// Try system `which` / `where` first
const whichResult = spawnSyncImpl(isWin ? "where" : "which", [name], {
encoding: "utf-8",
env: { ...process.env, PATH: pathEnv },
});
if (whichResult.status === 0) {
const line = whichResult.stdout.trim().split("\n")[0];
if (line) return line;
}
// Fallback: walk PATH directories manually
const sep = isWin ? ";" : ":";
const ext = isWin ? ".exe" : "";
const dirs = pathEnv.split(sep);
for (const dir of dirs) {
if (!dir) continue;
const fullPath = join(dir, name + ext);
if (existsSyncImpl(fullPath)) {
return fullPath;
}
}
return undefined;
}
export function detectClients(options?: DetectOptions): ClientInfo[] {
const spawnSyncImpl = options?.spawnSync ?? defaultSpawnSync;
const existsSyncImpl = options?.existsSync ?? defaultExistsSync;
const pathEnv = options?.pathEnv ?? process.env.PATH ?? "";
return CLIENT_NAMES.map((name) => {
const path = findBinary(name, pathEnv, spawnSyncImpl, existsSyncImpl);
if (!path) {
return { name, found: false };
}
const versionResult = spawnSyncImpl(path, ["--version"], {
encoding: "utf-8",
});
const version =
versionResult.status === 0
? parseVersion(versionResult.stdout)
: undefined;
return { name, path, version, found: true };
});
}