#!/usr/bin/env node
import { createReadStream } from "node:fs";
import { copyFile, mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const artifactsDir = join(root, "packages", "coding-agent", "release", "artifacts");
let serverRoot;
let server;
function run(command, args, options = {}) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn(command, args, {
cwd: root,
stdio: "inherit",
...options,
});
child.once("error", rejectRun);
child.once("exit", (code, signal) => {
if (code === 0) resolveRun();
else rejectRun(new Error(`${command} exited with ${signal ?? `code ${code ?? "unknown"}`}`));
});
});
}
function serveFiles(directory) {
return createServer(async (request, response) => {
try {
if (request.method !== "GET" && request.method !== "HEAD") {
response.writeHead(405).end();
return;
}
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
const path = resolve(directory, pathname.replace(/^\/+/, ""));
const pathFromRoot = relative(directory, path);
if (pathFromRoot.startsWith(`..${sep}`) || pathFromRoot === "..") {
response.writeHead(403).end();
return;
}
const info = await stat(path);
if (!info.isFile()) {
response.writeHead(404).end();
return;
}
response.writeHead(200, {
"content-length": info.size,
"content-type": path.endsWith(".json") ? "application/json" : "application/octet-stream",
});
if (request.method === "HEAD") response.end();
else createReadStream(path).pipe(response);
} catch {
response.writeHead(404).end();
}
});
}
async function listen(httpServer) {
await new Promise((resolveListen, rejectListen) => {
httpServer.once("error", rejectListen);
httpServer.listen(0, "127.0.0.1", resolveListen);
});
const address = httpServer.address();
if (!address || typeof address === "string") throw new Error("could not allocate a loopback update port");
return address.port;
}
async function stageRelease(version) {
const releaseDir = join(serverRoot, "releases", `v${version}`);
await mkdir(releaseDir, { recursive: true });
const manifest = JSON.parse(await readFile(join(artifactsDir, "latest.json"), "utf8"));
if (manifest.version !== `v${version}` || !Array.isArray(manifest.tarballs)) {
throw new Error("the packed release manifest is invalid");
}
await copyFile(join(artifactsDir, "latest.json"), join(serverRoot, "latest.json"));
await copyFile(join(artifactsDir, "SHA256SUMS"), join(releaseDir, "SHA256SUMS"));
for (const tarball of manifest.tarballs) {
if (!tarball || typeof tarball.file !== "string" || basename(tarball.file) !== tarball.file) {
throw new Error("the packed release contains an invalid tarball path");
}
await copyFile(join(artifactsDir, tarball.file), join(releaseDir, tarball.file));
}
}
async function main() {
if (process.argv.includes("--help")) {
console.log("Usage: node scripts/update-prime-local.mjs");
console.log("Builds the working tree and performs a native update through a temporary loopback release server.");
return;
}
const packageJson = JSON.parse(await readFile(join(root, "packages", "coding-agent", "package.json"), "utf8"));
const version = packageJson.version;
if (typeof version !== "string" || !version) throw new Error("could not read the Prime Agent package version");
serverRoot = await mkdtemp(join(tmpdir(), "prime-agent-local-update-"));
server = serveFiles(serverRoot);
const port = await listen(server);
const baseUrl = `http://127.0.0.1:${port}`;
console.log("Building Prime Agent packages...");
await run("npm", ["run", "build"]);
await run(process.execPath, ["scripts/pack-prime-agent-release.mjs", "--base-url", baseUrl]);
await stageRelease(version);
const manifestResponse = await fetch(`${baseUrl}/latest.json`);
if (!manifestResponse.ok) throw new Error("the loopback release manifest is not reachable");
console.log(`Local v${version} release is ready on ${baseUrl}`);
console.log("Triggering native update and daemon restart coordinator...");
await run("prime-agent", ["update", "--force"], {
env: { ...process.env, PRIME_AGENT_DOWNLOAD_BASE_URL: baseUrl },
});
console.log("Update complete. Daemon restarted and sessions restored.");
}
try {
await main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
} finally {
if (server) await new Promise((resolveClose) => server.close(resolveClose));
if (serverRoot) await rm(serverRoot, { recursive: true, force: true });
}