openpetswithchatandmcp/scripts/release-npm.mjs
2026-06-11 21:47:36 +02:00

194 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const scriptsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptsDir, "..");
const repository = "alvinunreal/openpets";
const publishOrder = [
"packages/sdk",
"packages/client",
"packages/agent-events",
"packages/mcp",
"packages/claude",
"packages/opencode",
"packages/cursor",
"packages/pi",
"packages/cli",
"packages/install-pet",
];
const allowedArgs = new Set([
"--yes",
"--dry-run",
"--skip-checks",
"--help",
]);
const rawArgs = process.argv.slice(2).filter((arg) => arg !== "--");
const options = parseArgs(rawArgs);
if (options.help) {
printHelp();
process.exit(0);
}
const packages = publishOrder.map((relativePath) => {
const packageDir = join(repoRoot, relativePath);
const packageJson = readJson(join(packageDir, "package.json"));
return { relativePath, packageDir, packageJson, name: packageJson.name, version: packageJson.version };
});
main();
function main() {
preflight();
assertPublishablePackages();
if (!options.skipChecks) {
run("pnpm", ["build"], { cwd: repoRoot });
run("pnpm", ["check"], { cwd: repoRoot });
}
const existing = findAlreadyPublishedPackages();
console.log("\nNPM publish plan:");
for (const pkg of packages) {
const alreadyPublished = existing.some((candidate) => candidate.name === pkg.name && candidate.version === pkg.version);
console.log(`- ${pkg.name}@${pkg.version}${alreadyPublished ? " (already published)" : ""}`);
}
if (!options.yes || options.dryRun) {
console.log("\nDry run: package publish commands will be validated without uploading.");
}
for (const pkg of packages) {
const alreadyPublished = existing.some((candidate) => candidate.name === pkg.name && candidate.version === pkg.version);
if (alreadyPublished) {
console.log(`\nSkipping already published ${pkg.name}@${pkg.version}`);
continue;
}
const args = ["publish", "--access", "public", "--tag", options.tag, "--no-git-checks"];
if (!options.yes || options.dryRun) args.push("--dry-run");
if (options.otp) args.push("--otp", options.otp);
run("pnpm", args, { cwd: pkg.packageDir });
}
if (options.yes && !options.dryRun) {
console.log("\nNPM packages published successfully.");
} else {
console.log("\nDry run complete. Re-run with --yes to publish to npm.");
}
}
function parseArgs(args) {
const parsed = { yes: false, dryRun: false, skipChecks: false, help: false, tag: "latest", otp: "" };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--tag" || arg === "--otp") {
const value = args[index + 1];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}.`);
if (arg === "--tag") parsed.tag = value;
if (arg === "--otp") parsed.otp = value;
index += 1;
continue;
}
if (!allowedArgs.has(arg)) throw new Error(`Unknown npm release option: ${arg}`);
if (arg === "--yes") parsed.yes = true;
if (arg === "--dry-run") parsed.dryRun = true;
if (arg === "--skip-checks") parsed.skipChecks = true;
if (arg === "--help") parsed.help = true;
}
return parsed;
}
function preflight() {
requireCommand("pnpm", ["--version"]);
requireCommand("npm", ["--version"]);
if (options.yes && !options.dryRun) run("npm", ["whoami"], { cwd: repoRoot });
const remoteUrl = commandOutput("git", ["remote", "get-url", "origin"], { cwd: repoRoot }).trim();
if (!remoteUrl.includes(repository)) throw new Error(`Expected origin remote to point at ${repository}. Current origin: ${remoteUrl}`);
const status = commandOutput("git", ["status", "--porcelain"], { cwd: repoRoot }).trim();
if (status) throw new Error(`Git working tree must be clean before npm release.\n${status}`);
run("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoRoot });
const upstream = commandOutput("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], { cwd: repoRoot }).trim();
if (!upstream) throw new Error("Release branch must have an upstream remote branch.");
run("git", ["fetch", "origin"], { cwd: repoRoot });
const localHead = commandOutput("git", ["rev-parse", "HEAD"], { cwd: repoRoot }).trim();
const remoteHead = commandOutput("git", ["rev-parse", upstream], { cwd: repoRoot }).trim();
if (localHead !== remoteHead) throw new Error(`HEAD must be pushed to ${upstream} before npm release.`);
}
function assertPublishablePackages() {
const versions = new Set(packages.map((pkg) => pkg.version));
if (versions.size !== 1) throw new Error(`Publishable packages must use one shared version. Found: ${[...versions].join(", ")}`);
for (const pkg of packages) {
if (!pkg.name || !pkg.version) throw new Error(`${pkg.relativePath}/package.json must include name and version.`);
if (pkg.packageJson.private) throw new Error(`${pkg.name} is private and cannot be published.`);
if (pkg.packageJson.publishConfig?.access !== "public") throw new Error(`${pkg.name} must set publishConfig.access to "public".`);
if (!/^\d+\.\d+\.\d+$/.test(pkg.version)) throw new Error(`${pkg.name} version must be stable semver. Current: ${pkg.version}`);
}
}
function findAlreadyPublishedPackages() {
return packages.filter((pkg) => commandSucceeds("npm", ["view", `${pkg.name}@${pkg.version}`, "version", "--json"], { cwd: repoRoot }));
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function requireCommand(command, args) {
if (!commandSucceeds(command, args, { cwd: repoRoot })) throw new Error(`Required command is unavailable: ${command}`);
}
function commandSucceeds(command, args, options) {
return spawnSync(command, args, { cwd: options.cwd, stdio: "ignore" }).status === 0;
}
function commandOutput(command, args, options) {
const result = spawnSync(command, args, { cwd: options.cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout}`);
return result.stdout;
}
function run(command, args, options) {
console.log(`\n$ ${command} ${args.join(" ")}`);
const result = spawnSync(command, args, { cwd: options.cwd, stdio: "inherit" });
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}.`);
}
function printHelp() {
console.log(`Usage: pnpm release:npm -- --yes
Publishes the public OpenPets npm packages in dependency order.
Packages:
1. @open-pets/plugin-sdk
2. @open-pets/client
3. @open-pets/agent-events
4. @open-pets/mcp
5. @open-pets/claude
6. @open-pets/opencode
7. @open-pets/cursor
8. @open-pets/pi
9. @open-pets/cli
10. install-pet
Options:
--yes publish to npm; without this, runs pnpm publish --dry-run
--dry-run force dry-run behavior even with --yes
--skip-checks skip pnpm build and pnpm check
--tag <tag> npm dist-tag to publish under (default: latest)
--otp <code> npm two-factor authentication one-time password
--help show this help
`);
}