File size: 6,447 Bytes
d4f7e1d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | #!/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 });
}
}
|