Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

43 lines · 2.3 KBRaw History
1import { chmod, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
2import path from "node:path";
3import { Api } from "../api.js";
4import type { Command } from "../cli.js";
5import { CliError } from "../io.js";
6import { VERSION } from "../version.js";
7
8const SHEBANG = "#!/usr/bin/env node\n";
9
10/** Replaces the file this CLI runs from with the server's current build (served at /-/cli/diffinery.mjs). */
11export const upgradeCommand: Command = {
12 name: "upgrade",
13 summary: "Update the CLI to the server's latest build",
14 description: `Downloads <server>/-/cli/diffinery.mjs and replaces the file this CLI runs from, when it changed.
15The install script (curl -fsSL https://diffinery.com/install | bash) does the same and sets up PATH.`,
16 maxArgs: 0,
17 async run(ctx) {
18 const cfg = await ctx.config();
19 if (!ctx.io.selfPath) throw new CliError("can't tell which file this CLI runs from; reinstall with curl -fsSL https://diffinery.com/install | bash");
20 const self = await realpath(ctx.io.selfPath);
21 const current = await readFile(self, "utf8").catch(() => "");
22 if (!current.startsWith(SHEBANG)) throw new CliError(`${self} isn't an installed diffinery CLI (running from source?); reinstall with curl -fsSL ${cfg.url}/install | bash`);
23 const res = await new Api(cfg.url, null).raw<string>("GET", "/-/cli/diffinery.mjs", { text: true, headers: { accept: "text/javascript" } });
24 const next = res.data;
25 if (!next.startsWith(SHEBANG) || next.length < 10_000) throw new CliError(`${cfg.url}/-/cli/diffinery.mjs doesn't look like the CLI; nothing changed`);
26 const version = res.headers.get("x-diffinery-cli-version") ?? VERSION;
27 if (next === current) {
28 ctx.out(`diffinery ${VERSION} is up to date.`);
29 return;
30 }
31 // Write beside it and rename over it, so a failed download never leaves half a file.
32 const tmp = path.join(path.dirname(self), `.diffinery-upgrade-${process.pid}.mjs`);
33 try {
34 await writeFile(tmp, next, { mode: 0o755 });
35 await chmod(tmp, 0o755);
36 await rename(tmp, self);
37 } catch (e) {
38 await rm(tmp, { force: true });
39 throw new CliError(`couldn't replace ${self}: ${(e as Error).message}`);
40 }
41 ctx.out(`${ctx.c.green("✓")} Updated diffinery ${VERSION === version ? VERSION : `${VERSION} → ${version}`} (${self})`);
42 },
43};