feat(M1): Project Scaffold
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import minimist from "minimist";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
type ClientName,
|
||||
type ClientInfo,
|
||||
type ExecResult,
|
||||
ClientNotFoundError,
|
||||
} from "./types.js";
|
||||
|
||||
const CLIENT_NAMES: ClientName[] = ["codex", "claude", "opencode"];
|
||||
const VERSION_FLAGS: Record<ClientName, string> = {
|
||||
codex: "--version",
|
||||
claude: "--version",
|
||||
opencode: "--version",
|
||||
};
|
||||
|
||||
function whichSync(cmd: string): string | undefined {
|
||||
const isWin = process.platform === "win32";
|
||||
const result = spawnSync(isWin ? "where" : "which", [cmd], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (result.status === 0) {
|
||||
return result.stdout.trim().split("\n")[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function discoverClients(): ClientInfo[] {
|
||||
return CLIENT_NAMES.map((name) => {
|
||||
const path = whichSync(name);
|
||||
if (!path) {
|
||||
return { name, found: false };
|
||||
}
|
||||
const versionResult = spawnSync(name, [VERSION_FLAGS[name]], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const version =
|
||||
versionResult.status === 0 ? versionResult.stdout.trim() : undefined;
|
||||
return { name, path, version, found: true };
|
||||
});
|
||||
}
|
||||
|
||||
export function execClient(
|
||||
client: ClientName,
|
||||
prompt: string,
|
||||
clientPath?: string
|
||||
): ExecResult {
|
||||
const path =
|
||||
clientPath ?? discoverClients().find((c) => c.name === client)?.path;
|
||||
if (!path) {
|
||||
throw new ClientNotFoundError(client);
|
||||
}
|
||||
const result = spawnSync(client, [prompt], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
return {
|
||||
stdout: result.stdout ?? "",
|
||||
stderr: result.stderr ?? "",
|
||||
exitCode: typeof result.status === "number" ? result.status : 1,
|
||||
};
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`AI CLI Dispatch
|
||||
|
||||
Usage:
|
||||
ai-cli-dispatch list [--json]
|
||||
ai-cli-dispatch exec --client <client> --prompt <prompt>
|
||||
ai-cli-dispatch --help
|
||||
|
||||
Clients: codex, claude, opencode`);
|
||||
}
|
||||
|
||||
export async function main(argv: string[]): Promise<number> {
|
||||
const rawArgs = argv.slice(2);
|
||||
// When run via tsx, argv[2] is the script path (e.g. src/cli.ts); skip it.
|
||||
const parseArgs =
|
||||
rawArgs[0]?.includes("cli.ts") ? rawArgs.slice(1) : rawArgs;
|
||||
|
||||
const args = minimist(parseArgs, {
|
||||
string: ["client", "prompt"],
|
||||
boolean: ["json", "help", "debug"],
|
||||
alias: { h: "help" },
|
||||
});
|
||||
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const command = args._[0];
|
||||
|
||||
if (command === "list") {
|
||||
const clients = discoverClients();
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(clients, null, 2));
|
||||
} else {
|
||||
for (const c of clients) {
|
||||
const status = c.found
|
||||
? `✓ ${c.version ?? "unknown version"}`
|
||||
: "✗ not found";
|
||||
console.log(`${c.name}: ${status}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "exec") {
|
||||
const client = args.client as ClientName | undefined;
|
||||
const prompt = args.prompt as string | undefined;
|
||||
if (!client || !CLIENT_NAMES.includes(client)) {
|
||||
console.error(
|
||||
`Error: --client must be one of ${CLIENT_NAMES.join(", ")}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (!prompt) {
|
||||
console.error("Error: --prompt is required");
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
const result = execClient(client, prompt);
|
||||
if (result.stdout) process.stdout.write(result.stdout);
|
||||
if (result.stderr) process.stderr.write(result.stderr);
|
||||
return result.exitCode;
|
||||
} catch (err) {
|
||||
if (err instanceof ClientNotFoundError) {
|
||||
console.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (command) {
|
||||
console.error(`Unknown command: ${command}`);
|
||||
} else {
|
||||
console.error("Error: no command given");
|
||||
}
|
||||
printHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const isMain =
|
||||
import.meta.url.startsWith("file://") &&
|
||||
!!process.argv[1] &&
|
||||
import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
|
||||
|
||||
if (isMain) {
|
||||
main(process.argv).then((code) => process.exit(code));
|
||||
}
|
||||
Reference in New Issue
Block a user