diff --git a/bun.lock b/bun.lock index 2679f92..a3e114a 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@openai/codex": "0.133.0", "pg": "^8.13.1", + "yaml": "^2.8.0", }, "devDependencies": { "@types/node": "^22.10.0", @@ -124,5 +125,7 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], } } diff --git a/package.json b/package.json index 3a7a82a..f299528 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "@openai/codex": "0.133.0", - "pg": "^8.13.1" + "pg": "^8.13.1", + "yaml": "^2.8.0" }, "devDependencies": { "@types/pg": "^8.11.10", diff --git a/src/common/aipod-specs.ts b/src/common/aipod-specs.ts index 6d4f5ea..a2ff2d2 100644 --- a/src/common/aipod-specs.ts +++ b/src/common/aipod-specs.ts @@ -1,12 +1,11 @@ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; +import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import { AgentRunError } from "./errors.js"; import type { AipodSpec, AipodSpecRecord, BackendProfile, CreateQueueTaskInput, ExecutionPolicy, JsonRecord, JsonValue, RenderAipodInput, RenderedAipodQueueTask, ResourceBundleRef, SessionRef, WorkspaceRef } from "./types.js"; import { backendProfileSpec, isBackendProfile } from "./backend-profiles.js"; import { asRecord, stableHash, validateCreateQueueTask, validateExecutionPolicy, validateResourceBundleRef, validateSessionRef } from "./validation.js"; -declare const Bun: { YAML: { parse(text: string): unknown; stringify(value: unknown): string } }; - const aipodApiVersion = "agentrun.pikastech.local/v0.1"; const aipodKind = "AipodSpec"; @@ -39,7 +38,7 @@ export async function applyAipodSpec(input: unknown, dir = aipodSpecDirectory()) const spec = aipodSpecFromInput(input, "api"); await mkdir(dir, { recursive: true }); const file = path.join(dir, `${fileSafeAipodName(spec.metadata.name)}.yaml`); - await writeFile(file, Bun.YAML.stringify(spec), "utf8"); + await writeFile(file, stringifyYaml(spec), "utf8"); const record = await loadAipodSpecFile(file); return { action: "aipod-spec-apply", mutation: true, item: summarizeAipodSpecRecord(record), valuesPrinted: false }; } @@ -53,7 +52,7 @@ export async function deleteAipodSpec(name: string, dir = aipodSpecDirectory()): export function parseAipodSpecYaml(text: string, source = "stdin"): AipodSpec { let parsed: unknown; try { - parsed = Bun.YAML.parse(text); + parsed = parseYaml(text); } catch (error) { throw new AgentRunError("schema-invalid", `aipod-spec YAML parse failed: ${error instanceof Error ? error.message : String(error)}`, { httpStatus: 400, details: { source, valuesPrinted: false } }); } diff --git a/src/selftest/cases/76-aipod-spec.ts b/src/selftest/cases/76-aipod-spec.ts index 6d1767b..f672b08 100644 --- a/src/selftest/cases/76-aipod-spec.ts +++ b/src/selftest/cases/76-aipod-spec.ts @@ -12,6 +12,10 @@ const selfTest: SelfTestCase = async (context) => { const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: new MemoryAgentRunStore(), aipodSpecDir: path.join(context.root, "config", "aipods") }); try { const client = new ManagerClient(server.baseUrl); + const parsedWithoutBunGlobal = await runNodeParserCompat(context); + assert.equal(parsedWithoutBunGlobal.name, "Artificer"); + assert.equal(parsedWithoutBunGlobal.hasBunGlobal, false); + const listed = await client.get("/api/v1/aipod-specs") as JsonRecord; assert.equal(listed.action, "aipod-spec-list"); assert.equal((listed.items as JsonRecord[]).some((item) => item.name === "Artificer"), true); @@ -68,7 +72,7 @@ const selfTest: SelfTestCase = async (context) => { assert.equal(commands.some((item) => item.includes("aipod-specs render ")), true); assert.equal(commands.some((item) => item.includes("queue submit --aipod ")), true); assertNoSecretLeak(submitPlan); - return { name: "aipod-spec", tests: ["aipod-spec-artificer-yaml-render", "aipod-spec-git-mirror-url", "queue-submit-aipod-dry-run", "aipod-cli-help"] }; + return { name: "aipod-spec", tests: ["aipod-spec-yaml-parser-runtime-compatible", "aipod-spec-artificer-yaml-render", "aipod-spec-git-mirror-url", "queue-submit-aipod-dry-run", "aipod-cli-help"] }; } finally { await new Promise((resolve) => server.server.close(() => resolve())); } @@ -76,6 +80,17 @@ const selfTest: SelfTestCase = async (context) => { export default selfTest; +async function runNodeParserCompat(context: { root: string }): Promise { + const script = `import { readFileSync } from "node:fs"; +import { parseAipodSpecYaml } from "./src/common/aipod-specs.ts"; +const spec = parseAipodSpecYaml(readFileSync("config/aipods/artificer.yaml", "utf8"), "selftest-no-bun-yaml"); +console.log(JSON.stringify({ name: spec.metadata.name, hasBunGlobal: typeof globalThis.Bun !== "undefined" }));`; + const proc = spawn("node", ["--import", "tsx", "--eval", script], { cwd: context.root, stdio: ["ignore", "pipe", "pipe"] }); + const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise((resolve) => proc.on("close", resolve))]); + assert.equal(code, 0, stderr || stdout); + return JSON.parse(stdout) as JsonRecord; +} + async function runCliJson(context: { root: string }, managerUrl: string, args: string[]): Promise { const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, "--manager-url", managerUrl, ...args], { stdio: ["ignore", "pipe", "pipe"] }); const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise((resolve) => proc.on("close", resolve))]); diff --git a/tools/apply_patch b/tools/apply_patch old mode 100644 new mode 100755