Skip to content

Commit f001b1d

Browse files
koki-developclaude
andcommitted
feat: Add run subcommand for executing source files in Codize sandbox
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5d80391 commit f001b1d

File tree

3 files changed

+126
-7
lines changed

3 files changed

+126
-7
lines changed

src/commands/run.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { readFileSync } from "node:fs";
2+
import { basename, extname } from "node:path";
3+
import { CodizeApiError, CodizeClient } from "@codize/sdk";
4+
import type { Command } from "commander";
5+
import { CliError } from "../error.ts";
6+
7+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
8+
".ts": "typescript",
9+
".js": "javascript",
10+
".py": "python",
11+
".rb": "ruby",
12+
".go": "go",
13+
".rs": "rust",
14+
};
15+
16+
function detectLanguage(filePath: string): string | null {
17+
const ext = extname(filePath).toLowerCase();
18+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
19+
}
20+
21+
function resolveLanguage(files: string[]): string {
22+
const entrypoint = files[0];
23+
if (entrypoint == null) {
24+
throw new CliError("No files specified.");
25+
}
26+
const detected = detectLanguage(entrypoint);
27+
if (detected == null) {
28+
throw new CliError(
29+
`Cannot detect language for '${entrypoint}'. Use --language to specify.`,
30+
);
31+
}
32+
return detected;
33+
}
34+
35+
function readFiles(files: string[]): { name: string; content: string }[] {
36+
return files.map((filePath) => {
37+
let content: string;
38+
try {
39+
content = readFileSync(filePath, "utf-8");
40+
} catch (err) {
41+
const message = err instanceof Error ? err.message : String(err);
42+
throw new CliError(`Cannot read file '${filePath}': ${message}`);
43+
}
44+
return { name: basename(filePath), content };
45+
});
46+
}
47+
48+
export function registerRunCommand(program: Command): void {
49+
program
50+
.command("run")
51+
.description("Execute source files in the Codize sandbox")
52+
.argument("<files...>", "Source files to execute")
53+
.option(
54+
"-l, --language <language>",
55+
"Language override (auto-detected from extension by default)",
56+
)
57+
.option(
58+
"-k, --api-key <key>",
59+
"Codize API key (defaults to CODIZE_API_KEY env var)",
60+
)
61+
.action(
62+
async (
63+
files: string[],
64+
options: { language?: string; apiKey?: string },
65+
) => {
66+
const apiKey = options.apiKey ?? process.env["CODIZE_API_KEY"];
67+
if (!apiKey) {
68+
throw new CliError(
69+
"API key is required. Set CODIZE_API_KEY or use --api-key.",
70+
);
71+
}
72+
73+
const language = options.language ?? resolveLanguage(files);
74+
const filePayloads = readFiles(files);
75+
76+
const client = new CodizeClient({ apiKey });
77+
let result: Awaited<ReturnType<typeof client.sandbox.execute>>;
78+
try {
79+
result = await client.sandbox.execute({
80+
language,
81+
files: filePayloads,
82+
});
83+
} catch (err) {
84+
if (err instanceof CodizeApiError) {
85+
throw new CliError(
86+
`API error [${err.code}] ${err.message} (HTTP ${err.status})`,
87+
);
88+
}
89+
throw err;
90+
}
91+
92+
if (result.data.compile != null && result.data.compile.output !== "") {
93+
process.stderr.write(result.data.compile.output);
94+
}
95+
96+
process.stdout.write(result.data.run.output);
97+
if (result.data.compile != null && result.data.compile.exitCode !== 0) {
98+
process.exitCode = result.data.compile.exitCode ?? 1;
99+
} else {
100+
process.exitCode = result.data.run.exitCode ?? 1;
101+
}
102+
},
103+
);
104+
}

src/error.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class CliError extends Error {
2+
readonly exitCode: number;
3+
4+
constructor(message: string, exitCode: number = 1) {
5+
super(message);
6+
this.name = "CliError";
7+
this.exitCode = exitCode;
8+
}
9+
}

src/index.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { Command } from "commander";
22
import packageJson from "../package.json" with { type: "json" };
3+
import { CliError } from "./error.ts";
4+
import { registerRunCommand } from "./commands/run.ts";
35

46
const program = new Command();
57

6-
program
7-
.name("TODO")
8-
.version(packageJson.version)
9-
.action(() => {
10-
console.log("hello world");
11-
});
8+
program.name("codize").version(packageJson.version);
129

13-
program.parse(process.argv);
10+
registerRunCommand(program);
11+
12+
program.parseAsync(process.argv).catch((err: unknown) => {
13+
const message =
14+
err instanceof CliError
15+
? err.message
16+
: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`;
17+
process.stderr.write(`error: ${message}\n`);
18+
process.exit(err instanceof CliError ? err.exitCode : 1);
19+
});

0 commit comments

Comments
 (0)