194 lines
7.1 KiB
JavaScript
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
|
|
`);
|
|
}
|