@seamapi/cli 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +39 -4
  2. package/bin/cli.js +26 -6
  3. package/bin/cli.js.map +1 -1
  4. package/lib/args/parse.d.ts +9 -0
  5. package/lib/args/parse.js +42 -2
  6. package/lib/args/parse.js.map +1 -1
  7. package/lib/args/validate.d.ts +10 -0
  8. package/lib/args/validate.js +23 -0
  9. package/lib/args/validate.js.map +1 -1
  10. package/lib/auth/operations.d.ts +7 -20
  11. package/lib/auth/operations.js +13 -38
  12. package/lib/auth/operations.js.map +1 -1
  13. package/lib/commands/local/login.js +4 -12
  14. package/lib/commands/local/login.js.map +1 -1
  15. package/lib/commands/local/select-endpoint.js +10 -7
  16. package/lib/commands/local/select-endpoint.js.map +1 -1
  17. package/lib/commands/local/select-workspace.js +12 -4
  18. package/lib/commands/local/select-workspace.js.map +1 -1
  19. package/lib/commands/registry.d.ts +11 -0
  20. package/lib/commands/registry.js +11 -0
  21. package/lib/commands/registry.js.map +1 -1
  22. package/lib/commands/spec.d.ts +12 -0
  23. package/lib/commands/spec.js +16 -0
  24. package/lib/commands/spec.js.map +1 -1
  25. package/lib/context.d.ts +5 -4
  26. package/lib/context.js +23 -8
  27. package/lib/context.js.map +1 -1
  28. package/lib/interactions/endpoint-selection.js +5 -23
  29. package/lib/interactions/endpoint-selection.js.map +1 -1
  30. package/lib/overrides.d.ts +20 -0
  31. package/lib/overrides.js +23 -0
  32. package/lib/overrides.js.map +1 -0
  33. package/lib/render/help.js +20 -1
  34. package/lib/render/help.js.map +1 -1
  35. package/lib/version.d.ts +1 -1
  36. package/lib/version.js +1 -1
  37. package/package.json +1 -1
  38. package/src/bin/cli.ts +31 -6
  39. package/src/lib/args/parse.ts +47 -2
  40. package/src/lib/args/validate.ts +37 -0
  41. package/src/lib/auth/operations.ts +13 -54
  42. package/src/lib/commands/local/login.ts +4 -15
  43. package/src/lib/commands/local/select-endpoint.ts +11 -7
  44. package/src/lib/commands/local/select-workspace.ts +13 -4
  45. package/src/lib/commands/registry.ts +20 -0
  46. package/src/lib/commands/spec.ts +31 -0
  47. package/src/lib/context.ts +30 -12
  48. package/src/lib/interactions/endpoint-selection.ts +5 -29
  49. package/src/lib/overrides.ts +32 -0
  50. package/src/lib/render/help.ts +21 -1
  51. package/src/lib/version.ts +1 -1
@@ -1,6 +1,8 @@
1
1
  import type { Parameter } from '@seamapi/blueprint'
2
2
 
3
+ import type { CommandDefinition } from 'lib/commands/spec.js'
3
4
  import { NonInteractiveError, UsageError } from 'lib/errors.js'
5
+ import type { AuthOverrides } from 'lib/overrides.js'
4
6
 
5
7
  import { toArgName, toGivenArgName } from './parse.js'
6
8
 
@@ -40,6 +42,41 @@ export const assertRequiredParams = (
40
42
  * Only arguments are checked. Params read from stdin are passed through as
41
43
  * given, so a caller may send whatever the API itself accepts.
42
44
  */
45
+ /**
46
+ * Refuse the auth overrides on a command that selects what they override.
47
+ *
48
+ * `--endpoint` and `--workspace-id` scope one command and are never stored,
49
+ * so on `seam select ...` they would read as the value to store and quietly
50
+ * do nothing of the kind. The positional is what stores.
51
+ */
52
+ export const assertNoAuthOverrides = (
53
+ { path, positional }: CommandDefinition,
54
+ overrides: AuthOverrides,
55
+ ): void => {
56
+ if (path[0] !== 'select') return
57
+
58
+ const given = [
59
+ overrides.endpoint == null ? null : '--endpoint',
60
+ overrides.workspaceId == null ? null : '--workspace-id',
61
+ ].filter((flag) => flag != null)
62
+
63
+ if (given.length === 0) return
64
+
65
+ const command = `seam ${path.join(' ')}`
66
+
67
+ throw new UsageError(
68
+ `${given.join(' and ')} cannot be used with ${command}: ${
69
+ given.length === 1 ? 'it overrides' : 'they override'
70
+ } a single command rather than changing what is selected.`,
71
+ {
72
+ hint:
73
+ positional == null
74
+ ? `Run '${command}' to change what is selected.`
75
+ : `Run '${command} <${positional.name}>' to change what is selected.`,
76
+ },
77
+ )
78
+ }
79
+
43
80
  export const assertKnownArgs = (
44
81
  argParams: Record<string, unknown>,
45
82
  command: string[],
@@ -1,5 +1,3 @@
1
- import { randomBytes } from 'node:crypto'
2
-
3
1
  import { type ConfigStore, getConfigStore } from 'lib/config/index.js'
4
2
  import { type AuthContext, resolveAuth } from 'lib/context.js'
5
3
  import {
@@ -46,49 +44,33 @@ export const assertMutable = (
46
44
  assertEnvVarUnset(envVar, value, action)
47
45
  }
48
46
 
49
- export interface LoginOptions {
50
- endpoint?: string | undefined
51
- token?: string | undefined
52
- workspaceId?: string | undefined
53
- }
54
-
55
47
  /**
56
- * Store the given credentials, validating the token first.
48
+ * Store a token, validating it first.
57
49
  *
58
- * The token is stored under the endpoint it will be used with, so a given
59
- * endpoint is stored and re-resolved before the token key is derived.
50
+ * The token is stored under the endpoint it will be used with, which
51
+ * `--endpoint` or the environment may have pointed elsewhere for this one
52
+ * command: logging in to another endpoint stores a token for it without
53
+ * selecting it. The workspace is only what the token is validated against,
54
+ * as a Personal Access Token is meaningless without one.
60
55
  *
61
56
  * Validation reaches the network, so a test may inject its own `validate`.
62
57
  */
63
58
  export const login = async (
64
- { endpoint, token, workspaceId }: LoginOptions,
59
+ token: string,
65
60
  config: ConfigStore = getConfigStore(),
66
61
  validate: typeof validateToken = validateToken,
67
62
  ): Promise<void> => {
68
- let auth = resolveAuth(config)
63
+ const auth = resolveAuth(config)
69
64
 
70
65
  // Nothing is stored while the environment overrides it, so refuse before
71
- // storing anything rather than part way through.
66
+ // validating rather than after reaching the network.
72
67
  assertMutable(auth, 'token', 'log in')
73
- if (endpoint != null) assertMutable(auth, 'endpoint', 'select an endpoint')
74
- if (workspaceId != null) {
75
- assertMutable(auth, 'workspaceId', 'select a workspace')
76
- }
77
68
 
78
- if (endpoint != null) {
79
- storeEndpoint(endpoint, config)
80
- auth = resolveAuth(config)
81
- }
69
+ await validate(token, auth.workspaceId ?? undefined)
82
70
 
83
- if (token != null) {
84
- await validate(token, workspaceId)
85
- config.set(`${auth.endpoint}.pat`, token)
86
- config.delete('current_workspace_id')
87
- }
88
-
89
- if (workspaceId != null) {
90
- config.set('current_workspace_id', workspaceId)
91
- }
71
+ config.set(`${auth.endpoint}.pat`, token)
72
+ // The selection belongs to whoever was logged in before.
73
+ config.delete('current_workspace_id')
92
74
  }
93
75
 
94
76
  /** Store the token for the current endpoint, e.g., one just prompted for. */
@@ -134,29 +116,6 @@ export const selectWorkspace = (
134
116
  config.set('current_workspace_id', workspaceId)
135
117
  }
136
118
 
137
- /**
138
- * Point the CLI at a fake Seam Connect endpoint and store the well-known
139
- * token it accepts. Returns the generated endpoint URL for reporting.
140
- */
141
- export const selectFakeEndpoint = ({
142
- urlSeed = randomBytes(5).toString('hex'),
143
- config = getConfigStore(),
144
- }: {
145
- urlSeed?: string
146
- config?: ConfigStore
147
- } = {}): { endpoint: string; token: string } => {
148
- const auth = resolveAuth(config)
149
- assertMutable(auth, 'endpoint', 'select an endpoint')
150
- assertMutable(auth, 'token', 'log in')
151
-
152
- const endpoint = `https://${urlSeed}.fakeseamconnect.seam.vc`
153
- const token = 'seam_apikey1_token'
154
- storeEndpoint(endpoint, config)
155
- config.set(`${endpoint}.pat`, token)
156
-
157
- return { endpoint, token }
158
- }
159
-
160
119
  /** Store whether API definitions come from the endpoint instead of npm. */
161
120
  export const setUseRemoteApiDefs = (
162
121
  useRemoteApiDefs: boolean,
@@ -10,24 +10,13 @@ export const loginCommand: Command = {
10
10
  kind: 'cli',
11
11
  title: 'Log in to Seam.',
12
12
  description:
13
- 'Prompts for a personal access token unless one is passed with --token.',
14
- flags: [
15
- stringFlag('endpoint', 'Seam API endpoint to log in to.'),
16
- stringFlag('token', 'Personal access token to log in with.'),
17
- stringFlag('workspace-id', 'Workspace to select after logging in.'),
18
- ],
13
+ 'Prompts for a personal access token unless one is passed with --token. The token is stored for the selected endpoint, or for the one --endpoint names.',
14
+ flags: [stringFlag('token', 'Personal access token to log in with.')],
19
15
  },
20
16
  requiresAuth: false,
21
17
  execute: async ({ args }, ctx) => {
22
- if (args['token'] || args['workspace_id'] || args['endpoint']) {
23
- await login(
24
- {
25
- endpoint: args['endpoint'] ? args['endpoint'] : undefined,
26
- token: args['token'] ? String(args['token']).trim() : undefined,
27
- workspaceId: args['workspace_id'] ? args['workspace_id'] : undefined,
28
- },
29
- ctx.config,
30
- )
18
+ if (args['token']) {
19
+ await login(String(args['token']).trim(), ctx.config)
31
20
  return { kind: 'done' }
32
21
  }
33
22
  assertMutable(ctx.auth, 'token', 'log in')
@@ -1,6 +1,5 @@
1
1
  import { assertMutable, selectEndpoint } from 'lib/auth/operations.js'
2
2
  import type { Command } from 'lib/commands/registry.js'
3
- import { stringFlag } from 'lib/commands/spec.js'
4
3
  import { NonInteractiveError } from 'lib/errors.js'
5
4
  import { interactForEndpointSelection } from 'lib/interactions/index.js'
6
5
 
@@ -9,19 +8,24 @@ export const selectEndpointCommand: Command = {
9
8
  path: ['select', 'endpoint'],
10
9
  kind: 'cli',
11
10
  title: 'Select the Seam API endpoint.',
12
- description: '',
13
- flags: [stringFlag('endpoint', 'Seam API endpoint to select.')],
11
+ description:
12
+ 'Stores the endpoint every later command runs against. To use one for a single command instead, pass --endpoint to that command.',
13
+ flags: [],
14
+ positional: {
15
+ name: 'url',
16
+ description: 'Seam API endpoint to select.',
17
+ },
14
18
  },
15
19
  requiresAuth: false,
16
- execute: async ({ args }, ctx) => {
20
+ execute: async ({ positional }, ctx) => {
17
21
  assertMutable(ctx.auth, 'endpoint', 'select an endpoint')
18
- if (args['endpoint']) {
19
- selectEndpoint(args['endpoint'], ctx.config)
22
+ if (positional != null) {
23
+ selectEndpoint(positional, ctx.config)
20
24
  return { kind: 'done' }
21
25
  }
22
26
  if (ctx.interactivity === 'non-interactive') {
23
27
  throw new NonInteractiveError(
24
- 'Missing required parameter for select endpoint: --endpoint',
28
+ 'Missing required argument for select endpoint: <url>',
25
29
  )
26
30
  }
27
31
  await interactForEndpointSelection()
@@ -1,4 +1,4 @@
1
- import { assertMutable } from 'lib/auth/operations.js'
1
+ import { assertMutable, selectWorkspace } from 'lib/auth/operations.js'
2
2
  import type { Command } from 'lib/commands/registry.js'
3
3
  import { NonInteractiveError } from 'lib/errors.js'
4
4
  import { interactForWorkspaceId } from 'lib/interactions/index.js'
@@ -8,15 +8,24 @@ export const selectWorkspaceCommand: Command = {
8
8
  path: ['select', 'workspace'],
9
9
  kind: 'cli',
10
10
  title: 'Select the current workspace.',
11
- description: '',
11
+ description:
12
+ 'Stores the workspace every later command runs against. To use one for a single command instead, pass --workspace-id to that command.',
12
13
  flags: [],
14
+ positional: {
15
+ name: 'workspace-id',
16
+ description: 'Workspace to select.',
17
+ },
13
18
  },
14
19
  requiresAuth: true,
15
- execute: async (_invocation, ctx) => {
20
+ execute: async ({ positional }, ctx) => {
16
21
  assertMutable(ctx.auth, 'workspaceId', 'select a workspace')
22
+ if (positional != null) {
23
+ selectWorkspace(positional, ctx.config)
24
+ return { kind: 'done' }
25
+ }
17
26
  if (ctx.interactivity === 'non-interactive') {
18
27
  throw new NonInteractiveError(
19
- 'Cannot select a workspace in non-interactive mode: pass --workspace-id to "seam login"',
28
+ 'Missing required argument for select workspace: <workspace-id>',
20
29
  )
21
30
  }
22
31
  await interactForWorkspaceId()
@@ -38,6 +38,8 @@ export interface Command {
38
38
  /** Everything a single run of a command was given. */
39
39
  export interface Invocation {
40
40
  path: string[]
41
+ /** The value written after the command path, when the command takes one. */
42
+ positional?: string | undefined
41
43
  /** Params given as arguments, held to what the command accepts. */
42
44
  argParams: Record<string, unknown>
43
45
  /** Params piped in as JSON, passed through as given. */
@@ -93,6 +95,24 @@ export const localCommandDefinitions: CommandDefinition[] = localCommands
93
95
  export const findLocalCommand = (path: string[]): Command | undefined =>
94
96
  localCommands.find((command) => isSamePath(command.definition.path, path))
95
97
 
98
+ /**
99
+ * The local command the given words invoke with a value after its path, e.g.,
100
+ * `select endpoint` for `select endpoint https://connect.getseam.com`, or
101
+ * `undefined` when the words are not a command taking one.
102
+ *
103
+ * Only commands declaring a positional match, so a stray word after any other
104
+ * command stays what it has always been: no command at all.
105
+ */
106
+ export const findLocalCommandTakingPositional = (
107
+ words: string[],
108
+ ): Command | undefined =>
109
+ localCommands.find(
110
+ ({ definition }) =>
111
+ definition.positional != null &&
112
+ words.length === definition.path.length + 1 &&
113
+ isSamePath(definition.path, words.slice(0, -1)),
114
+ )
115
+
96
116
  /** Parameter names a command accepts as arguments. */
97
117
  export const acceptedParamsOf = (definition: CommandDefinition): Set<string> =>
98
118
  new Set(
@@ -23,6 +23,17 @@ export interface CommandFlag {
23
23
  */
24
24
  export type CommandKind = 'cli' | 'api'
25
25
 
26
+ /**
27
+ * A value written after the command rather than behind a flag, e.g., the URL
28
+ * in `seam select endpoint <url>`. At most one, always required: it is the
29
+ * one thing the command is about.
30
+ */
31
+ export interface CommandPositional {
32
+ /** Name shown in the usage line, without the angle brackets. */
33
+ name: string
34
+ description: string
35
+ }
36
+
26
37
  export interface CommandDefinition {
27
38
  path: string[]
28
39
  kind: CommandKind
@@ -31,6 +42,8 @@ export interface CommandDefinition {
31
42
  /** Longer prose about the command, empty when there is none to add. */
32
43
  description: string
33
44
  flags: CommandFlag[]
45
+ /** The value the command takes after its path, when it takes one. */
46
+ positional?: CommandPositional
34
47
  }
35
48
 
36
49
  export interface Subcommand {
@@ -56,6 +69,15 @@ export interface CommandSpec {
56
69
  }
57
70
 
58
71
  export const globalFlags: CommandFlag[] = [
72
+ {
73
+ long: 'endpoint',
74
+ short: null,
75
+ description:
76
+ 'Seam API endpoint to run this one command against, instead of the selected one.',
77
+ values: [],
78
+ takesValue: true,
79
+ isRequired: false,
80
+ },
59
81
  {
60
82
  long: 'help',
61
83
  short: 'h',
@@ -115,6 +137,15 @@ export const globalFlags: CommandFlag[] = [
115
137
  takesValue: false,
116
138
  isRequired: false,
117
139
  },
140
+ {
141
+ long: 'workspace-id',
142
+ short: null,
143
+ description:
144
+ 'Workspace to run this one command against, instead of the selected one.',
145
+ values: [],
146
+ takesValue: true,
147
+ isRequired: false,
148
+ },
118
149
  ]
119
150
 
120
151
  export const flagTokens = (flag: CommandFlag): string[] => {
@@ -8,18 +8,20 @@ import {
8
8
  } from './env.js'
9
9
  import type { SeamApi } from './http/api.js'
10
10
  import type { Output } from './output/output.js'
11
+ import { getAuthOverrides } from './overrides.js'
11
12
 
12
13
  export const defaultEndpoint = 'https://connect.getseam.com'
13
14
 
14
15
  /** Where a resolved value came from, e.g., to refuse writes the env shadows. */
15
- export type ValueSource = 'env' | 'config' | 'default'
16
+ export type ValueSource = 'flag' | 'env' | 'config' | 'default'
16
17
 
17
18
  /**
18
19
  * The endpoint, token, and workspace requests are made with.
19
20
  *
20
- * Resolved in one place so the precedence rule exists once: an environment
21
- * variable wins over the stored value, and the endpoint falls back to Seam.
22
- * The source tags say where each value came from.
21
+ * Resolved in one place so the precedence rule exists once: a flag given for
22
+ * the one command wins over an environment variable, which wins over the
23
+ * stored value, and the endpoint falls back to Seam. The source tags say
24
+ * where each value came from.
23
25
  */
24
26
  export interface AuthContext {
25
27
  endpoint: string
@@ -33,35 +35,51 @@ export interface AuthContext {
33
35
  export const resolveAuth = (
34
36
  config: ConfigStore = getConfigStore(),
35
37
  ): AuthContext => {
38
+ const { endpoint: flagEndpoint, workspaceId: flagWorkspaceId } =
39
+ getAuthOverrides()
40
+
36
41
  const envEndpoint = getEndpointFromEnv()
37
42
  // Configs written before the endpoint was called one still hold it under
38
43
  // `server`, so fall back to that key rather than silently resetting them.
39
44
  const storedEndpoint = config.get('endpoint') ?? config.get('server')
40
45
  const endpoint =
41
- envEndpoint ?? (typeof storedEndpoint === 'string' ? storedEndpoint : null)
46
+ flagEndpoint ??
47
+ envEndpoint ??
48
+ (typeof storedEndpoint === 'string' ? storedEndpoint : null)
42
49
 
43
50
  const envToken = getTokenFromEnv()
51
+ // The token is stored per endpoint, so an overridden endpoint is read with
52
+ // the token belonging to it rather than the one it replaced.
44
53
  const storedToken = readString(
45
54
  config.get(`${endpoint ?? defaultEndpoint}.pat`),
46
55
  )
47
56
 
48
57
  const envWorkspaceId = getWorkspaceIdFromEnv()
49
58
  const storedWorkspaceId = readString(config.get('current_workspace_id'))
59
+ const workspaceId = flagWorkspaceId ?? envWorkspaceId ?? storedWorkspaceId
50
60
 
51
61
  return {
52
62
  endpoint: endpoint ?? defaultEndpoint,
53
63
  endpointSource:
54
- envEndpoint != null ? 'env' : endpoint != null ? 'config' : 'default',
64
+ flagEndpoint != null
65
+ ? 'flag'
66
+ : envEndpoint != null
67
+ ? 'env'
68
+ : endpoint != null
69
+ ? 'config'
70
+ : 'default',
55
71
  token: envToken ?? storedToken,
56
72
  tokenSource:
57
73
  envToken != null ? 'env' : storedToken != null ? 'config' : null,
58
- workspaceId: envWorkspaceId ?? storedWorkspaceId,
74
+ workspaceId,
59
75
  workspaceIdSource:
60
- envWorkspaceId != null
61
- ? 'env'
62
- : storedWorkspaceId != null
63
- ? 'config'
64
- : null,
76
+ flagWorkspaceId != null
77
+ ? 'flag'
78
+ : envWorkspaceId != null
79
+ ? 'env'
80
+ : storedWorkspaceId != null
81
+ ? 'config'
82
+ : null,
65
83
  }
66
84
  }
67
85
 
@@ -1,24 +1,14 @@
1
- import { randomBytes } from 'node:crypto'
2
-
3
- import {
4
- assertMutable,
5
- selectEndpoint,
6
- selectFakeEndpoint,
7
- } from 'lib/auth/operations.js'
1
+ import { assertMutable, selectEndpoint } from 'lib/auth/operations.js'
8
2
  import { getConfigStore } from 'lib/config/index.js'
9
3
  import { resolveAuth } from 'lib/context.js'
10
4
  import { getOutput } from 'lib/output/get-output.js'
11
- import { promptAutocomplete, promptText } from 'lib/prompt.js'
5
+ import { promptAutocomplete } from 'lib/prompt.js'
12
6
 
13
7
  export async function interactForEndpointSelection() {
14
8
  const config = getConfigStore()
15
9
  assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint')
16
10
 
17
- const endpoints = [
18
- 'http://localhost:3020',
19
- 'https://connect.getseam.com',
20
- 'https://fakeseamconnect.seam.vc',
21
- ]
11
+ const endpoints = ['http://localhost:3020', 'https://connect.getseam.com']
22
12
 
23
13
  // Searchable, as selecting a device or a command is.
24
14
  const endpoint = await promptAutocomplete({
@@ -29,20 +19,6 @@ export async function interactForEndpointSelection() {
29
19
  })),
30
20
  })
31
21
 
32
- const output = getOutput()
33
- if (endpoint === endpoints[2]) {
34
- let userUrlSeed = await promptText({
35
- message:
36
- 'You can input a custom endpoint URL or leave this field empty to use a new fakeserver.',
37
- })
38
-
39
- if (userUrlSeed.trim().length === 0) {
40
- userUrlSeed = randomBytes(5).toString('hex')
41
- }
42
- selectFakeEndpoint({ urlSeed: userUrlSeed, config })
43
- output.info(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
44
- } else {
45
- selectEndpoint(endpoint, config)
46
- }
47
- output.info(`Endpoint set to ${endpoint}`)
22
+ selectEndpoint(endpoint, config)
23
+ getOutput().info(`Endpoint set to ${endpoint}`)
48
24
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The endpoint and workspace may be overridden for a single command.
3
+ *
4
+ * `--endpoint` and `--workspace-id` change what one invocation resolves to
5
+ * and are never stored: only `seam select endpoint` and `seam select
6
+ * workspace` write those settings. They are held here rather than threaded
7
+ * through every call because auth resolves ambiently, exactly as the
8
+ * environment variables they shadow do (see `env.ts`).
9
+ *
10
+ * Set once from the parsed arguments before anything resolves auth, and
11
+ * reset between tests.
12
+ */
13
+
14
+ export interface AuthOverrides {
15
+ endpoint: string | null
16
+ workspaceId: string | null
17
+ }
18
+
19
+ const noOverrides: AuthOverrides = { endpoint: null, workspaceId: null }
20
+
21
+ let overrides: AuthOverrides = noOverrides
22
+
23
+ export const getAuthOverrides = (): AuthOverrides => overrides
24
+
25
+ export const setAuthOverrides = (next: AuthOverrides): void => {
26
+ overrides = next
27
+ }
28
+
29
+ /** Drop the overrides, e.g., between tests sharing a process. */
30
+ export const resetAuthOverrides = (): void => {
31
+ overrides = noOverrides
32
+ }
@@ -131,6 +131,7 @@ const commandSections = (
131
131
  ): Section[] => {
132
132
  const name = ['seam', ...command.path].join(' ')
133
133
  const hasFlags = command.flags.length > 0
134
+ const { positional } = command
134
135
 
135
136
  return [
136
137
  {
@@ -139,7 +140,26 @@ const commandSections = (
139
140
  (line) => line !== '',
140
141
  ),
141
142
  },
142
- { header: 'Usage', content: `${name} [options]` },
143
+ {
144
+ header: 'Usage',
145
+ content:
146
+ positional == null
147
+ ? `${name} [options]`
148
+ : `${name} <${positional.name}> [options]`,
149
+ },
150
+ ...(positional == null
151
+ ? []
152
+ : [
153
+ {
154
+ header: 'Arguments',
155
+ content: [
156
+ {
157
+ name: `{underline <${positional.name}>}`,
158
+ summary: positional.description,
159
+ },
160
+ ],
161
+ },
162
+ ]),
143
163
  // The command's own parameters are what the request is made of, so keep
144
164
  // them apart from the options every seam command takes.
145
165
  ...(hasFlags ? [optionSection(command.flags, 'Parameters')] : []),
@@ -1,5 +1,5 @@
1
1
  // Versions are replaced with generated values when the package is packed.
2
- const seamapiCliVersion = '0.20.0'
2
+ const seamapiCliVersion = '0.22.0'
3
3
  const seamapiBlueprintVersion = '1.5.0'
4
4
 
5
5
  export { seamapiBlueprintVersion }