feat(S-202): Test-drive and implement src/detect.ts
This commit is contained in:
@@ -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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user