From 21aa05c30b9ffdbdfe836074cb44234ef335e396 Mon Sep 17 00:00:00 2001 From: Astralchemist Date: Tue, 26 May 2026 12:33:41 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20'rig=20update'=20command=20?= =?UTF-8?q?=E2=80=94=20self-update=20to=20the=20latest=20npm=20release=20(?= =?UTF-8?q?--check=20to=20report=20only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/update-cmd.test.ts | 26 ++++++++++++ package.json | 2 +- packages/cli/src/index.ts | 3 ++ packages/cli/src/update-cmd.ts | 75 ++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 __tests__/update-cmd.test.ts create mode 100644 packages/cli/src/update-cmd.ts diff --git a/__tests__/update-cmd.test.ts b/__tests__/update-cmd.test.ts new file mode 100644 index 0000000..3fbf665 --- /dev/null +++ b/__tests__/update-cmd.test.ts @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, test } from 'bun:test'; +import { isNewer } from '../packages/cli/src/update-cmd.ts'; + +describe('rig update — version comparison', () => { + test('detects a newer patch / minor / major', () => { + expect(isNewer('0.1.3', '0.1.4')).toBe(true); + expect(isNewer('0.1.4', '0.2.0')).toBe(true); + expect(isNewer('0.9.9', '1.0.0')).toBe(true); + }); + + test('equal or older is not newer', () => { + expect(isNewer('0.1.4', '0.1.4')).toBe(false); + expect(isNewer('0.1.4', '0.1.3')).toBe(false); + expect(isNewer('1.0.0', '0.9.9')).toBe(false); + }); + + test('handles uneven segment counts', () => { + expect(isNewer('0.1', '0.1.1')).toBe(true); + expect(isNewer('0.1.0', '0.1')).toBe(false); + }); + + test('a prerelease on an equal core is not treated as newer', () => { + expect(isNewer('0.1.4', '0.1.4-rc.1')).toBe(false); + }); +}); diff --git a/package.json b/package.json index e18b1a0..562f388 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rig-constellation", - "version": "0.1.3", + "version": "0.1.4", "private": false, "type": "module", "description": "Local-first semantic knowledge graph", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 960e001..3324dff 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,6 +14,7 @@ import { runServe } from './serve-cmd.ts'; import { runStart } from './start.ts'; import { runStatus } from './status.ts'; import { runSync } from './sync-cmd.ts'; +import { runUpdate } from './update-cmd.ts'; const COMMANDS: Record void | Promise> = { start: runStart, @@ -28,6 +29,7 @@ const COMMANDS: Record void | Promise> = { doctor: runDoctor, bench: runBenchCmd, 'export-docs': runExportDocs, + update: runUpdate, }; const HELP = `rig — local-first semantic knowledge graph @@ -47,6 +49,7 @@ commands: doctor diagnose environment, db, model cache, agent configs bench benchmark SQL + analytical surfaces (--fixture for synthetic stress test, --json for raw) export-docs render waypoints as a markdown doc tree (--out , default ./docs) + update update rig to the latest npm release (--check to only report) `; const [, , cmd] = process.argv; diff --git a/packages/cli/src/update-cmd.ts b/packages/cli/src/update-cmd.ts new file mode 100644 index 0000000..074f2bd --- /dev/null +++ b/packages/cli/src/update-cmd.ts @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import pkg from '../../../package.json' with { type: 'json' }; + +const PKG_NAME = 'rig-constellation'; +const REGISTRY = `https://registry.npmjs.org/${PKG_NAME}/latest`; + +// Compare two dotted version strings. Returns true when `b` is strictly newer +// than `a`. Numeric-only compare is enough for our 0.x line; a prerelease +// suffix on an otherwise-equal version is treated as older. +export const isNewer = (a: string, b: string): boolean => { + const part = (v: string) => v.split('-')[0].split('.').map((n) => Number.parseInt(n, 10) || 0); + const [pa, pb] = [part(a), part(b)]; + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const x = pa[i] ?? 0; + const y = pb[i] ?? 0; + if (y !== x) return y > x; + } + return false; // equal core → not newer (ignores prerelease ordering) +}; + +// Local install if the package resolves under the current project's +// node_modules; otherwise assume a global install. Drives `npm i` vs `npm i -g`. +const isLocalInstall = (cwd: string): boolean => + existsSync(join(cwd, 'node_modules', PKG_NAME, 'package.json')); + +const fetchLatest = async (): Promise => { + try { + const r = await fetch(REGISTRY, { signal: AbortSignal.timeout(5000) }); + if (!r.ok) return null; + const body = (await r.json()) as { version?: string }; + return typeof body.version === 'string' ? body.version : null; + } catch { + return null; + } +}; + +export async function runUpdate(cwd: string): Promise { + const current = (pkg as { version: string }).version; + const checkOnly = process.argv.includes('--check') || process.argv.includes('-n'); + + const latest = await fetchLatest(); + if (!latest) { + console.error(`rig update: couldn't reach the npm registry (you're on ${current}). Check your connection.`); + process.exitCode = 1; + return; + } + + if (!isNewer(current, latest)) { + console.log(`✓ rig ${current} is the latest.`); + return; + } + + const local = isLocalInstall(cwd); + const args = local ? ['i', `${PKG_NAME}@latest`] : ['i', '-g', `${PKG_NAME}@latest`]; + const cmd = `npm ${args.join(' ')}`; + console.log(`rig ${current} installed · ${latest} available`); + console.log(` ${local ? 'local' : 'global'} install → ${cmd}`); + + if (checkOnly) { + console.log(' (run `rig update` without --check to apply)'); + return; + } + + console.log(' updating…\n'); + const res = spawnSync('npm', args, { stdio: 'inherit', cwd: local ? cwd : undefined }); + if (res.status !== 0) { + console.error(`\nrig update: \`${cmd}\` failed (exit ${res.status ?? '?'}). Run it manually.`); + process.exitCode = res.status ?? 1; + return; + } + console.log(`\n✓ updated to ${PKG_NAME}@${latest}.`); +}