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 });
	}
}