Files
openwork/apps/app/scripts/bump-version.mjs
2026-03-19 14:27:34 -07:00

191 lines
5.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const ROOT = process.cwd();
const REPO_ROOT = path.resolve(ROOT, "../..");
const args = process.argv.slice(2);
const usage = () => {
console.log(`Usage:
node scripts/bump-version.mjs patch|minor|major
node scripts/bump-version.mjs --set x.y.z
node scripts/bump-version.mjs --dry-run [patch|minor|major|--set x.y.z]`);
};
const isDryRun = args.includes("--dry-run");
// pnpm forwards args to scripts with an explicit "--" separator; strip it so
// "pnpm bump:set -- 0.1.21" works as expected.
const filtered = args.filter((arg) => arg !== "--dry-run" && arg !== "--");
if (!filtered.length) {
usage();
process.exit(1);
}
let mode = filtered[0];
let explicit = null;
if (mode === "--set") {
explicit = filtered[1] ?? null;
if (!explicit) {
console.error("--set requires a version like 0.1.21");
process.exit(1);
}
}
const semverPattern = /^\d+\.\d+\.\d+$/;
const readJson = async (filePath) =>
JSON.parse(await readFile(filePath, "utf8"));
const bump = (value, bumpMode) => {
if (!semverPattern.test(value)) {
throw new Error(`Invalid version: ${value}`);
}
const [major, minor, patch] = value.split(".").map(Number);
if (bumpMode === "major") return `${major + 1}.0.0`;
if (bumpMode === "minor") return `${major}.${minor + 1}.0`;
if (bumpMode === "patch") return `${major}.${minor}.${patch + 1}`;
throw new Error(`Unknown bump mode: ${bumpMode}`);
};
const targetVersion = async () => {
if (explicit) return explicit;
const pkg = await readJson(path.join(ROOT, "package.json"));
return bump(pkg.version, mode);
};
const updatePackageJson = async (nextVersion) => {
const uiPath = path.join(ROOT, "package.json");
const tauriPath = path.join(REPO_ROOT, "apps", "desktop", "package.json");
const orchestratorPath = path.join(
REPO_ROOT,
"apps",
"orchestrator",
"package.json",
);
const serverPath = path.join(REPO_ROOT, "apps", "server", "package.json");
const opencodeRouterPath = path.join(
REPO_ROOT,
"apps",
"opencode-router",
"package.json",
);
const uiData = await readJson(uiPath);
const tauriData = await readJson(tauriPath);
const orchestratorData = await readJson(orchestratorPath);
const serverData = await readJson(serverPath);
const opencodeRouterData = await readJson(opencodeRouterPath);
uiData.version = nextVersion;
tauriData.version = nextVersion;
// Desktop pins opencodeRouterVersion for sidecar bundling; keep it aligned.
tauriData.opencodeRouterVersion = nextVersion;
orchestratorData.version = nextVersion;
// Ensure openwork-orchestrator uses the same openwork-server/opencode-router versions.
orchestratorData.dependencies = orchestratorData.dependencies ?? {};
orchestratorData.dependencies["openwork-server"] = nextVersion;
orchestratorData.dependencies["opencode-router"] = nextVersion;
serverData.version = nextVersion;
opencodeRouterData.version = nextVersion;
if (!isDryRun) {
await writeFile(uiPath, JSON.stringify(uiData, null, 2) + "\n");
await writeFile(tauriPath, JSON.stringify(tauriData, null, 2) + "\n");
await writeFile(
orchestratorPath,
JSON.stringify(orchestratorData, null, 2) + "\n",
);
await writeFile(serverPath, JSON.stringify(serverData, null, 2) + "\n");
await writeFile(
opencodeRouterPath,
JSON.stringify(opencodeRouterData, null, 2) + "\n",
);
}
};
const updateCargoToml = async (nextVersion) => {
const filePath = path.join(
REPO_ROOT,
"apps",
"desktop",
"src-tauri",
"Cargo.toml",
);
const raw = await readFile(filePath, "utf8");
const updated = raw.replace(
/\bversion\s*=\s*"[^"]+"/m,
`version = "${nextVersion}"`,
);
if (!isDryRun) {
await writeFile(filePath, updated);
// Regenerate Cargo.lock so it stays in sync with the version bump.
const { execFileSync } = await import("node:child_process");
try {
execFileSync("cargo", ["generate-lockfile"], {
cwd: path.join(REPO_ROOT, "apps", "desktop", "src-tauri"),
stdio: "ignore",
});
} catch {
// cargo may not be installed (e.g. CI without Rust); skip silently.
}
}
};
const updateTauriConfig = async (nextVersion) => {
const filePath = path.join(
REPO_ROOT,
"apps",
"desktop",
"src-tauri",
"tauri.conf.json",
);
const data = JSON.parse(await readFile(filePath, "utf8"));
data.version = nextVersion;
if (!isDryRun) {
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
}
};
const main = async () => {
if (explicit && !semverPattern.test(explicit)) {
throw new Error(`Invalid explicit version: ${explicit}`);
}
if (explicit === null && !["patch", "minor", "major"].includes(mode)) {
throw new Error(`Unknown mode: ${mode}`);
}
const nextVersion = await targetVersion();
await updatePackageJson(nextVersion);
await updateCargoToml(nextVersion);
await updateTauriConfig(nextVersion);
console.log(
JSON.stringify(
{
ok: true,
version: nextVersion,
dryRun: isDryRun,
files: [
"apps/app/package.json",
"apps/desktop/package.json",
"apps/orchestrator/package.json",
"apps/server/package.json",
"apps/opencode-router/package.json",
"apps/desktop/src-tauri/Cargo.toml",
"apps/desktop/src-tauri/tauri.conf.json",
],
},
null,
2,
),
);
};
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify({ ok: false, error: message }));
process.exit(1);
});