linkshell-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/cli/src/commands/doctor.d.ts +1 -0
- package/dist/cli/src/commands/doctor.js +112 -0
- package/dist/cli/src/commands/doctor.js.map +1 -0
- package/dist/cli/src/commands/setup.d.ts +1 -0
- package/dist/cli/src/commands/setup.js +48 -0
- package/dist/cli/src/commands/setup.js.map +1 -0
- package/dist/cli/src/config.d.ts +12 -0
- package/dist/cli/src/config.js +23 -0
- package/dist/cli/src/config.js.map +1 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +108 -0
- package/dist/cli/src/index.js.map +1 -0
- package/dist/cli/src/providers.d.ts +12 -0
- package/dist/cli/src/providers.js +61 -0
- package/dist/cli/src/providers.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +40 -0
- package/dist/cli/src/runtime/bridge-session.js +317 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -0
- package/dist/cli/src/runtime/scrollback.d.ts +11 -0
- package/dist/cli/src/runtime/scrollback.js +33 -0
- package/dist/cli/src/runtime/scrollback.js.map +1 -0
- package/dist/cli/src/utils/lan-ip.d.ts +5 -0
- package/dist/cli/src/utils/lan-ip.js +20 -0
- package/dist/cli/src/utils/lan-ip.js.map +1 -0
- package/dist/cli/tsconfig.tsbuildinfo +1 -0
- package/dist/shared-protocol/src/index.d.ts +380 -0
- package/dist/shared-protocol/src/index.js +158 -0
- package/dist/shared-protocol/src/index.js.map +1 -0
- package/package.json +49 -0
- package/src/commands/doctor.ts +119 -0
- package/src/commands/setup.ts +65 -0
- package/src/config.ts +34 -0
- package/src/index.ts +139 -0
- package/src/providers.ts +91 -0
- package/src/runtime/bridge-session.ts +407 -0
- package/src/runtime/scrollback.ts +43 -0
- package/src/types/qrcode-terminal.d.ts +7 -0
- package/src/utils/lan-ip.ts +19 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { loadConfig, getConfigPath } from "../config.js";
|
|
3
|
+
|
|
4
|
+
interface CheckResult {
|
|
5
|
+
name: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function check(name: string, fn: () => string): CheckResult {
|
|
11
|
+
try {
|
|
12
|
+
return { name, ok: true, detail: fn() };
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return { name, ok: false, detail: e instanceof Error ? e.message : String(e) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function which(bin: string): string | undefined {
|
|
19
|
+
try {
|
|
20
|
+
return execSync(`which ${bin}`, { encoding: "utf8", timeout: 5000 }).trim() || undefined;
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function checkGateway(url: string): Promise<CheckResult> {
|
|
27
|
+
try {
|
|
28
|
+
const httpUrl = url.replace(/\/ws\/?$/, "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const res = await fetch(`${httpUrl}/healthz`, { signal: AbortSignal.timeout(5000) });
|
|
31
|
+
const latency = Date.now() - start;
|
|
32
|
+
if (!res.ok) return { name: "Gateway", ok: false, detail: `HTTP ${res.status}` };
|
|
33
|
+
return { name: "Gateway", ok: true, detail: `reachable (${latency}ms)` };
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return { name: "Gateway", ok: false, detail: e instanceof Error ? e.message : "unreachable" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runDoctor(gatewayUrl?: string): Promise<void> {
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
const gateway = gatewayUrl ?? config.gateway;
|
|
42
|
+
|
|
43
|
+
process.stdout.write("\n LinkShell Doctor\n\n");
|
|
44
|
+
|
|
45
|
+
const results: CheckResult[] = [];
|
|
46
|
+
|
|
47
|
+
// Node.js version
|
|
48
|
+
results.push(check("Node.js", () => {
|
|
49
|
+
const ver = process.versions.node;
|
|
50
|
+
const major = Number(ver.split(".")[0]);
|
|
51
|
+
if (major < 18) throw new Error(`v${ver} (need >= 18)`);
|
|
52
|
+
return `v${ver}`;
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// node-pty
|
|
56
|
+
results.push(check("node-pty", () => {
|
|
57
|
+
try {
|
|
58
|
+
require("node-pty");
|
|
59
|
+
return "loaded";
|
|
60
|
+
} catch {
|
|
61
|
+
// Try dynamic import path for ESM
|
|
62
|
+
try {
|
|
63
|
+
execSync("node -e \"require('node-pty')\"", { timeout: 5000, stdio: "pipe" });
|
|
64
|
+
return "loaded";
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("native module not built — run: pnpm approve-builds && pnpm install --force");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// Claude CLI
|
|
72
|
+
const claudePath = which("claude");
|
|
73
|
+
if (claudePath) {
|
|
74
|
+
results.push(check("Claude CLI", () => {
|
|
75
|
+
const ver = execSync("claude --version 2>&1", { encoding: "utf8", timeout: 5000 }).trim();
|
|
76
|
+
return `${ver} (${claudePath})`;
|
|
77
|
+
}));
|
|
78
|
+
} else {
|
|
79
|
+
results.push({ name: "Claude CLI", ok: false, detail: "not found — npm i -g @anthropic-ai/claude-code" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Codex CLI
|
|
83
|
+
const codexPath = which("codex");
|
|
84
|
+
if (codexPath) {
|
|
85
|
+
results.push(check("Codex CLI", () => `found (${codexPath})`));
|
|
86
|
+
} else {
|
|
87
|
+
results.push({ name: "Codex CLI", ok: false, detail: "not found — npm i -g @openai/codex" });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Config
|
|
91
|
+
results.push(check("Config", () => {
|
|
92
|
+
const path = getConfigPath();
|
|
93
|
+
const cfg = loadConfig();
|
|
94
|
+
const keys = Object.keys(cfg).filter((k) => cfg[k as keyof typeof cfg] !== undefined);
|
|
95
|
+
if (keys.length === 0) return `${path} (empty — run: linkshell setup)`;
|
|
96
|
+
return `${path} (${keys.join(", ")})`;
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// Gateway
|
|
100
|
+
if (gateway) {
|
|
101
|
+
results.push(await checkGateway(gateway));
|
|
102
|
+
} else {
|
|
103
|
+
results.push({ name: "Gateway", ok: false, detail: "no gateway configured — run: linkshell setup" });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Print results
|
|
107
|
+
for (const r of results) {
|
|
108
|
+
const icon = r.ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
109
|
+
process.stdout.write(` ${icon} ${r.name}: ${r.detail}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const failed = results.filter((r) => !r.ok);
|
|
113
|
+
process.stdout.write("\n");
|
|
114
|
+
if (failed.length === 0) {
|
|
115
|
+
process.stdout.write(" \x1b[32mAll checks passed.\x1b[0m\n\n");
|
|
116
|
+
} else {
|
|
117
|
+
process.stdout.write(` \x1b[33m${failed.length} issue(s) found.\x1b[0m\n\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { loadConfig, saveConfig, getConfigPath } from "../config.js";
|
|
3
|
+
import type { LinkShellConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
function ask(rl: readline.Interface, question: string, defaultValue?: string): Promise<string> {
|
|
6
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
9
|
+
resolve(answer.trim() || defaultValue || "");
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function choose(rl: readline.Interface, question: string, options: string[], defaultIdx = 0): Promise<string> {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
process.stdout.write(` ${question}\n`);
|
|
17
|
+
for (let i = 0; i < options.length; i++) {
|
|
18
|
+
const marker = i === defaultIdx ? "\x1b[36m>\x1b[0m" : " ";
|
|
19
|
+
process.stdout.write(` ${marker} ${i + 1}. ${options[i]}\n`);
|
|
20
|
+
}
|
|
21
|
+
rl.question(` Choice (${defaultIdx + 1}): `, (answer) => {
|
|
22
|
+
const idx = Number(answer.trim()) - 1;
|
|
23
|
+
resolve(options[idx >= 0 && idx < options.length ? idx : defaultIdx]!);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runSetup(): Promise<void> {
|
|
29
|
+
const existing = loadConfig();
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
|
|
32
|
+
process.stdout.write("\n LinkShell Setup\n\n");
|
|
33
|
+
|
|
34
|
+
const gateway = await ask(rl, "Gateway URL", existing.gateway ?? "wss://localhost:8787/ws");
|
|
35
|
+
|
|
36
|
+
const provider = await choose(
|
|
37
|
+
rl,
|
|
38
|
+
"Default provider:",
|
|
39
|
+
["claude", "codex", "custom"],
|
|
40
|
+
["claude", "codex", "custom"].indexOf(existing.provider ?? "claude"),
|
|
41
|
+
) as LinkShellConfig["provider"];
|
|
42
|
+
|
|
43
|
+
let command: string | undefined;
|
|
44
|
+
if (provider === "custom") {
|
|
45
|
+
command = await ask(rl, "Custom command", existing.command ?? "bash");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const clientName = await ask(rl, "Client name", existing.clientName ?? require("os").hostname());
|
|
49
|
+
|
|
50
|
+
rl.close();
|
|
51
|
+
|
|
52
|
+
const config: LinkShellConfig = {
|
|
53
|
+
gateway,
|
|
54
|
+
provider,
|
|
55
|
+
command,
|
|
56
|
+
clientName,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
|
|
61
|
+
process.stdout.write(`\n \x1b[32mConfig saved to ${getConfigPath()}\x1b[0m\n\n`);
|
|
62
|
+
process.stdout.write(" Next steps:\n");
|
|
63
|
+
process.stdout.write(" linkshell doctor — verify your setup\n");
|
|
64
|
+
process.stdout.write(" linkshell start — start a bridge session\n\n");
|
|
65
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export interface LinkShellConfig {
|
|
6
|
+
gateway?: string;
|
|
7
|
+
pairingGateway?: string;
|
|
8
|
+
provider?: "claude" | "codex" | "custom";
|
|
9
|
+
command?: string;
|
|
10
|
+
clientName?: string;
|
|
11
|
+
cols?: number;
|
|
12
|
+
rows?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CONFIG_DIR = join(homedir(), ".linkshell");
|
|
16
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
17
|
+
|
|
18
|
+
export function loadConfig(): LinkShellConfig {
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
21
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8")) as LinkShellConfig;
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function saveConfig(config: LinkShellConfig): void {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getConfigPath(): string {
|
|
33
|
+
return CONFIG_FILE;
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { BridgeSession } from "./runtime/bridge-session.js";
|
|
4
|
+
import { resolveProviderConfig } from "./providers.js";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { runDoctor } from "./commands/doctor.js";
|
|
7
|
+
import { runSetup } from "./commands/setup.js";
|
|
8
|
+
import { getLanIp } from "./utils/lan-ip.js";
|
|
9
|
+
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name("linkshell")
|
|
15
|
+
.description(
|
|
16
|
+
"Bridge a local Claude/Codex terminal session to a remote gateway",
|
|
17
|
+
)
|
|
18
|
+
.version("0.1.0");
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("start")
|
|
22
|
+
.description("Start a bridge session")
|
|
23
|
+
.option("--gateway <url>", "Gateway websocket URL (omit to start built-in gateway)", config.gateway)
|
|
24
|
+
.option(
|
|
25
|
+
"--pairing-gateway <url-or-host>",
|
|
26
|
+
"Public HTTP gateway used in QR/deep link output",
|
|
27
|
+
config.pairingGateway,
|
|
28
|
+
)
|
|
29
|
+
.option("--port <port>", "Port for built-in gateway (default: 8787)", "8787")
|
|
30
|
+
.option("--session-id <id>", "Session identifier (auto-created if omitted)")
|
|
31
|
+
.option(
|
|
32
|
+
"--provider <provider>",
|
|
33
|
+
"claude | codex | custom",
|
|
34
|
+
config.provider ?? "claude",
|
|
35
|
+
)
|
|
36
|
+
.option("--command <command>", "Override provider executable", config.command)
|
|
37
|
+
.option(
|
|
38
|
+
"--client-name <name>",
|
|
39
|
+
"Display name for this CLI",
|
|
40
|
+
config.clientName ?? "local-cli",
|
|
41
|
+
)
|
|
42
|
+
.option(
|
|
43
|
+
"--cols <cols>",
|
|
44
|
+
"Initial terminal columns",
|
|
45
|
+
String(config.cols ?? 120),
|
|
46
|
+
)
|
|
47
|
+
.option("--rows <rows>", "Initial terminal rows", String(config.rows ?? 36))
|
|
48
|
+
.option("--verbose", "Enable verbose logging")
|
|
49
|
+
.allowUnknownOption(true)
|
|
50
|
+
.action(async (options, command) => {
|
|
51
|
+
const passthroughArgs = command.args.filter(
|
|
52
|
+
(value: string) => value !== "--",
|
|
53
|
+
);
|
|
54
|
+
const providerConfig = resolveProviderConfig({
|
|
55
|
+
provider: options.provider,
|
|
56
|
+
command: options.command,
|
|
57
|
+
args: passthroughArgs,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let gatewayUrl = options.gateway as string | undefined;
|
|
61
|
+
let gatewayHttpUrl: string;
|
|
62
|
+
let pairingGateway = options.pairingGateway as string | undefined;
|
|
63
|
+
let embeddedGatewayHandle: { close: () => Promise<void> } | undefined;
|
|
64
|
+
|
|
65
|
+
if (!gatewayUrl) {
|
|
66
|
+
// Start built-in gateway
|
|
67
|
+
const { startEmbeddedGateway } = await import("@linkshell/gateway/embedded");
|
|
68
|
+
const port = Number(options.port);
|
|
69
|
+
const gw = await startEmbeddedGateway({
|
|
70
|
+
port,
|
|
71
|
+
logLevel: options.verbose ? "debug" : "warn",
|
|
72
|
+
silent: false,
|
|
73
|
+
});
|
|
74
|
+
embeddedGatewayHandle = gw;
|
|
75
|
+
gatewayUrl = gw.wsUrl;
|
|
76
|
+
gatewayHttpUrl = gw.httpUrl;
|
|
77
|
+
|
|
78
|
+
// Auto-detect LAN IP for QR code
|
|
79
|
+
const lanIp = getLanIp();
|
|
80
|
+
if (!pairingGateway && lanIp !== "127.0.0.1") {
|
|
81
|
+
pairingGateway = `http://${lanIp}:${gw.port}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.stderr.write(`\n Built-in gateway started on port ${gw.port}\n`);
|
|
85
|
+
if (pairingGateway) {
|
|
86
|
+
process.stderr.write(` LAN address: ${pairingGateway}\n`);
|
|
87
|
+
}
|
|
88
|
+
process.stderr.write("\n");
|
|
89
|
+
} else {
|
|
90
|
+
gatewayHttpUrl = gatewayUrl
|
|
91
|
+
.replace(/\/ws\/?$/, "")
|
|
92
|
+
.replace(/^wss:/, "https:")
|
|
93
|
+
.replace(/^ws:/, "http:");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const session = new BridgeSession({
|
|
97
|
+
gatewayUrl,
|
|
98
|
+
gatewayHttpUrl,
|
|
99
|
+
pairingGateway,
|
|
100
|
+
sessionId: options.sessionId,
|
|
101
|
+
cols: Number(options.cols),
|
|
102
|
+
rows: Number(options.rows),
|
|
103
|
+
clientName: options.clientName,
|
|
104
|
+
verbose: Boolean(options.verbose),
|
|
105
|
+
providerConfig,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Clean up embedded gateway on exit
|
|
109
|
+
if (embeddedGatewayHandle) {
|
|
110
|
+
const cleanup = async () => {
|
|
111
|
+
await embeddedGatewayHandle!.close();
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGINT", () => { cleanup().then(() => process.exit(0)); });
|
|
114
|
+
process.on("SIGTERM", () => { cleanup().then(() => process.exit(0)); });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await session.start();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
program
|
|
121
|
+
.command("doctor")
|
|
122
|
+
.description("Check your environment and connectivity")
|
|
123
|
+
.option("--gateway <url>", "Gateway URL to test", config.gateway)
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
await runDoctor(options.gateway);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command("setup")
|
|
130
|
+
.description("Interactive setup wizard")
|
|
131
|
+
.action(async () => {
|
|
132
|
+
await runSetup();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
process.stderr.write(`${message}\n`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type ProviderName = "claude" | "codex" | "custom";
|
|
4
|
+
|
|
5
|
+
export interface ProviderConfig {
|
|
6
|
+
provider: ProviderName;
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
env: NodeJS.ProcessEnv;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function which(bin: string): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
return execSync(`which ${bin}`, { encoding: "utf8" }).trim() || undefined;
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveClaudeProvider(input: {
|
|
21
|
+
command?: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
}): ProviderConfig {
|
|
24
|
+
const command = input.command ?? "claude";
|
|
25
|
+
const resolved = which(command);
|
|
26
|
+
if (!resolved) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Claude CLI not found ("${command}"). Install it with: npm install -g @anthropic-ai/claude-code`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Claude starts an interactive REPL by default — that's exactly what we want in the PTY.
|
|
33
|
+
// Pass through any extra args the user provided.
|
|
34
|
+
return {
|
|
35
|
+
provider: "claude",
|
|
36
|
+
command: resolved,
|
|
37
|
+
args: input.args,
|
|
38
|
+
env: { ...process.env },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveCodexProvider(input: {
|
|
43
|
+
command?: string;
|
|
44
|
+
args: string[];
|
|
45
|
+
}): ProviderConfig {
|
|
46
|
+
const command = input.command ?? "codex";
|
|
47
|
+
const resolved = which(command);
|
|
48
|
+
if (!resolved) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Codex CLI not found ("${command}"). Install it with: npm install -g @openai/codex`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
"[warn] OPENAI_API_KEY not set — Codex may fail to authenticate\n",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
provider: "codex",
|
|
62
|
+
command: resolved,
|
|
63
|
+
args: input.args,
|
|
64
|
+
env: { ...process.env },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveProviderConfig(input: {
|
|
69
|
+
provider: ProviderName;
|
|
70
|
+
command?: string;
|
|
71
|
+
args: string[];
|
|
72
|
+
}): ProviderConfig {
|
|
73
|
+
switch (input.provider) {
|
|
74
|
+
case "claude":
|
|
75
|
+
return resolveClaudeProvider(input);
|
|
76
|
+
case "codex":
|
|
77
|
+
return resolveCodexProvider(input);
|
|
78
|
+
case "custom": {
|
|
79
|
+
if (!input.command) {
|
|
80
|
+
throw new Error("custom provider requires --command");
|
|
81
|
+
}
|
|
82
|
+
const resolved = which(input.command);
|
|
83
|
+
return {
|
|
84
|
+
provider: "custom",
|
|
85
|
+
command: resolved ?? input.command,
|
|
86
|
+
args: input.args,
|
|
87
|
+
env: { ...process.env },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|