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
2 changes: 2 additions & 0 deletions packages/sim-cli/src/contract/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,8 @@ export const CLI_CONTRACT: CliContract = {
// `serviceId`, while `credentials create` matches a service-account provider
// on `providerId`. Each is empty on the kind of row that does not use it.
listCredentialProviders: {
// The providers group holds nothing but this list, so naming the group lists them.
defaultSubcommand: true,
columns: [
{ header: 'type' },
{ header: 'service', path: 'serviceId' },
Expand Down
5 changes: 5 additions & 0 deletions packages/sim-cli/src/contract/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ export interface CommandSpec {
command?: string
/** Run this operation when its top-level group is invoked without a subcommand. */
groupDefault?: boolean
/**
* Run this leaf when its group is named without a subcommand, as the group's default. The group
* keeps no options of its own, so the leaf parses its options in either spelling.
*/
defaultSubcommand?: true
/** Alternate leaf command names, such as `ls` for `list`. */
aliases?: readonly string[]
/**
Expand Down
9 changes: 7 additions & 2 deletions packages/sim-cli/src/runtime/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,18 +477,23 @@ function addLeafCommand(
for (const segment of rest.slice(0, -1)) {
parent = nestedGroup(parent, segment)
}
parent.addCommand(buildLeaf(operation, spec, rest[rest.length - 1]))
parent.addCommand(buildLeaf(operation, spec, rest[rest.length - 1]), {
isDefault: spec.defaultSubcommand === true,
})
return
}

group.addCommand(buildLeaf(operation, spec, rest[0]))
group.addCommand(buildLeaf(operation, spec, rest[0]), {
isDefault: spec.defaultSubcommand === true,
})
}

function variantCommandSpec(spec: CommandSpec, variant: CommandVariantSpec): CommandSpec {
return {
...spec,
command: variant.command,
groupDefault: false,
defaultSubcommand: undefined,
aliases: [],
positionals: variant.positionals,
requestFields: variant.requestFields,
Expand Down
51 changes: 51 additions & 0 deletions packages/sim-cli/src/runtime/group-default.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import { runEmbeddedCli } from '../embed'

const identity = { endpoint: 'https://sim.test', apiKey: 'fixture', workspaceId: 'workspace' }

/** Runs one embedded invocation, recording the path of each request it sends. */
async function run(argv: string[]) {
const paths: string[] = []
const result = await runEmbeddedCli(argv, {
...identity,
transport: async (url) => {
paths.push(new URL(String(url)).pathname)
return Response.json({ data: [] })
},
})
return { ...result, paths }
}

describe('a nested group that defaults to its list', () => {
it('lists credential providers when the providers group is named, as list does', async () => {
const bare = await run(['credentials', 'providers'])
const listed = await run(['credentials', 'providers', 'list'])
expect(bare.exitCode).toBe(0)
expect(bare.paths).toEqual(['/api/v2/credentials/providers'])
expect(listed.paths).toEqual(bare.paths)
})

it('keeps each spelling’s own options: a leaf option reaches the leaf, the bare group takes it too', async () => {
const searchOf = async (argv: string[]) => {
const queries: string[] = []
await runEmbeddedCli(argv, {
...identity,
transport: async (url) => {
queries.push(new URL(String(url)).searchParams.get('search') ?? '')
return Response.json({ data: [] })
},
})
return queries
}
expect(await searchOf(['credentials', 'providers', 'list', '--search', 'github'])).toEqual([
'github',
])
expect(await searchOf(['credentials', 'providers', '--search', 'github'])).toEqual(['github'])
})

it('leaves a group without a default printing its usage, sending nothing', async () => {
const group = await run(['credentials'])
expect(group.paths).toEqual([])
expect(group.stderr + group.stdout).toContain('Usage: sim credentials')
})
})
Loading