Skip to content

Commit 7264686

Browse files
committed
improvement(sim-cli): publish each command's path aliases and renamed paths in the command inventory
The worker builds its agent grammar from this inventory and did not know that sim runs files mv, workflows folders ls or kb as their commands, or still runs retired paths such as tables count create and workflows deployment list as the commands they were renamed to, so it refused or misread them. Each command now carries the commander aliases of its path segments (pathAliases, aligned with path) and the paths it was renamed from (renamedFrom).
1 parent c82ee71 commit 7264686

2 files changed

Lines changed: 103 additions & 5 deletions

File tree

‎packages/sim-cli/scripts/print-command-inventory.ts‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ interface InventoryCommand extends CommandReference {
6060
mothershipUnavailable?: true
6161
/** In a group hidden from `--help` until launch; the worker exposes it only when Sim enables it. */
6262
unpublished?: true
63+
/**
64+
* The commander aliases of each `path` segment, aligned with `path` (`kb` for `knowledge`,
65+
* `ls` for `list`), so the worker reads `sim kb folders ls` as the command it runs. Absent
66+
* when no segment has one.
67+
*/
68+
pathAliases?: string[][]
69+
/**
70+
* Paths this command used to have (`tables count create` for `tables rows count`). The CLI
71+
* still runs each as this command, hidden from help, so the worker reads it as this one.
72+
*/
73+
renamedFrom?: string[][]
6374
}
6475

6576
function isHiddenCommand(command: Command): boolean {
@@ -75,10 +86,17 @@ function subcommands(command: Command, atRoot: boolean): Command[] {
7586
)
7687
}
7788

78-
function collectLeaves(command: Command, prefix: string[]): { path: string[]; command: Command }[] {
79-
const children = subcommands(command, prefix.length === 0)
80-
if (children.length === 0) return [{ path: prefix, command }]
81-
return children.flatMap((child) => collectLeaves(child, [...prefix, child.name()]))
89+
interface Leaf {
90+
path: string[]
91+
/** The commands `path` names, outermost first. */
92+
chain: Command[]
93+
command: Command
94+
}
95+
96+
function collectLeaves(command: Command, chain: Command[]): Leaf[] {
97+
const children = subcommands(command, chain.length === 0)
98+
if (children.length === 0) return [{ path: chain.map((node) => node.name()), chain, command }]
99+
return children.flatMap((child) => collectLeaves(child, [...chain, child]))
82100
}
83101

84102
/**
@@ -135,7 +153,7 @@ function mothershipRefuses(operations: readonly V2OperationName[]): boolean {
135153

136154
const program = buildProgram()
137155
const inventory: InventoryCommand[] = collectLeaves(program, []).map(
138-
({ path: cmdPath, command }) => {
156+
({ path: cmdPath, chain, command }) => {
139157
const args: InventoryArgument[] = command.registeredArguments.map((argument) => ({
140158
name: argument.name(),
141159
required: argument.required,
@@ -173,6 +191,13 @@ const inventory: InventoryCommand[] = collectLeaves(program, []).map(
173191
? commandReference(OPENAPI_DOCS, op, jsonFields, cursorSlot(op) !== null)
174192
: {}
175193
const called = calledOperations(command) ?? []
194+
const pathAliases = chain.map((node) => node.aliases())
195+
const contract = operation ? CLI_CONTRACT[operation] : undefined
196+
const renamedFrom =
197+
contract?.renamedFrom &&
198+
(contract.command === undefined || contract.command === cmdPath.join(' '))
199+
? contract.renamedFrom.map((from) => from.split(' '))
200+
: []
176201
return {
177202
path: cmdPath,
178203
description: command.description(),
@@ -181,6 +206,8 @@ const inventory: InventoryCommand[] = collectLeaves(program, []).map(
181206
...reference,
182207
...(mothershipRefuses(called) ? { mothershipUnavailable: true as const } : {}),
183208
...(UNPUBLISHED_GROUPS.has(cmdPath[0] ?? '') ? { unpublished: true as const } : {}),
209+
...(pathAliases.some((aliases) => aliases.length > 0) ? { pathAliases } : {}),
210+
...(renamedFrom.length > 0 ? { renamedFrom } : {}),
184211
}
185212
}
186213
)

‎packages/sim-cli/src/contract/reference.test.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { execFileSync } from 'node:child_process'
22
import { fileURLToPath } from 'node:url'
3+
import type { Command } from 'commander'
34
import { describe, expect, it } from 'vitest'
45
import {
56
commandReference,
@@ -9,6 +10,7 @@ import {
910
import { runEmbeddedCli } from '#sim-cli/embed'
1011
import { V2_OPERATIONS } from '#sim-cli/generated/v2-api'
1112
import { buildProgram, UNPUBLISHED_GROUPS } from '#sim-cli/program'
13+
import { operationOf } from '#sim-cli/runtime/called-operations'
1214
import { cursorSlot } from '#sim-cli/runtime/request'
1315

1416
const ROOT = fileURLToPath(new URL('../../../../', import.meta.url))
@@ -19,10 +21,30 @@ interface InventoryEntry {
1921
body?: string
2022
mothershipUnavailable?: true
2123
unpublished?: true
24+
pathAliases?: string[][]
25+
renamedFrom?: string[][]
2226
}
2327

2428
let produced: InventoryEntry[] | undefined
2529

30+
/** The requests the embedded CLI sends for `argv`, answered with one generic success. */
31+
async function requests(argv: string[]) {
32+
const seen: string[] = []
33+
const result = await runEmbeddedCli(['--output', 'json', ...argv], {
34+
endpoint: 'https://sim.internal.test',
35+
apiKey: 'test-key',
36+
workspaceId: 'a2e3ab27-2f9d-4b8a-a2f2-3c47a1b0c9d1',
37+
transport: async (input, init) => {
38+
seen.push(`${init?.method ?? 'GET'} ${String(input)} ${String(init?.body ?? '')}`)
39+
return new Response(JSON.stringify({ data: { id: 'folder-1', count: 0 } }), {
40+
status: 200,
41+
headers: { 'content-type': 'application/json' },
42+
})
43+
},
44+
})
45+
return { exitCode: result.exitCode, stderr: result.stderr, seen }
46+
}
47+
2648
/** Read the actual producer, including command aliases and generated API documents. */
2749
function inventory(): InventoryEntry[] {
2850
produced ??= JSON.parse(
@@ -118,6 +140,55 @@ describe('CLI reference producer', () => {
118140
expect(flag('files get')).toBeUndefined()
119141
})
120142

143+
it('publishes the aliases of each path segment, which the real CLI runs as the command', async () => {
144+
const commands = inventory()
145+
const aliasesOf = (path: string) =>
146+
commands.find((entry) => entry.path.join(' ') === path)?.pathAliases
147+
expect(aliasesOf('files move')).toEqual([['file'], ['mv']])
148+
expect(aliasesOf('knowledge folders list')).toEqual([['kb'], [], ['ls']])
149+
expect(aliasesOf('workflows deploy')).toEqual([['workflow'], []])
150+
expect(aliasesOf('blocks get')).toBeUndefined()
151+
152+
const canonical = await requests(['files', 'move', '--file-ids', 'f1', '--to', 'folder-1'])
153+
expect(canonical.exitCode, canonical.stderr).toBe(0)
154+
expect(canonical.seen.length).toBeGreaterThan(0)
155+
expect(await requests(['file', 'mv', '--file-ids', 'f1', '--to', 'folder-1'])).toEqual(
156+
canonical
157+
)
158+
})
159+
160+
it('publishes each renamed path on the command the real CLI still runs it as', async () => {
161+
const commands = inventory()
162+
const renamed = commands.flatMap((entry) =>
163+
(entry.renamedFrom ?? []).map((from) => ({ from, to: entry.path }))
164+
)
165+
expect(renamed.map(({ from }) => from.join(' ')).sort()).toEqual([
166+
'files restore create',
167+
'tables columns run',
168+
'tables count create',
169+
'tables rows find',
170+
'workflows deployment list',
171+
])
172+
const program = buildProgram()
173+
const resolve = (path: readonly string[]) =>
174+
path.reduce<Command | undefined>(
175+
(node, name) =>
176+
node?.commands.find((child) => child.name() === name || child.aliases().includes(name)),
177+
program
178+
)
179+
for (const { from, to } of renamed) {
180+
const old = resolve(from)
181+
expect(old, from.join(' ')).toBeDefined()
182+
expect(old && operationOf(old), from.join(' ')).toBe(operationOf(resolve(to) ?? program))
183+
}
184+
185+
const current = await requests(['tables', 'rows', 'count', 'table-1'])
186+
expect(current.exitCode, current.stderr).toBe(0)
187+
const retired = await requests(['tables', 'count', 'create', 'table-1'])
188+
expect(retired.exitCode, retired.stderr).toBe(0)
189+
expect(retired.seen).toEqual(current.seen)
190+
})
191+
121192
it('the real CLI accepts both row bodies and prints their distinct 201 payloads', async () => {
122193
const identity = {
123194
endpoint: 'https://sim.internal.test',

0 commit comments

Comments
 (0)