diff --git a/src/cli/dispatch.test.ts b/src/cli/dispatch.test.ts index 6a9c58506..7afd5320f 100644 --- a/src/cli/dispatch.test.ts +++ b/src/cli/dispatch.test.ts @@ -185,6 +185,16 @@ register({ handler: async (args) => ({ id: (args as Record).id, agent_group_id: 'g1' }), }); +// Echoes args back — used to assert dash-joined positional id resolution. +register({ + name: 'groups-get', + description: 'test command (groups get)', + resource: 'groups', + access: 'open', + parseArgs: (raw) => raw, + handler: async (args) => ({ echo: args }), +}); + import { dispatch } from './dispatch.js'; import type { CallerContext } from './frame.js'; @@ -584,3 +594,19 @@ describe('CLI scope enforcement', () => { } }); }); + +// --- Dash-joined positional id resolution (generated ids contain dashes) --- + +describe('dash-joined positional id resolution', () => { + it('resolves `groups-get-` to (groups get, id=)', async () => { + const uuid = '550e8400-e29b-41d4-a716-446655440000'; + + const resp = await dispatch({ id: '1', command: `groups-get-${uuid}`, args: {} }, { caller: 'host' }); + + expect(resp.ok).toBe(true); + if (resp.ok) { + const data = resp.data as { echo: Record }; + expect(data.echo.id).toBe(uuid); + } + }); +}); diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index c35365272..f0dc430cb 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -26,19 +26,24 @@ export async function dispatch( ): Promise { let cmd = lookup(req.command); - // Fallback: if the full command isn't registered, trim the last - // dash-segment and treat it as the target ID. This lets clients join - // all positional args with dashes (e.g. `ncl groups get abc123` - // → command "groups-get-abc123" → trim → "groups-get" + id "abc123"). + // Fallback: if the full command isn't registered, split the dash-joined + // command and treat the longest registered prefix as the command, with the + // re-joined remainder as the target ID. Clients join all positional args + // with dashes (e.g. `ncl groups get abc123` → command "groups-get-abc123"), + // and generated ids (UUIDs, `sess-…`, `appr-…`) themselves contain dashes, + // so trimming a single trailing segment isn't enough — walk prefixes from + // longest to shortest so `groups-get-` still resolves to + // "groups-get" + id "". if (!cmd) { - const idx = req.command.lastIndexOf('-'); - if (idx > 0) { - const shortened = req.command.slice(0, idx); - const tail = req.command.slice(idx + 1); + const parts = req.command.split('-'); + for (let i = parts.length - 1; i > 0; i--) { + const shortened = parts.slice(0, i).join('-'); const fallback = lookup(shortened); if (fallback) { + const tail = parts.slice(i).join('-'); cmd = fallback; req = { ...req, command: shortened, args: { ...req.args, id: req.args.id ?? tail } }; + break; } } }