From b6d522f199f8294dee1d0faed65656b006645fed Mon Sep 17 00:00:00 2001 From: Astralchemist Date: Tue, 26 May 2026 13:15:27 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20suggest=20the=20nearest=20command?= =?UTF-8?q?=20on=20unknown=20input=20=E2=80=94=20'rig=20server'=20?= =?UTF-8?q?=E2=86=92=20serve,=20'rig=20claude'=20=E2=86=92=20install=20cla?= =?UTF-8?q?ude?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/suggest.test.ts | 32 ++++++++++++++++++++++++++++ packages/cli/src/index.ts | 6 +++++- packages/cli/src/suggest.ts | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 __tests__/suggest.test.ts create mode 100644 packages/cli/src/suggest.ts diff --git a/__tests__/suggest.test.ts b/__tests__/suggest.test.ts new file mode 100644 index 0000000..4b27bb7 --- /dev/null +++ b/__tests__/suggest.test.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, test } from 'bun:test'; +import { suggestCommand } from '../packages/cli/src/suggest.ts'; + +const COMMANDS = [ + 'start', 'init', 'status', 'index', 'sync', 'query', 'files', + 'install', 'serve', 'doctor', 'bench', 'export-docs', 'update', +]; + +describe('rig — did-you-mean command suggestions', () => { + test('common typos map to the right command', () => { + expect(suggestCommand('server', COMMANDS)).toBe('serve'); + expect(suggestCommand('statuss', COMMANDS)).toBe('status'); + expect(suggestCommand('udpate', COMMANDS)).toBe('update'); + expect(suggestCommand('exprt-docs', COMMANDS)).toBe('export-docs'); + }); + + test('agent names route to `install `', () => { + expect(suggestCommand('claude', COMMANDS)).toBe('install claude'); + expect(suggestCommand('cursor', COMMANDS)).toBe('install cursor'); + expect(suggestCommand('openrouter', COMMANDS)).toBe('install openrouter'); + }); + + test('far-off gibberish suggests nothing', () => { + expect(suggestCommand('xyzzy', COMMANDS)).toBeNull(); + expect(suggestCommand('frobnicate', COMMANDS)).toBeNull(); + }); + + test('an exact command is its own nearest match (distance 0)', () => { + expect(suggestCommand('serve', COMMANDS)).toBe('serve'); + }); +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3324dff..b259d7b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,6 +15,7 @@ import { runStart } from './start.ts'; import { runStatus } from './status.ts'; import { runSync } from './sync-cmd.ts'; import { runUpdate } from './update-cmd.ts'; +import { suggestCommand } from './suggest.ts'; const COMMANDS: Record void | Promise> = { start: runStart, @@ -74,7 +75,10 @@ if (!cmd) { const handler = COMMANDS[cmd]; if (!handler) { - console.error(`rig: unknown command '${cmd}'. Try \`rig --help\`.`); + const hint = suggestCommand(cmd, Object.keys(COMMANDS)); + console.error( + `rig: unknown command '${cmd}'.${hint ? ` Did you mean \`rig ${hint}\`?` : ''} Try \`rig --help\`.`, + ); process.exit(2); } await handler(process.cwd()); diff --git a/packages/cli/src/suggest.ts b/packages/cli/src/suggest.ts new file mode 100644 index 0000000..a210acf --- /dev/null +++ b/packages/cli/src/suggest.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Agent names are valid arguments to `install`, not commands — `rig claude` +// almost always means `rig install claude`. +const AGENTS = ['claude', 'cursor', 'codex', 'opencode', 'openrouter']; + +// Levenshtein edit distance — small inputs, so the simple O(n·m) table is fine. +const editDistance = (a: string, b: string): number => { + const m = a.length; + const n = b.length; + const row = Array.from({ length: n + 1 }, (_, i) => i); + for (let i = 1; i <= m; i++) { + let prev = row[0]; + row[0] = i; + for (let j = 1; j <= n; j++) { + const tmp = row[j]; + row[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, row[j], row[j - 1]); + prev = tmp; + } + } + return row[n]; +}; + +// Suggest the closest real command for an unknown one. Returns the suggestion +// as the user would type it (e.g. `serve`, or `install claude`), or null when +// nothing is close enough to be worth showing. +export const suggestCommand = (input: string, commands: string[]): string | null => { + if (AGENTS.includes(input)) return `install ${input}`; + let best: string | null = null; + let bestDist = Number.POSITIVE_INFINITY; + for (const c of commands) { + const d = editDistance(input, c); + if (d < bestDist) { + bestDist = d; + best = c; + } + } + // Only suggest when the typo is plausibly that command: within 2 edits, or + // within half the input length for longer words (e.g. "exprt-docs"). + const threshold = Math.max(2, Math.floor(input.length / 2)); + return best !== null && bestDist <= threshold ? best : null; +}; From f1cf6eb9fef8137d2e1682b158aff4b17f9e211e Mon Sep 17 00:00:00 2001 From: Astralchemist Date: Tue, 26 May 2026 13:15:28 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=200.1.6=20=E2=80=94=20OIDC?= =?UTF-8?q?=20trusted=20publishing=20(drop=20NPM=5FTOKEN=20secret)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 12 +++++++++--- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7e453f..11e29c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,8 @@ name: Release # Publish to npm + cut a GitHub Release when a version tag is pushed. # Flow: bump package.json → merge to main → `git tag v0.1.5 && git push --tags`. -# Requires repo secret NPM_TOKEN (a granular/automation token with publish + bypass-2FA). +# Auth via OIDC trusted publishing (no NPM_TOKEN); requires id-token: write +# and the GitHub Actions trusted publisher registered on the npm package. on: push: tags: ['v*.*.*'] @@ -10,6 +11,7 @@ on: permissions: contents: write # cut the GitHub Release + id-token: write # mint the OIDC token for npm trusted publishing jobs: publish: @@ -32,6 +34,10 @@ jobs: - name: Install run: bun install --frozen-lockfile + # Trusted publishing needs npm >= 11.5.1; Node 20 ships npm 10.x. + - name: Upgrade npm for trusted publishing + run: npm install -g npm@latest + # The prepublishOnly gate runs the full test suite, which includes # pre-commit-hook.test.ts — that asserts the hook is installed. - name: Install git hooks @@ -49,10 +55,10 @@ jobs: # `npm publish` runs prepublishOnly (build + tests + gen:check + compat + # migrations) before uploading, so the gate is enforced here too. + # Auth is via OIDC (id-token: write above) — no token env needed. + # Provenance is attached automatically when publishing over OIDC. - name: Publish to npm run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Release if: github.event_name == 'push' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f681a9..e9e1a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to Rig are documented here. The format follows ## [Unreleased] +## [0.1.6] - 2026-05-26 + +### Added +- `rig ` now suggests the closest command ("Did you mean `rig serve`?"), and maps agent names to `install` (`rig claude` → `rig install claude`). + +### Changed +- Release workflow publishes to npm via **OIDC trusted publishing** (signed provenance, no `NPM_TOKEN` secret). Requires a trusted publisher registered on the npm package. + ## [0.1.5] - 2026-05-26 ### Added diff --git a/package.json b/package.json index 2849abd..a056928 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rig-constellation", - "version": "0.1.5", + "version": "0.1.6", "private": false, "type": "module", "description": "Local-first semantic knowledge graph",