|
| 1 | +/** |
| 2 | + * `qc update`: compare this qc with the npm registry and reinstall it with the |
| 3 | + * package manager that put it there (judged from where the qc binary lives). |
| 4 | + */ |
| 5 | +import { spawn } from 'node:child_process'; |
| 6 | +import { realpathSync } from 'node:fs'; |
| 7 | + |
| 8 | +export const PACKAGE = '@profullstack/qryptchat'; |
| 9 | + |
| 10 | +/** -1, 0 or 1, comparing x.y.z versions (a pre-release sorts before its release). */ |
| 11 | +export function compareVersions(a, b) { |
| 12 | + const parse = (v) => { |
| 13 | + const [core, pre = ''] = String(v).replace(/^v/, '').split('-'); |
| 14 | + return { nums: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre }; |
| 15 | + }; |
| 16 | + const x = parse(a); |
| 17 | + const y = parse(b); |
| 18 | + for (let i = 0; i < 3; i++) { |
| 19 | + const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0); |
| 20 | + if (d) return Math.sign(d); |
| 21 | + } |
| 22 | + if (x.pre === y.pre) return 0; |
| 23 | + if (!x.pre) return 1; |
| 24 | + if (!y.pre) return -1; |
| 25 | + return x.pre < y.pre ? -1 : 1; |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * The install command for where qc lives, or { npx: true } when it runs from |
| 30 | + * the npx cache (`npx @profullstack/qryptchat@latest` already fetches the newest). |
| 31 | + */ |
| 32 | +export function installCommand(binPath = '') { |
| 33 | + const p = binPath.replaceAll('\\', '/'); |
| 34 | + const spec = `${PACKAGE}@latest`; |
| 35 | + if (p.includes('/_npx/')) return { npx: true }; |
| 36 | + if (p.includes('/.bun/')) return { cmd: 'bun', args: ['add', '-g', spec] }; |
| 37 | + if (p.includes('/pnpm/')) return { cmd: 'pnpm', args: ['add', '-g', spec] }; |
| 38 | + return { cmd: 'npm', args: ['install', '-g', spec] }; |
| 39 | +} |
| 40 | + |
| 41 | +export async function latestVersion(fetchImpl = fetch) { |
| 42 | + const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: 'application/json' } }); |
| 43 | + if (!res.ok) throw new Error(`Could not reach the npm registry (HTTP ${res.status}).`); |
| 44 | + const { version } = await res.json(); |
| 45 | + if (!version) throw new Error('The npm registry did not say which version is latest.'); |
| 46 | + return version; |
| 47 | +} |
| 48 | + |
| 49 | +function run(cmd, args) { |
| 50 | + return new Promise((resolve, reject) => { |
| 51 | + const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32' }); |
| 52 | + child.on('error', (err) => reject(new Error(`Could not run ${cmd}: ${err.message}`))); |
| 53 | + child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}.`)))); |
| 54 | + }); |
| 55 | +} |
| 56 | + |
| 57 | +/** Returns a line for the user; installs unless `check` is set. */ |
| 58 | +export async function update({ current, check = false, binPath = process.argv[1], fetchImpl = fetch, runImpl = run }) { |
| 59 | + const latest = await latestVersion(fetchImpl); |
| 60 | + if (compareVersions(latest, current) <= 0) return { updated: false, line: `qc ${current} is the latest.` }; |
| 61 | + let real = binPath || ''; |
| 62 | + try { |
| 63 | + real = realpathSync(real); |
| 64 | + } catch {} |
| 65 | + const how = installCommand(real); |
| 66 | + if (how.npx) return { updated: false, line: `qc ${latest} is out (you have ${current}). Run: npx ${PACKAGE}@latest` }; |
| 67 | + const command = `${how.cmd} ${how.args.join(' ')}`; |
| 68 | + if (check) return { updated: false, line: `qc ${latest} is out (you have ${current}). Update with: qc update (runs ${command})` }; |
| 69 | + await runImpl(how.cmd, how.args); |
| 70 | + return { updated: true, line: `qc ${current} -> ${latest}.` }; |
| 71 | +} |
0 commit comments