feat: switch auto-update to npm i -g and show update hints on failure
Some checks are pending
CI / test (push) Waiting to run

Replace GitHub releases download with `npm i -g` so platform package
installations also get auto-updates. On permission failure, write a hint
file that bin/ocr.js reads to prompt the user to update manually.
This commit is contained in:
kite 2026-06-23 19:09:44 +08:00
parent ca3b03dfc9
commit c69108656b
2 changed files with 49 additions and 100 deletions

View file

@ -17,6 +17,17 @@ if (!resolved) {
} }
const binaryPath = resolved.path; const binaryPath = resolved.path;
const hintFile = path.join(os.homedir(), ".opencodereview", "update-available");
try {
const hint = JSON.parse(fs.readFileSync(hintFile, "utf8"));
if (hint.version && hint.pkg) {
console.error(
`\x1b[33m[ocr] A new version (v${hint.version}) is available. Run to update:\x1b[0m\n` +
`\x1b[33m npm i -g ${hint.pkg}@${hint.version}\x1b[0m\n`
);
}
} catch (_) {}
if (!process.env.OCR_NO_UPDATE) { if (!process.env.OCR_NO_UPDATE) {
const stateDir = path.join(os.homedir(), ".opencodereview"); const stateDir = path.join(os.homedir(), ".opencodereview");
const tsFile = path.join(stateDir, "last-update-check"); const tsFile = path.join(stateDir, "last-update-check");

View file

@ -7,22 +7,13 @@ const os = require("os");
const https = require("https"); const https = require("https");
const { spawnSync } = require("child_process"); const { spawnSync } = require("child_process");
const { const { resolveNativeBinary } = require("./platform");
IS_WINDOWS, const { loadPackageJson } = require("./install.js");
BINARY_NAME,
detectPlatform,
loadPackageJson,
buildUrl,
downloadText,
downloadBinary,
computeChecksum,
} = require("./install.js");
const packageRoot = path.join(__dirname, "..");
const binDir = path.join(packageRoot, "bin");
const binaryPath = path.join(binDir, BINARY_NAME);
const stateDir = path.join(os.homedir(), ".opencodereview"); const stateDir = path.join(os.homedir(), ".opencodereview");
const tsFile = path.join(stateDir, "last-update-check"); const tsFile = path.join(stateDir, "last-update-check");
const lockFile = path.join(stateDir, "update.lock"); const lockFile = path.join(stateDir, "update.lock");
const hintFile = path.join(stateDir, "update-available");
const DEFAULT_REGISTRY = "https://registry.npmjs.org"; const DEFAULT_REGISTRY = "https://registry.npmjs.org";
@ -65,9 +56,9 @@ function releaseLock() {
} catch (_) {} } catch (_) {}
} }
function getInstalledVersion() { function getInstalledVersion(binPath) {
try { try {
const result = spawnSync(binaryPath, ["version"], { const result = spawnSync(binPath, ["version"], {
encoding: "utf8", encoding: "utf8",
timeout: 3000, timeout: 3000,
}); });
@ -118,24 +109,30 @@ function fetchLatestVersion(pkg) {
}); });
} }
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
function semverGt(a, b) { function semverGt(a, b) {
const pa = a.split(".").map(Number); const pa = a.replace(/-.*$/, "").split(".").map(Number);
const pb = b.split(".").map(Number); const pb = b.replace(/-.*$/, "").split(".").map(Number);
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
if ((pa[i] || 0) > (pb[i] || 0)) return true; if ((pa[i] || 0) > (pb[i] || 0)) return true;
if ((pa[i] || 0) < (pb[i] || 0)) return false; if ((pa[i] || 0) < (pb[i] || 0)) return false;
} }
const aPre = a.includes("-");
const bPre = b.includes("-");
if (bPre && !aPre) return true;
return false; return false;
} }
function cleanupTemp() { function writeHint(latestVersion, pkgName) {
try { try {
const files = fs.readdirSync(binDir); fs.writeFileSync(hintFile, JSON.stringify({ version: latestVersion, pkg: pkgName }));
for (const f of files) { } catch (_) {}
if (f.startsWith(".opencodereview.tmp.")) { }
fs.unlinkSync(path.join(binDir, f));
} function removeHint() {
} try {
fs.unlinkSync(hintFile);
} catch (_) {} } catch (_) {}
} }
@ -144,97 +141,38 @@ async function main() {
if (!acquireLock()) return; if (!acquireLock()) return;
cleanupTemp();
try { try {
const { resolveNativeBinary } = require("./platform");
const resolved = resolveNativeBinary(); const resolved = resolveNativeBinary();
if (resolved && resolved.fromPlatformPkg) { if (!resolved) return;
info("Binary managed by platform package, skipping auto-update.");
return; const installedVersion = getInstalledVersion(resolved.path);
}
const installedVersion = getInstalledVersion();
if (!installedVersion) return; if (!installedVersion) return;
const pkg = loadPackageJson(); const pkg = loadPackageJson();
const latestVersion = await fetchLatestVersion(pkg); const latestVersion = await fetchLatestVersion(pkg);
if (!latestVersion) return; if (!latestVersion) return;
if (!semverGt(latestVersion, installedVersion)) return; if (!SEMVER_RE.test(latestVersion)) return;
const { os: platform, arch } = detectPlatform(); if (!semverGt(latestVersion, installedVersion)) {
const config = pkg.ocrConfig; removeHint();
return;
const vars = { version: latestVersion, os: platform, arch };
let downloadUrl = buildUrl(config.urlPattern, vars);
if (IS_WINDOWS) {
downloadUrl += ".exe";
} }
const tempPath = path.join(binDir, `.opencodereview.tmp.${process.pid}`); const pkgName = pkg.name;
await downloadBinary(downloadUrl, tempPath); const IS_WINDOWS = process.platform === "win32";
if (!IS_WINDOWS) { const result = spawnSync("npm", ["i", "-g", `${pkgName}@${latestVersion}`], {
fs.chmodSync(tempPath, 0o755); encoding: "utf8",
} timeout: 120000,
shell: IS_WINDOWS,
});
if (config.checksumPattern) { if (result.status === 0) {
const checksumUrl = buildUrl(config.checksumPattern, vars); removeHint();
let shaContent;
try {
shaContent = await downloadText(checksumUrl);
} catch (_) {
fs.unlinkSync(tempPath);
return;
}
let actualSha;
try {
actualSha = await computeChecksum(tempPath);
} catch (_) {
fs.unlinkSync(tempPath);
return;
}
let verified = false;
for (const line of shaContent.split("\n")) {
const trimmed = line.trim();
if (trimmed.includes(`-${platform}-${arch}`)) {
const expectedSha = trimmed.split(/\s+/)[0].toLowerCase();
if (expectedSha && actualSha !== expectedSha) {
fs.unlinkSync(tempPath);
return;
}
verified = true;
break;
}
}
if (!verified) {
fs.unlinkSync(tempPath);
return;
}
}
if (IS_WINDOWS) {
const oldPath = binaryPath + ".old";
try { fs.unlinkSync(oldPath); } catch (_) {}
try {
fs.renameSync(binaryPath, oldPath);
} catch (e) {
if (fs.existsSync(binaryPath)) {
throw e;
}
}
try {
fs.renameSync(tempPath, binaryPath);
} catch (e) {
try { fs.renameSync(oldPath, binaryPath); } catch (_) {}
throw e;
}
try { fs.unlinkSync(oldPath); } catch (_) {}
} else { } else {
fs.renameSync(tempPath, binaryPath); writeHint(latestVersion, pkgName);
} }
} catch (_) { } catch (_) {
cleanupTemp();
} finally { } finally {
releaseLock(); releaseLock();
} }