Move the action from Deno to Node.js and Vite+
Agents / agent (pull_request) Has been skipped
Check / check (pull_request) Successful in 58s

This commit is contained in:
Danny Kim
2026-10-16 02:20:49 +09:00
parent 9b55e98d8c
commit 8f23da2b42
45 changed files with 5664 additions and 1731 deletions
+119
View File
@@ -0,0 +1,119 @@
// Runs Claude Code for one profile and returns its final result text.
import { createInterface } from "node:readline";
import type { Readable } from "node:stream";
import { isClaudeAuthFailure } from "./auth.ts";
import type { ClaudeFailureFields } from "./auth.ts";
import { chunks } from "./command.ts";
import { agentCommand } from "./isolation.ts";
import { isObject } from "./json.ts";
import type { JsonObject } from "./json.ts";
import { AgentAuthError, agentEnvironment } from "./profiles.ts";
import { PROCESS_ENV, WORKSPACE } from "./run-context.ts";
import type { RuntimeProfile } from "./runtime.ts";
// Claude Code's stream-json `result` event, in its own field names.
type ClaudeResult = ClaudeFailureFields &
Partial<Record<"is_error", boolean>> & {
subtype: string;
};
// Echoes stderr as it arrives and resolves with all of it.
async function copyError(stream: Readable): Promise<string> {
const decoder = new TextDecoder();
let error = "";
for await (const chunk of chunks(stream)) {
const text = decoder.decode(chunk, { stream: true });
error += text;
console.error(text);
}
return error + decoder.decode();
}
// Prints the visible text of each streamed message part.
function printParts(event: JsonObject): void {
const content = isObject(event.message) ? event.message.content : undefined;
for (const part of Array.isArray(content) ? content : []) {
if (isObject(part)) {
const text = part.thinking ?? part.text ?? part.name;
if (typeof text === "string" && text !== "") {
console.log(text);
}
}
}
}
function toResult(event: JsonObject): ClaudeResult {
const { is_error: isError, result, subtype, api_error_status: apiErrorStatus } = event;
return {
...(typeof isError === "boolean" && { is_error: isError }),
...(typeof result === "string" && { result }),
subtype: String(subtype),
...((typeof apiErrorStatus === "number" || apiErrorStatus === null) && {
api_error_status: apiErrorStatus,
}),
};
}
// Prints each streamed message part and returns the final `result` event.
async function readEvents(stream: Readable): Promise<ClaudeResult | undefined> {
let result: ClaudeResult | undefined;
for await (const line of createInterface({ input: stream, crlfDelay: Infinity })) {
if (line !== "") {
const event: unknown = JSON.parse(line);
if (isObject(event)) {
printParts(event);
if (event.type === "result") {
result = toResult(event);
}
}
}
}
return result;
}
function checkResult(result: ClaudeResult | undefined, error: string): string {
if (result?.is_error === true) {
const failure = new Error(result.result ?? `claude ended with ${result.subtype}`);
if (isClaudeAuthFailure(result)) {
throw new AgentAuthError("claude sign-in failed", { cause: failure });
}
throw failure;
}
if (result?.result === undefined) {
throw new Error(error.trim() || `claude ended with ${result?.subtype ?? "no result"}`);
}
return result.result;
}
export async function runClaude(
profile: RuntimeProfile,
token: string,
prompt: string,
): Promise<string> {
const args = [
"--print",
"--dangerously-skip-permissions",
"--model",
profile.model,
"--output-format",
"stream-json",
"--verbose",
prompt,
];
const claude = agentCommand(profile, "claude", args, {
cwd: WORKSPACE,
env: { ...agentEnvironment(PROCESS_ENV, profile, token), IS_SANDBOX: "1" },
clearEnv: true,
stdout: "piped",
stderr: "piped",
}).spawn();
const errorText = copyError(claude.stderr);
const result = await readEvents(claude.stdout);
const [status, error] = await Promise.all([claude.status, errorText]);
const body = checkResult(result, error);
if (!status.success) {
throw new Error(error.trim() || `claude exited with ${status.code}`);
}
return body;
}