feat(M2): Client Detection & Configuration
This commit is contained in:
@@ -5,8 +5,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import type { ClientName } from "./types.js";
|
||||
|
||||
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
|
||||
import { CLIENT_NAMES, isWindows } from "./constants.js";
|
||||
|
||||
export interface ResolvedConfig {
|
||||
paths: Partial<Record<ClientName, string>>;
|
||||
@@ -23,8 +22,7 @@ export interface ResolveConfigOptions {
|
||||
}
|
||||
|
||||
function defaultWhichSync(cmd: string): string | undefined {
|
||||
const isWin = process.platform === "win32";
|
||||
const result = spawnSync(isWin ? "where" : "which", [cmd], {
|
||||
const result = spawnSync(isWindows() ? "where" : "which", [cmd], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (result.status === 0) {
|
||||
@@ -85,7 +83,10 @@ export function resolveConfig(
|
||||
fileDefault;
|
||||
|
||||
const result: ResolvedConfig = { paths };
|
||||
if (defaultClient !== undefined) {
|
||||
if (
|
||||
defaultClient !== undefined &&
|
||||
CLIENT_NAMES.includes(defaultClient as ClientName)
|
||||
) {
|
||||
result.defaultClient = defaultClient as ClientName;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ClientName } from "./types.js";
|
||||
|
||||
export const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
|
||||
|
||||
export function isWindows(): boolean {
|
||||
return process.platform === "win32";
|
||||
}
|
||||
@@ -2,8 +2,7 @@ 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"];
|
||||
import { CLIENT_NAMES, isWindows } from "./constants.js";
|
||||
|
||||
export interface DetectOptions {
|
||||
pathEnv?: string;
|
||||
@@ -13,7 +12,7 @@ export interface DetectOptions {
|
||||
|
||||
function parseVersion(stdout: string): string | undefined {
|
||||
const match = stdout.match(/v?\d+\.\d+\.\d+(?:[-+.]\w+)*/);
|
||||
return match ? match[0] : undefined;
|
||||
return match ? match[0].replace(/^v/, "") : undefined;
|
||||
}
|
||||
|
||||
function findBinary(
|
||||
@@ -22,10 +21,8 @@ function findBinary(
|
||||
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], {
|
||||
const whichResult = spawnSyncImpl(isWindows() ? "where" : "which", [name], {
|
||||
encoding: "utf-8",
|
||||
env: { ...process.env, PATH: pathEnv },
|
||||
});
|
||||
@@ -35,8 +32,8 @@ function findBinary(
|
||||
}
|
||||
|
||||
// Fallback: walk PATH directories manually
|
||||
const sep = isWin ? ";" : ":";
|
||||
const ext = isWin ? ".exe" : "";
|
||||
const sep = isWindows() ? ";" : ":";
|
||||
const ext = isWindows() ? ".exe" : "";
|
||||
const dirs = pathEnv.split(sep);
|
||||
for (const dir of dirs) {
|
||||
if (!dir) continue;
|
||||
|
||||
@@ -108,4 +108,31 @@ describe("resolveConfig", () => {
|
||||
});
|
||||
assert.strictEqual(config.defaultClient, "codex");
|
||||
});
|
||||
|
||||
it("ignores invalid defaultClient from file", () => {
|
||||
const config = resolveConfig({
|
||||
existsSync: () => true,
|
||||
readFileSync: () => JSON.stringify({ defaultClient: "foo" }),
|
||||
whichSync: () => undefined,
|
||||
});
|
||||
assert.strictEqual(config.defaultClient, undefined);
|
||||
});
|
||||
|
||||
it("ignores invalid defaultClient from env var", () => {
|
||||
const config = resolveConfig({
|
||||
env: { AI_CLI_DEFAULT_CLIENT: "bar" },
|
||||
existsSync: () => false,
|
||||
whichSync: () => undefined,
|
||||
});
|
||||
assert.strictEqual(config.defaultClient, undefined);
|
||||
});
|
||||
|
||||
it("ignores invalid defaultClient from flag", () => {
|
||||
const config = resolveConfig({
|
||||
flags: { "default-client": "baz" },
|
||||
existsSync: () => false,
|
||||
whichSync: () => undefined,
|
||||
});
|
||||
assert.strictEqual(config.defaultClient, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +139,6 @@ describe("detectClients", () => {
|
||||
clients.find((c) => c.name === "claude")!.version,
|
||||
"0.4.5-beta"
|
||||
);
|
||||
assert.strictEqual(clients.find((c) => c.name === "opencode")!.version, "v2.0.0");
|
||||
assert.strictEqual(clients.find((c) => c.name === "opencode")!.version, "2.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user