* feat(setup)!: expand agent integrations and simplify onboarding Add native configuration formats for OpenCode, Antigravity, GitHub Copilot CLI, VS Code, and Devin. Preserve JSONC comments and unrelated settings while making setup and uninstall ownership-aware and idempotent. Select agent targets before planning, separate discovery from intent, and keep agent-only setup independent of optional provider prerequisites. Cover configuration boundaries and interactive behavior in packaged checks. BREAKING CHANGE: unscoped setup refreshes only existing REA-owned registrations. Use --all-detected to configure every detected client. Setup JSON contains a compact doctor summary; use rea doctor for full diagnostics. Cancellation and successful dry runs now exit successfully. * feat(cli)!: replace upgrade with verified updates Resolve an exact release through npm's configured registry, update only the identified installation, and verify its new executable before reporting success. Return a read-only maintenance plan for existing enabled REA integrations. BREAKING CHANGE: replace rea upgrade with rea update; no alias is retained. Successful update JSON uses status "updated", reports the verified installed version, and returns an unapplied maintenance plan instead of opening setup.
181 lines
6.5 KiB
JavaScript
181 lines
6.5 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { createServer } from "node:http";
|
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { exec, json } from "./lib/verify-package-core.mjs";
|
|
|
|
/** Exercise the packaged updater with real npm and a local release registry fixture. */
|
|
export async function verifyPackageUpdate({
|
|
prefix,
|
|
packageRoot,
|
|
tarball,
|
|
workspace,
|
|
environment,
|
|
}) {
|
|
const metadataPath = join(packageRoot, "package.json");
|
|
const metadata = json(await readFile(metadataPath, "utf8"));
|
|
const archive = await readFile(tarball);
|
|
const entry = join(packageRoot, "scripts", "rea.mjs");
|
|
const home = join(workspace, "update-home");
|
|
const codexPath = join(home, ".codex", "config.toml");
|
|
const originalConfig =
|
|
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@0.0.0", "mcp"]\n';
|
|
await mkdir(dirname(codexPath), { recursive: true });
|
|
await writeFile(codexPath, originalConfig);
|
|
const server = createServer((request, response) => {
|
|
void serveRegistry(request, response).catch((error) => {
|
|
response.writeHead(500);
|
|
response.end(String(error));
|
|
});
|
|
});
|
|
let registry;
|
|
async function serveRegistry(request, response) {
|
|
const pathname = new URL(request.url, registry).pathname;
|
|
if (pathname === "/rea-agents") {
|
|
response.setHeader("Content-Type", "application/json");
|
|
response.end(
|
|
JSON.stringify({
|
|
name: metadata.name,
|
|
"dist-tags": { latest: metadata.version },
|
|
versions: {
|
|
[metadata.version]: {
|
|
...metadata,
|
|
dist: {
|
|
tarball: `${registry}/release.tgz`,
|
|
integrity: `sha512-${createHash("sha512").update(archive).digest("base64")}`,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
} else if (pathname === "/release.tgz") {
|
|
response.end(archive);
|
|
} else if (request.method === "POST") {
|
|
response.setHeader("Content-Type", "application/json");
|
|
response.end("{}");
|
|
} else {
|
|
const upstream = await fetch(`https://registry.npmjs.org${request.url}`);
|
|
response.writeHead(upstream.status, {
|
|
"Content-Type":
|
|
upstream.headers.get("content-type") ?? "application/octet-stream",
|
|
});
|
|
response.end(Buffer.from(await upstream.arrayBuffer()));
|
|
}
|
|
}
|
|
await new Promise((resolveListen, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolveListen);
|
|
});
|
|
try {
|
|
const address = server.address();
|
|
if (typeof address !== "object" || address === null)
|
|
throw new Error("Registry fixture did not bind");
|
|
registry = `http://127.0.0.1:${address.port}`;
|
|
// Model an older installed release's identity while retaining the new updater
|
|
// under test. The registry supplies the unmodified packed target artifact.
|
|
const generatedPath = join(
|
|
packageRoot,
|
|
"dist",
|
|
"generatedPackageMetadata.js",
|
|
);
|
|
const generated = await readFile(generatedPath, "utf8");
|
|
const previousVersion = "0.0.0";
|
|
const previousGenerated = generated.replace(
|
|
`version: "${metadata.version}"`,
|
|
`version: "${previousVersion}"`,
|
|
);
|
|
if (previousGenerated === generated)
|
|
throw new Error("Could not create previous-release identity fixture");
|
|
await writeFile(generatedPath, previousGenerated);
|
|
await writeFile(
|
|
metadataPath,
|
|
JSON.stringify({ ...metadata, version: previousVersion }),
|
|
);
|
|
const runtimeEnvironment = {
|
|
...environment,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
APPDATA: join(home, "AppData", "Roaming"),
|
|
CLAUDE_CONFIG_DIR: home,
|
|
CODEX_HOME: join(home, ".codex"),
|
|
COPILOT_HOME: join(home, ".copilot"),
|
|
XDG_CONFIG_HOME: join(home, ".config"),
|
|
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
|
|
npm_config_prefix: prefix,
|
|
npm_config_registry: registry,
|
|
};
|
|
const previous = (
|
|
await exec(process.execPath, [entry, "--version"], {
|
|
env: runtimeEnvironment,
|
|
})
|
|
).stdout.trim();
|
|
if (previous !== previousVersion)
|
|
throw new Error(`Previous-release fixture reported ${previous}`);
|
|
const harness = join(workspace, "verify-update.mjs");
|
|
await writeFile(
|
|
harness,
|
|
`
|
|
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/Update.js")).href)};
|
|
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/UpdateRuntime.js")).href)};
|
|
const host = systemUpdateHost(${JSON.stringify(packageRoot)}, ${JSON.stringify(home)});
|
|
const result = await runUpdate(${JSON.stringify(previousVersion)}, host, "structured");
|
|
process.stdout.write(JSON.stringify(result));
|
|
if (result.status === "failed") process.exitCode = 1;
|
|
`,
|
|
);
|
|
const result = json(
|
|
(
|
|
await exec(process.execPath, [harness], {
|
|
env: runtimeEnvironment,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
})
|
|
).stdout,
|
|
);
|
|
if (
|
|
result.status !== "updated" ||
|
|
result.installedVersion !== metadata.version ||
|
|
result.command.at(-1) !== `${metadata.name}@${metadata.version}` ||
|
|
result.maintenance.status !== "planned" ||
|
|
result.maintenance.scope.clients.join(",") !== "codex" ||
|
|
result.maintenance.scope.skill !== false
|
|
)
|
|
throw new Error(
|
|
`Packaged updater failed its exact-release contract: ${JSON.stringify(result)}`,
|
|
);
|
|
if ((await readFile(codexPath, "utf8")) !== originalConfig)
|
|
throw new Error(
|
|
"Update applied integration maintenance without approval",
|
|
);
|
|
const current = (
|
|
await exec(process.execPath, [entry, "--version"], {
|
|
env: runtimeEnvironment,
|
|
})
|
|
).stdout.trim();
|
|
if (current !== metadata.version)
|
|
throw new Error(`Updated executable reported ${current}`);
|
|
const help = (
|
|
await exec(process.execPath, [entry, "--help"], {
|
|
env: runtimeEnvironment,
|
|
})
|
|
).stdout;
|
|
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
|
|
throw new Error(
|
|
"Updated CLI did not expose update as its sole maintenance command",
|
|
);
|
|
return {
|
|
installed_version: result.installedVersion,
|
|
exact_release: true,
|
|
maintenance_read_only: true,
|
|
};
|
|
} finally {
|
|
server.closeAllConnections();
|
|
await new Promise((resolveClose, reject) =>
|
|
server.close((error) =>
|
|
error === undefined ? resolveClose() : reject(error),
|
|
),
|
|
);
|
|
}
|
|
}
|