| 1 | import { chmod, readFile, realpath, rename, rm, writeFile } from "node:fs/promises"; |
| 2 | import path from "node:path"; |
| 3 | import { Api } from "../api.js"; |
| 4 | import type { Command } from "../cli.js"; |
| 5 | import { CliError } from "../io.js"; |
| 6 | import { VERSION } from "../version.js"; |
| 7 | |
| 8 | const 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). */ |
| 11 | export 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. |
| 15 | The 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 | }; |