From 1efe28ccdc65ffdd20069869e00be0b1e23937ea Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 9 May 2026 12:04:45 +0300 Subject: [PATCH] feat(cli): support space-separated multi-word verbs `ncl groups config get` now works alongside `ncl groups config-get`. Parser joins all positionals with dashes; dispatcher falls back by trimming the last segment as a target ID (`ncl groups get abc123`). Co-Authored-By: Claude Opus 4.6 (1M context) --- container/agent-runner/src/cli/ncl.ts | 9 +++------ src/cli/client.ts | 19 +++++-------------- src/cli/dispatch.ts | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/container/agent-runner/src/cli/ncl.ts b/container/agent-runner/src/cli/ncl.ts index d86c601b7..c8353688c 100644 --- a/container/agent-runner/src/cli/ncl.ts +++ b/container/agent-runner/src/cli/ncl.ts @@ -165,12 +165,9 @@ function parseArgv(argv: string[]): { process.exit(2); } - const command = positional.length >= 2 ? `${positional[0]}-${positional[1]}` : positional[0]; - - // Third positional is the target ID - if (positional.length >= 3) { - args.id = positional[2]; - } + // Join all positionals with dashes. The dispatcher trims the last + // segment as a target ID if the full name isn't a registered command. + const command = positional.join('-'); return { command, args, json }; } diff --git a/src/cli/client.ts b/src/cli/client.ts index 98527ed20..93ed50090 100644 --- a/src/cli/client.ts +++ b/src/cli/client.ts @@ -85,20 +85,11 @@ function parseArgv(argv: string[]): { process.exit(2); } - // Single word: `ncl help` - // Two words: `ncl groups list`, `ncl groups help` - // Three words: `ncl groups get abc123` - let command: string; - if (positional.length === 1) { - command = positional[0]; - } else { - command = `${positional[0]}-${positional[1]}`; - } - - // Third positional is the target ID - if (positional.length >= 3) { - args.id = positional[2]; - } + // Join all positionals with dashes to form the command name. + // If the full name isn't a command, the dispatcher will try trimming + // the last segment and using it as the target ID (e.g. `groups get abc` + // → command "groups-get", id "abc"). + const command = positional.join('-'); return { command, args, json }; } diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 7b247eb42..268e4d2c4 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -13,7 +13,25 @@ import type { CallerContext, ErrorCode, RequestFrame, ResponseFrame } from './fr import { lookup } from './registry.js'; export async function dispatch(req: RequestFrame, ctx: CallerContext): Promise { - const cmd = lookup(req.command); + 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"). + 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 fallback = lookup(shortened); + if (fallback) { + cmd = fallback; + req = { ...req, command: shortened, args: { ...req.args, id: req.args.id ?? tail } }; + } + } + } + if (!cmd) { return err(req.id, 'unknown-command', `no command "${req.command}"`); }