feat(M2): Client Detection & Configuration
This commit is contained in:
@@ -5,8 +5,7 @@ import {
|
|||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import type { ClientName } from "./types.js";
|
import type { ClientName } from "./types.js";
|
||||||
|
import { CLIENT_NAMES, isWindows } from "./constants.js";
|
||||||
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
|
|
||||||
|
|
||||||
export interface ResolvedConfig {
|
export interface ResolvedConfig {
|
||||||
paths: Partial<Record<ClientName, string>>;
|
paths: Partial<Record<ClientName, string>>;
|
||||||
@@ -23,8 +22,7 @@ export interface ResolveConfigOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultWhichSync(cmd: string): string | undefined {
|
function defaultWhichSync(cmd: string): string | undefined {
|
||||||
const isWin = process.platform === "win32";
|
const result = spawnSync(isWindows() ? "where" : "which", [cmd], {
|
||||||
const result = spawnSync(isWin ? "where" : "which", [cmd], {
|
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
if (result.status === 0) {
|
if (result.status === 0) {
|
||||||
@@ -85,7 +83,10 @@ export function resolveConfig(
|
|||||||
fileDefault;
|
fileDefault;
|
||||||
|
|
||||||
const result: ResolvedConfig = { paths };
|
const result: ResolvedConfig = { paths };
|
||||||
if (defaultClient !== undefined) {
|
if (
|
||||||
|
defaultClient !== undefined &&
|
||||||
|
CLIENT_NAMES.includes(defaultClient as ClientName)
|
||||||
|
) {
|
||||||
result.defaultClient = defaultClient as ClientName;
|
result.defaultClient = defaultClient as ClientName;
|
||||||
}
|
}
|
||||||
return result;
|
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 { existsSync as defaultExistsSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type ClientName, type ClientInfo } from "./types.js";
|
import { type ClientName, type ClientInfo } from "./types.js";
|
||||||
|
import { CLIENT_NAMES, isWindows } from "./constants.js";
|
||||||
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
|
|
||||||
|
|
||||||
export interface DetectOptions {
|
export interface DetectOptions {
|
||||||
pathEnv?: string;
|
pathEnv?: string;
|
||||||
@@ -13,7 +12,7 @@ export interface DetectOptions {
|
|||||||
|
|
||||||
function parseVersion(stdout: string): string | undefined {
|
function parseVersion(stdout: string): string | undefined {
|
||||||
const match = stdout.match(/v?\d+\.\d+\.\d+(?:[-+.]\w+)*/);
|
const match = stdout.match(/v?\d+\.\d+\.\d+(?:[-+.]\w+)*/);
|
||||||
return match ? match[0] : undefined;
|
return match ? match[0].replace(/^v/, "") : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findBinary(
|
function findBinary(
|
||||||
@@ -22,10 +21,8 @@ function findBinary(
|
|||||||
spawnSyncImpl: typeof defaultSpawnSync,
|
spawnSyncImpl: typeof defaultSpawnSync,
|
||||||
existsSyncImpl: typeof defaultExistsSync
|
existsSyncImpl: typeof defaultExistsSync
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const isWin = process.platform === "win32";
|
|
||||||
|
|
||||||
// Try system `which` / `where` first
|
// Try system `which` / `where` first
|
||||||
const whichResult = spawnSyncImpl(isWin ? "where" : "which", [name], {
|
const whichResult = spawnSyncImpl(isWindows() ? "where" : "which", [name], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
env: { ...process.env, PATH: pathEnv },
|
env: { ...process.env, PATH: pathEnv },
|
||||||
});
|
});
|
||||||
@@ -35,8 +32,8 @@ function findBinary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: walk PATH directories manually
|
// Fallback: walk PATH directories manually
|
||||||
const sep = isWin ? ";" : ":";
|
const sep = isWindows() ? ";" : ":";
|
||||||
const ext = isWin ? ".exe" : "";
|
const ext = isWindows() ? ".exe" : "";
|
||||||
const dirs = pathEnv.split(sep);
|
const dirs = pathEnv.split(sep);
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
if (!dir) continue;
|
if (!dir) continue;
|
||||||
|
|||||||
@@ -108,4 +108,31 @@ describe("resolveConfig", () => {
|
|||||||
});
|
});
|
||||||
assert.strictEqual(config.defaultClient, "codex");
|
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,
|
clients.find((c) => c.name === "claude")!.version,
|
||||||
"0.4.5-beta"
|
"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