Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions __tests__/update-cmd.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (cwd: string) => void | Promise<void>> = {
start: runStart,
Expand All @@ -28,6 +29,7 @@ const COMMANDS: Record<string, (cwd: string) => void | Promise<void>> = {
doctor: runDoctor,
bench: runBenchCmd,
'export-docs': runExportDocs,
update: runUpdate,
};

const HELP = `rig — local-first semantic knowledge graph
Expand All @@ -47,6 +49,7 @@ commands:
doctor diagnose environment, db, model cache, agent configs
bench benchmark SQL + analytical surfaces (--fixture <N> for synthetic stress test, --json for raw)
export-docs render waypoints as a markdown doc tree (--out <dir>, default ./docs)
update update rig to the latest npm release (--check to only report)
`;

const [, , cmd] = process.argv;
Expand Down
75 changes: 75 additions & 0 deletions packages/cli/src/update-cmd.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> => {
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<void> {
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}.`);
}
Loading