#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { getPublicWorkspacePackages } from "./release-packages.mjs"; const codingAgentName = "@earendil-works/pi-coding-agent"; const developmentPackages = new Set(["pi-client", "pi-protocol", "pi-server"].map((name) => `@earendil-works/${name}`)); function run(command, args, options = {}) { console.log(`$ ${[command, ...args].join(" ")}`); const result = spawnSync(command, args, { encoding: "utf8", shell: process.platform === "win32", timeout: 300_000, ...options, }); if (result.status !== 0) { throw new Error(`Command failed: ${command} ${args.join(" ")}\n${result.stdout ?? ""}${result.stderr ?? ""}${result.error?.message ?? ""}`); } return result.stdout; } export function packReleasePackages(packages, tarballDirectory) { mkdirSync(tarballDirectory, { recursive: true }); const tarballs = new Map(); for (const pkg of packages) { const manifest = JSON.parse(readFileSync(join(pkg.directory, "package.json"), "utf8")); if (manifest.name !== pkg.name) throw new Error(`Unexpected package name in ${pkg.directory}`); const output = run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", tarballDirectory], { cwd: pkg.directory }); // npm <11.6 returns an array; newer npm can return an object keyed by package name. const parsed = JSON.parse(output); const packed = Array.isArray(parsed) ? parsed[0] : Object.values(parsed)[0]; tarballs.set(pkg.name, join(tarballDirectory, packed.filename)); } return tarballs; } export function installCodingAgentConsumer(directory, tarballs, packageManager = "npm") { mkdirSync(directory, { recursive: true }); const overrides = Object.fromEntries([...tarballs].map(([name, path]) => [ name, `file:./${relative(directory, path).replaceAll("\\", "/")}`, ])); if (!overrides[codingAgentName]) throw new Error("Missing coding-agent tarball"); // Only coding-agent is a direct dependency. Overrides select local artifacts // for declared transitive dependencies without installing undeclared packages. const manifest = { private: true, dependencies: { [codingAgentName]: overrides[codingAgentName] }, overrides, }; writeFileSync(join(directory, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`); const installArgs = packageManager === "bun" ? ["--production"] : ["--omit=dev", "--no-audit", "--no-fund"]; run(packageManager, ["install", "--ignore-scripts", ...installArgs], { cwd: directory }); } function checkInstalledPackages(nodeModules, seen = new Set()) { if (!existsSync(nodeModules)) return; const directories = readdirSync(nodeModules) .filter((name) => !name.startsWith(".")) .flatMap((name) => name.startsWith("@") ? readdirSync(join(nodeModules, name)).map((child) => join(nodeModules, name, child)) : [join(nodeModules, name)]); for (const directory of directories) { if (!existsSync(join(directory, "package.json"))) continue; const path = realpathSync(directory); if (seen.has(path)) continue; seen.add(path); const manifest = JSON.parse(readFileSync(join(path, "package.json"), "utf8")); if (developmentPackages.has(manifest.name)) throw new Error(`${manifest.name} must not be installed: ${path}`); checkInstalledPackages(join(path, "node_modules"), seen); } } export function smokeTestCodingAgentConsumer(directory, runtime = process.execPath) { checkInstalledPackages(join(directory, "node_modules")); const packageDir = join(directory, "node_modules", codingAgentName); const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")); for (const path of ["dist/client", "dist/experimental", "dist/cli/experimental", "dist/bundle/client.js", "dist/bundle/coordinator.js"]) { if (existsSync(join(packageDir, path))) throw new Error(`Published package contains development-only code: ${path}`); } const home = mkdtempSync(join(directory, "smoke-home-")); const entry = join(directory, "smoke-sdk.mjs"); const env = { PATH: process.env.PATH, HOME: home, USERPROFILE: home, APPDATA: home, LOCALAPPDATA: home, XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, PI_CODING_AGENT_DIR: join(home, ".pi", "agent"), PI_OFFLINE: "1", PI_TELEMETRY: "0", }; for (const name of ["SystemRoot", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT"]) { if (process.env[name]) env[name] = process.env[name]; } try { writeFileSync(entry, `import assert from "node:assert/strict"; import { createAgentSession, SessionManager, ModelRuntime } from "${codingAgentName}"; assert.equal(typeof createAgentSession, "function"); assert.equal(typeof SessionManager.inMemory, "function"); assert.equal(typeof ModelRuntime.create, "function"); for (const name of ["pi-client", "pi-protocol", "pi-server"]) { assert.throws(() => import.meta.resolve("@earendil-works/" + name), /Cannot find|cannot find/, name + " must not be installed"); } for (const subpath of ["/client", "/experimental/plugin"]) { assert.throws(() => import.meta.resolve("${codingAgentName}" + subpath), /not exported|not defined|Cannot find|cannot find/); } `); run(runtime, [entry], { cwd: directory, env, timeout: 30_000 }); for (const cli of new Set([manifest.bin.pi, "dist/cli.js"])) { const output = run(runtime, [join(packageDir, cli), "--version"], { cwd: directory, env, timeout: 30_000 }); if (output.trim() !== manifest.version) throw new Error(`Unexpected version from ${cli}: ${output}`); } } finally { rmSync(entry, { force: true }); rmSync(home, { recursive: true, force: true }); } console.log(`Coding-agent SDK and CLI consumer smoke tests passed (${runtime}).`); } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { if (process.argv.length !== 2) throw new Error("Usage: node scripts/coding-agent-consumer.mjs"); const root = mkdtempSync(join(tmpdir(), "pi-package-consumer-")); try { const tarballs = packReleasePackages(getPublicWorkspacePackages(), join(root, "tarballs")); const directory = join(root, "consumer"); installCodingAgentConsumer(directory, tarballs); smokeTestCodingAgentConsumer(directory); } finally { rmSync(root, { recursive: true, force: true }); } }