@seamapi/cli 0.9.0 → 0.11.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 (87) hide show
  1. package/README.md +83 -6
  2. package/bin/cli.js +106 -31
  3. package/bin/cli.js.map +1 -1
  4. package/lib/get-response-key.d.ts +9 -0
  5. package/lib/get-response-key.js +21 -0
  6. package/lib/get-response-key.js.map +1 -0
  7. package/lib/interact-for-action-attempt-poll.js +4 -3
  8. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  9. package/lib/interact-for-array.js +9 -7
  10. package/lib/interact-for-array.js.map +1 -1
  11. package/lib/interact-for-blueprint-object.js +21 -10
  12. package/lib/interact-for-blueprint-object.js.map +1 -1
  13. package/lib/interact-for-command-selection.js +17 -2
  14. package/lib/interact-for-command-selection.js.map +1 -1
  15. package/lib/interact-for-custom-metadata.js +10 -8
  16. package/lib/interact-for-custom-metadata.js.map +1 -1
  17. package/lib/interact-for-login.js +8 -6
  18. package/lib/interact-for-login.js.map +1 -1
  19. package/lib/interact-for-resource.js +2 -2
  20. package/lib/interact-for-resource.js.map +1 -1
  21. package/lib/interact-for-server-selection.js +7 -5
  22. package/lib/interact-for-server-selection.js.map +1 -1
  23. package/lib/interact-for-timestamp.js +2 -2
  24. package/lib/interact-for-timestamp.js.map +1 -1
  25. package/lib/interact-for-use-remote-api-defs.js +4 -3
  26. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  27. package/lib/interact-for-workspace-id.js +2 -2
  28. package/lib/interact-for-workspace-id.js.map +1 -1
  29. package/lib/output/create-memory-output.d.ts +13 -0
  30. package/lib/output/create-memory-output.js +22 -0
  31. package/lib/output/create-memory-output.js.map +1 -0
  32. package/lib/output/create-output.d.ts +50 -0
  33. package/lib/output/create-output.js +34 -0
  34. package/lib/output/create-output.js.map +1 -0
  35. package/lib/output/get-output.d.ts +12 -0
  36. package/lib/output/get-output.js +21 -0
  37. package/lib/output/get-output.js.map +1 -0
  38. package/lib/output/resolve-output-format.d.ts +16 -0
  39. package/lib/output/resolve-output-format.js +23 -0
  40. package/lib/output/resolve-output-format.js.map +1 -0
  41. package/lib/output/select-response-payload.d.ts +16 -0
  42. package/lib/output/select-response-payload.js +33 -0
  43. package/lib/output/select-response-payload.js.map +1 -0
  44. package/lib/types.d.ts +2 -1
  45. package/lib/util/cli-args.d.ts +45 -0
  46. package/lib/util/cli-args.js +58 -0
  47. package/lib/util/cli-args.js.map +1 -0
  48. package/lib/util/prompt.d.ts +17 -0
  49. package/lib/util/prompt.js +25 -0
  50. package/lib/util/prompt.js.map +1 -0
  51. package/lib/util/read-stdin-json.d.ts +22 -0
  52. package/lib/util/read-stdin-json.js +42 -0
  53. package/lib/util/read-stdin-json.js.map +1 -0
  54. package/lib/util/request-seam-api.d.ts +5 -2
  55. package/lib/util/request-seam-api.js +13 -15
  56. package/lib/util/request-seam-api.js.map +1 -1
  57. package/lib/util/with-loading.js +6 -1
  58. package/lib/util/with-loading.js.map +1 -1
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/version.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/bin/cli.ts +139 -33
  64. package/src/lib/get-response-key.ts +25 -0
  65. package/src/lib/interact-for-action-attempt-poll.ts +4 -3
  66. package/src/lib/interact-for-array.ts +9 -7
  67. package/src/lib/interact-for-blueprint-object.ts +27 -10
  68. package/src/lib/interact-for-command-selection.ts +23 -3
  69. package/src/lib/interact-for-custom-metadata.ts +10 -8
  70. package/src/lib/interact-for-login.ts +8 -6
  71. package/src/lib/interact-for-resource.ts +2 -3
  72. package/src/lib/interact-for-server-selection.ts +7 -6
  73. package/src/lib/interact-for-timestamp.ts +2 -2
  74. package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
  75. package/src/lib/interact-for-workspace-id.ts +2 -2
  76. package/src/lib/output/create-memory-output.ts +39 -0
  77. package/src/lib/output/create-output.ts +104 -0
  78. package/src/lib/output/get-output.ts +24 -0
  79. package/src/lib/output/resolve-output-format.ts +33 -0
  80. package/src/lib/output/select-response-payload.ts +56 -0
  81. package/src/lib/types.ts +2 -1
  82. package/src/lib/util/cli-args.ts +92 -0
  83. package/src/lib/util/prompt.ts +38 -0
  84. package/src/lib/util/read-stdin-json.ts +59 -0
  85. package/src/lib/util/request-seam-api.ts +24 -20
  86. package/src/lib/util/with-loading.ts +9 -1
  87. package/src/lib/version.ts +1 -1
package/src/bin/cli.ts CHANGED
@@ -4,11 +4,11 @@ import { isDeepStrictEqual as isEqual } from 'node:util'
4
4
 
5
5
  import chalk from 'chalk'
6
6
  import commandLineUsage from 'command-line-usage'
7
- import parseArgs, { type ParsedArgs } from 'minimist'
8
- import prompts from 'prompts'
7
+ import type { ParsedArgs } from 'minimist'
9
8
 
10
9
  import { getConfigStore } from 'lib/config/index.js'
11
10
  import { getApiBlueprint } from 'lib/get-api-blueprint.js'
11
+ import { getResponseKey } from 'lib/get-response-key.js'
12
12
  import { getServer } from 'lib/get-server.js'
13
13
  import { interactForActionAttemptPoll } from 'lib/interact-for-action-attempt-poll.js'
14
14
  import { interactForCommandParams } from 'lib/interact-for-command-params.js'
@@ -17,7 +17,19 @@ import { interactForLogin } from 'lib/interact-for-login.js'
17
17
  import { interactForServerSelection } from 'lib/interact-for-server-selection.js'
18
18
  import { interactForUseRemoteApiDefs } from 'lib/interact-for-use-remote-api-defs.js'
19
19
  import { interactForWorkspaceId } from 'lib/interact-for-workspace-id.js'
20
+ import { createOutput } from 'lib/output/create-output.js'
21
+ import { getOutput, setOutput } from 'lib/output/get-output.js'
22
+ import { resolveOutputFormat } from 'lib/output/resolve-output-format.js'
20
23
  import type { ContextHelpers } from 'lib/types.js'
24
+ import {
25
+ cliFlags,
26
+ getInteractivity,
27
+ type Interactivity,
28
+ NonInteractiveError,
29
+ parseCliArgs,
30
+ } from 'lib/util/cli-args.js'
31
+ import { canPrompt, prompt } from 'lib/util/prompt.js'
32
+ import { readStdinJson } from 'lib/util/read-stdin-json.js'
21
33
  import { RequestSeamApi } from 'lib/util/request-seam-api.js'
22
34
  import { validateToken } from 'lib/validate-token.js'
23
35
  import seamapiCliVersion from 'lib/version.js'
@@ -26,7 +38,7 @@ const sections = [
26
38
  {
27
39
  header: 'Seam CLI',
28
40
  content:
29
- 'Every seam command is interactive and will prompt you for any missing required properties with helpful suggestions. To avoid automatic behavior, pass -y ',
41
+ 'Every seam command runs as soon as every required property is given, and otherwise prompts you for what is missing with helpful suggestions. Pass -i to always review properties first, or -y to never be prompted. ',
30
42
  },
31
43
  {
32
44
  header: 'Options',
@@ -37,6 +49,26 @@ const sections = [
37
49
  alias: 'h',
38
50
  type: Boolean,
39
51
  },
52
+ {
53
+ name: 'interactive',
54
+ description:
55
+ 'Always prompt to review and edit properties, prefilled with the given arguments.',
56
+ alias: 'i',
57
+ type: Boolean,
58
+ },
59
+ {
60
+ name: 'non-interactive',
61
+ description:
62
+ 'Never prompt: exit with an error if the command or any required property is missing.',
63
+ alias: 'y',
64
+ type: Boolean,
65
+ },
66
+ {
67
+ name: 'json',
68
+ description:
69
+ 'Write the response to stdout as JSON. Enabled automatically when stdout is not a terminal, disable with {bold --no-json}.',
70
+ type: Boolean,
71
+ },
40
72
  {
41
73
  name: 'update',
42
74
  description: 'Force an update of the cached Seam API definitions.',
@@ -44,6 +76,14 @@ const sections = [
44
76
  },
45
77
  ],
46
78
  },
79
+ {
80
+ header: 'Output',
81
+ content: [
82
+ 'Only the response is written to stdout, so it is safe to pipe. Prompts, progress, and other information are written to stderr.',
83
+ 'The response is trimmed to the response key and pagination.',
84
+ 'Request params may be piped or redirected in as a JSON object. Params given as arguments win over params read from stdin.',
85
+ ],
86
+ },
47
87
  {
48
88
  header: 'Command List Examples',
49
89
  content: [
@@ -59,6 +99,14 @@ const sections = [
59
99
  summary: 'Create a connect webview to connect devices.',
60
100
  },
61
101
  { name: 'seam devices list', summary: 'List devices in your workspace.' },
102
+ {
103
+ name: 'seam devices list {bold --interactive}',
104
+ summary: 'Review and edit filters before listing devices.',
105
+ },
106
+ {
107
+ name: 'seam devices list {bold --non-interactive}',
108
+ summary: 'List devices, failing instead of prompting.',
109
+ },
62
110
  {
63
111
  name: 'seam locks unlock-door {bold --device-id} $MY_DOOR',
64
112
  summary: 'Unlock a lock.',
@@ -71,22 +119,30 @@ const sections = [
71
119
  name: 'seam access-codes list {bold --device-id} $MY_DOOR',
72
120
  summary: 'List you access codes.',
73
121
  },
122
+ {
123
+ name: 'seam devices list > devices.json',
124
+ summary: 'Write the response to a file as JSON.',
125
+ },
126
+ {
127
+ name: 'cat params.json | seam locks unlock-door',
128
+ summary: 'Pipe request params in as JSON.',
129
+ },
74
130
  ],
75
131
  },
76
132
  ]
77
133
 
78
134
  async function cli(args: ParsedArgs) {
79
135
  const config = getConfigStore()
136
+ const output = getOutput()
80
137
 
81
138
  if (args['help'] || args['h']) {
82
- const usage = commandLineUsage(sections)
83
- console.log(usage)
139
+ output.text(commandLineUsage(sections))
84
140
  return
85
141
  }
86
142
 
87
143
  if (args['version']) {
88
- console.log(seamapiCliVersion)
89
- process.exit(0)
144
+ output.text(seamapiCliVersion)
145
+ return
90
146
  }
91
147
 
92
148
  if (
@@ -98,10 +154,10 @@ async function cli(args: ParsedArgs) {
98
154
  const fakeApiUrl = `https://${randomstring}.fakeseamconnect.seam.vc`
99
155
 
100
156
  config.set('server', fakeApiUrl)
101
- console.log(`Server URL set to ${fakeApiUrl}`)
157
+ output.info(`Server URL set to ${fakeApiUrl}`)
102
158
 
103
159
  config.set(`${getServer()}.pat`, `seam_apikey1_token`)
104
- console.log(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
160
+ output.info(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
105
161
  return
106
162
  }
107
163
 
@@ -110,8 +166,9 @@ async function cli(args: ParsedArgs) {
110
166
  args._[0] !== 'login' &&
111
167
  !isEqual(args._, ['select', 'server'])
112
168
  ) {
113
- console.log(`Not logged in. Please run "seam login"`)
114
- process.exit(1)
169
+ output.error(`Not logged in. Please run "seam login"`)
170
+ process.exitCode = 1
171
+ return
115
172
  }
116
173
 
117
174
  args._ = args._.map((arg) => arg.toLowerCase().replace(/_/g, '-'))
@@ -129,24 +186,25 @@ async function cli(args: ParsedArgs) {
129
186
  update,
130
187
  })
131
188
 
132
- const commandParams: Record<string, any> = {}
133
-
134
- /**
135
- * Whether or not to auto-select first option
136
- */
137
- const is_interactive = args['y'] !== true
189
+ // Params piped or redirected in, e.g., `seam devices list < params.json`.
190
+ // Params given as arguments take precedence over these.
191
+ const commandParams: Record<string, any> = { ...(await readStdinJson()) }
138
192
 
139
193
  const ctx: ContextHelpers = {
140
194
  blueprint,
141
- is_interactive,
195
+ interactivity: getInteractivity(args, { canPrompt: canPrompt() }),
142
196
  }
143
197
 
198
+ const isNonInteractive = ctx.interactivity === 'non-interactive'
199
+
144
200
  for (const k in args) {
145
201
  if (k === '_') continue
146
202
  const v = args[k]
147
203
  delete args[k]
148
- args[k.replace(/-/g, '_')] = v
149
- commandParams[k.replace(/-/g, '_')] = v
204
+ const key = k.replace(/-/g, '_')
205
+ args[key] = v
206
+ if (cliFlags.includes(key)) continue
207
+ commandParams[key] = v
150
208
  }
151
209
 
152
210
  const selectedCommand = await interactForCommandSelection(args._, ctx)
@@ -167,19 +225,34 @@ async function cli(args: ParsedArgs) {
167
225
  if (args['token'] || args['workspace_id'] || args['server']) {
168
226
  return
169
227
  }
228
+ if (isNonInteractive) {
229
+ throw new NonInteractiveError(
230
+ 'Missing required parameter for login: --token',
231
+ )
232
+ }
170
233
  await interactForLogin()
171
234
  return
172
235
  } else if (isEqual(selectedCommand, ['logout'])) {
173
236
  config.delete('pat')
174
- console.log('Logged out!')
237
+ output.info('Logged out!')
175
238
  return
176
239
  } else if (isEqual(selectedCommand, ['config', 'reveal-location'])) {
177
- console.log(config.path)
240
+ output.text(config.path)
178
241
  return
179
242
  } else if (isEqual(selectedCommand, ['config', 'use-remote-api-defs'])) {
243
+ if (isNonInteractive) {
244
+ throw new NonInteractiveError(
245
+ 'Cannot select whether to use remote API definitions in non-interactive mode',
246
+ )
247
+ }
180
248
  await interactForUseRemoteApiDefs()
181
249
  return
182
250
  } else if (isEqual(selectedCommand, ['select', 'workspace'])) {
251
+ if (isNonInteractive) {
252
+ throw new NonInteractiveError(
253
+ 'Cannot select a workspace in non-interactive mode: pass --workspace-id to "seam login"',
254
+ )
255
+ }
183
256
  await interactForWorkspaceId()
184
257
  return
185
258
  } else if (isEqual(selectedCommand, ['events', 'list'])) {
@@ -194,6 +267,11 @@ async function cli(args: ParsedArgs) {
194
267
  config.delete('current_workspace_id')
195
268
  return
196
269
  }
270
+ if (isNonInteractive) {
271
+ throw new NonInteractiveError(
272
+ 'Missing required parameter for select server: --server',
273
+ )
274
+ }
197
275
  await interactForServerSelection()
198
276
  return
199
277
  } else if (isEqual(selectedCommand, ['health', 'get-health'])) {
@@ -243,22 +321,32 @@ async function cli(args: ParsedArgs) {
243
321
  const response = await RequestSeamApi({
244
322
  path: apiPath,
245
323
  params,
324
+ responseKey: getResponseKey(selectedCommand, ctx),
246
325
  })
247
326
 
248
327
  if (response.data?.connect_webview) {
249
- await handleConnectWebviewResponse(response.data.connect_webview)
328
+ await handleConnectWebviewResponse(
329
+ response.data.connect_webview,
330
+ ctx.interactivity,
331
+ )
250
332
  }
251
333
 
252
- if (response.data?.action_attempt) {
253
- interactForActionAttemptPoll(response.data.action_attempt)
334
+ if (response.data?.action_attempt && !isNonInteractive) {
335
+ await interactForActionAttemptPoll(response.data.action_attempt)
254
336
  }
255
337
  }
256
338
 
257
- const handleConnectWebviewResponse = async (connect_webview: any) => {
339
+ const handleConnectWebviewResponse = async (
340
+ connect_webview: any,
341
+ interactivity: Interactivity,
342
+ ) => {
258
343
  const url = connect_webview.url
259
344
 
260
- if (process.env['INSIDE_WEB_BROWSER'] !== '1') {
261
- const { action } = await prompts({
345
+ if (
346
+ interactivity !== 'non-interactive' &&
347
+ process.env['INSIDE_WEB_BROWSER'] !== '1'
348
+ ) {
349
+ const { action } = await prompt({
262
350
  type: 'confirm',
263
351
  name: 'action',
264
352
  message: 'Would you like to open the webview in your browser?',
@@ -281,12 +369,30 @@ const run = async (argv: string[]) => {
281
369
  return
282
370
  }
283
371
 
284
- await cli(parseArgs(argv, { string: ['code'] }))
372
+ const args = parseCliArgs(argv)
373
+
374
+ const isTty = process.stdout.isTTY === true
375
+
376
+ setOutput(
377
+ createOutput({
378
+ format: resolveOutputFormat(argv, { isTty }),
379
+ colors: isTty,
380
+ }),
381
+ )
382
+
383
+ await cli(args)
285
384
  }
286
385
 
287
- run(process.argv.slice(2)).catch((e) => {
288
- console.log(chalk.red(`CLI Error: ${e.toString()}\n${e.stack}`))
289
- if (e.toString().includes('object Object')) {
290
- console.log(e)
386
+ run(process.argv.slice(2)).catch((e: unknown) => {
387
+ const output = getOutput()
388
+ process.exitCode = 1
389
+
390
+ if (e instanceof NonInteractiveError) {
391
+ output.error(chalk.red(e.message))
392
+ return
291
393
  }
394
+
395
+ const error = e instanceof Error ? e : new Error(String(e))
396
+ output.error(chalk.red(`CLI Error: ${error.message}`))
397
+ if (error.stack != null) output.error(chalk.gray(error.stack))
292
398
  })
@@ -0,0 +1,25 @@
1
+ import { getCommandBlueprintDef } from './get-command-blueprint-def.js'
2
+ import type { ContextHelpers } from './types.js'
3
+
4
+ /**
5
+ * The top level response key documented for a command,
6
+ * e.g., `devices` for `seam devices list`.
7
+ *
8
+ * Returns null when the command is not in the blueprint,
9
+ * or when it does not respond with a resource.
10
+ */
11
+ export const getResponseKey = (
12
+ command: string[],
13
+ ctx: ContextHelpers,
14
+ ): string | null => {
15
+ let endpoint
16
+ try {
17
+ endpoint = getCommandBlueprintDef(command, ctx)
18
+ } catch {
19
+ return null
20
+ }
21
+
22
+ if (endpoint.response.responseType === 'void') return null
23
+
24
+ return endpoint.response.responseKey
25
+ }
@@ -1,14 +1,15 @@
1
1
  import type { ActionAttemptsGetResponse } from '@seamapi/http/connect'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { getSeam } from './get-seam.js'
4
+ import { getOutput } from './output/get-output.js'
5
+ import { prompt } from './util/prompt.js'
5
6
  import { withLoading } from './util/with-loading.js'
6
7
 
7
8
  export const interactForActionAttemptPoll = async (
8
9
  action_attempt: ActionAttemptsGetResponse['action_attempt'],
9
10
  ) => {
10
11
  if (action_attempt.status === 'pending') {
11
- const { poll_for_action_attempt } = await prompts({
12
+ const { poll_for_action_attempt } = await prompt({
12
13
  name: 'poll_for_action_attempt',
13
14
  message: "Would you like to poll the action attempt until it's ready?",
14
15
  type: 'toggle',
@@ -29,7 +30,7 @@ export const interactForActionAttemptPoll = async (
29
30
  { waitForActionAttempt: { pollingInterval: 240, timeout: 10_000 } },
30
31
  ),
31
32
  )
32
- console.dir(updated_action_attempt, { depth: null })
33
+ getOutput().data({ action_attempt: updated_action_attempt })
33
34
  }
34
35
  }
35
36
  }
@@ -1,19 +1,21 @@
1
- import prompts from 'prompts'
1
+ import { getOutput } from './output/get-output.js'
2
+ import { prompt } from './util/prompt.js'
2
3
 
3
4
  export const interactForArray = async (
4
5
  array: string[],
5
6
  message: string,
6
7
  ): Promise<string[]> => {
7
8
  const updatedArray = [...array]
9
+ const output = getOutput()
8
10
 
9
11
  const displayList = () => {
10
- console.log(`${message} Current list:`)
12
+ output.info(`${message} Current list:`)
11
13
  if (updatedArray.length > 0) {
12
14
  updatedArray.forEach((item, index) => {
13
- console.log(`${index + 1}: ${item}`)
15
+ output.info(`${index + 1}: ${item}`)
14
16
  })
15
17
  } else {
16
- console.log('The list is currently empty.')
18
+ output.info('The list is currently empty.')
17
19
  }
18
20
  }
19
21
 
@@ -21,7 +23,7 @@ export const interactForArray = async (
21
23
  do {
22
24
  displayList()
23
25
 
24
- const response = await prompts({
26
+ const response = await prompt({
25
27
  type: 'select',
26
28
  name: 'action',
27
29
  message: 'Choose an action:',
@@ -35,7 +37,7 @@ export const interactForArray = async (
35
37
  action = response.action
36
38
 
37
39
  if (action === 'add') {
38
- const { newItem } = await prompts({
40
+ const { newItem } = await prompt({
39
41
  type: 'text',
40
42
  name: 'newItem',
41
43
  message: 'Enter the new item:',
@@ -44,7 +46,7 @@ export const interactForArray = async (
44
46
  updatedArray.push(newItem)
45
47
  }
46
48
  } else if (action === 'remove') {
47
- const { index } = await prompts({
49
+ const { index } = await prompt({
48
50
  type: 'number',
49
51
  name: 'index',
50
52
  message: 'Enter the index of the item to remove:',
@@ -1,5 +1,4 @@
1
1
  import type { Parameter } from '@seamapi/blueprint'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { interactForAccessCode } from './interact-for-access-code.js'
5
4
  import { interactForAcsEntrance } from './interact-for-acs-entrance.js'
@@ -11,8 +10,11 @@ import { interactForCustomMetadata } from './interact-for-custom-metadata.js'
11
10
  import { interactForDevice } from './interact-for-device.js'
12
11
  import { interactForTimestamp } from './interact-for-timestamp.js'
13
12
  import { interactForUserIdentity } from './interact-for-user-identity.js'
13
+ import { getOutput } from './output/get-output.js'
14
14
  import type { ContextHelpers } from './types.js'
15
+ import { NonInteractiveError, toArgName } from './util/cli-args.js'
15
16
  import { ellipsis } from './util/ellipsis.js'
17
+ import { prompt } from './util/prompt.js'
16
18
 
17
19
  const ergonomicPropOrder = [
18
20
  'name',
@@ -47,12 +49,28 @@ export const interactForBlueprintObject = async (
47
49
 
48
50
  const haveAllRequiredParams = required.every((k) => args.params[k])
49
51
 
52
+ const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
53
+
50
54
  const should_auto_submit =
51
- !ctx.is_interactive && haveAllRequiredParams && !args.isSubProperty
55
+ ctx.interactivity !== 'interactive' &&
56
+ haveAllRequiredParams &&
57
+ !args.isSubProperty
52
58
  if (should_auto_submit) {
53
59
  return args.params
54
60
  }
55
61
 
62
+ if (ctx.interactivity === 'non-interactive') {
63
+ const missing = required.filter((k) => !args.params[k])
64
+ const target = args.isSubProperty ? `"${args.subPropertyPath}"` : cmdPath
65
+ throw new NonInteractiveError(
66
+ missing.length > 0
67
+ ? `Missing required ${
68
+ missing.length === 1 ? 'parameter' : 'parameters'
69
+ } for ${target}: ${missing.map(toArgName).join(' ')}`
70
+ : `Cannot prompt for ${target} in non-interactive mode`,
71
+ )
72
+ }
73
+
56
74
  const propSortScore = (prop: string) => {
57
75
  if (required.includes(prop)) return 100 - ergonomicPropOrder.indexOf(prop)
58
76
  if (args.params[prop] !== undefined) {
@@ -61,13 +79,12 @@ export const interactForBlueprintObject = async (
61
79
  return ergonomicPropOrder.indexOf(prop)
62
80
  }
63
81
 
64
- const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
65
82
  const parameterSelectionMessage = args.isSubProperty
66
83
  ? `Editing "${args.subPropertyPath}"`
67
84
  : `[${cmdPath}] Parameters`
68
85
 
69
- console.log('')
70
- const { paramToEdit } = await prompts({
86
+ getOutput().info()
87
+ const { paramToEdit } = await prompt({
71
88
  name: 'paramToEdit',
72
89
  message: parameterSelectionMessage,
73
90
  type: 'autocomplete',
@@ -187,7 +204,7 @@ export const interactForBlueprintObject = async (
187
204
  value = await interactForTimestamp()
188
205
  } else {
189
206
  value = (
190
- await prompts({
207
+ await prompt({
191
208
  name: 'value',
192
209
  message: `${paramToEdit}:`,
193
210
  type: 'text',
@@ -198,7 +215,7 @@ export const interactForBlueprintObject = async (
198
215
  return interactForBlueprintObject(args, ctx)
199
216
  } else if (prop.format === 'enum') {
200
217
  const value = (
201
- await prompts({
218
+ await prompt({
202
219
  name: 'value',
203
220
  message: `${paramToEdit}:`,
204
221
  type: 'select',
@@ -211,7 +228,7 @@ export const interactForBlueprintObject = async (
211
228
  args.params[paramToEdit] = value
212
229
  return interactForBlueprintObject(args, ctx)
213
230
  } else if (prop.format === 'boolean') {
214
- const { value } = await prompts({
231
+ const { value } = await prompt({
215
232
  name: 'value',
216
233
  message: `${paramToEdit}:`,
217
234
  type: 'toggle',
@@ -225,7 +242,7 @@ export const interactForBlueprintObject = async (
225
242
  return interactForBlueprintObject(args, ctx)
226
243
  } else if (prop.format === 'list' && prop.itemFormat === 'enum') {
227
244
  const value = (
228
- await prompts({
245
+ await prompt({
229
246
  name: 'value',
230
247
  message: `${paramToEdit}:`,
231
248
  type: 'autocompleteMultiselect',
@@ -256,7 +273,7 @@ export const interactForBlueprintObject = async (
256
273
  )
257
274
  return interactForBlueprintObject(args, ctx)
258
275
  } else if (prop.format === 'number') {
259
- const { value } = await prompts({
276
+ const { value } = await prompt({
260
277
  name: 'value',
261
278
  message: `${paramToEdit}:`,
262
279
  type: 'number',
@@ -1,8 +1,8 @@
1
1
  import { isDeepStrictEqual as isEqual } from 'node:util'
2
2
 
3
- import prompts from 'prompts'
4
-
5
3
  import type { ContextHelpers } from './types.js'
4
+ import { NonInteractiveError } from './util/cli-args.js'
5
+ import { prompt } from './util/prompt.js'
6
6
 
7
7
  const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
8
8
  const seen = new Set<unknown>()
@@ -64,6 +64,26 @@ export async function interactForCommandSelection(
64
64
  return commandPath
65
65
  }
66
66
 
67
+ if (helpers.interactivity === 'non-interactive') {
68
+ // The command path is itself a command, so call it directly rather than
69
+ // prompting to select one of its sub-commands.
70
+ if (possibleCommands.some((cmd) => cmd.length === commandPath.length)) {
71
+ return commandPath
72
+ }
73
+
74
+ const subcommands = possibleCommands
75
+ .map((cmd) => cmd[commandPath.length])
76
+ .filter((subcommand) => subcommand != null)
77
+ .sort(ergonomicSort)
78
+ throw new NonInteractiveError(
79
+ `${
80
+ commandPath.length === 0
81
+ ? 'Missing command'
82
+ : `Incomplete command "seam ${commandPath.join(' ')}"`
83
+ }: expected one of ${subcommands.join(', ')}`,
84
+ )
85
+ }
86
+
67
87
  // Add dynamic 'back' command for sub-commands to allow returning
68
88
  // to previous level.
69
89
  if (commandPath.length > 0) {
@@ -72,7 +92,7 @@ export async function interactForCommandSelection(
72
92
 
73
93
  const commandPathStr = commandPath.join('/').replace(/-/g, '_')
74
94
 
75
- const res = await prompts({
95
+ const res = await prompt({
76
96
  name: 'Command',
77
97
  type: 'autocomplete',
78
98
  choices: [
@@ -1,4 +1,5 @@
1
- import prompts from 'prompts'
1
+ import { getOutput } from './output/get-output.js'
2
+ import { prompt } from './util/prompt.js'
2
3
 
3
4
  // Structurally the CustomMetadata of @seamapi/types, spelled out here so the
4
5
  // published declarations do not depend on a development-only package.
@@ -12,15 +13,16 @@ export const interactForCustomMetadata = async (
12
13
  custom_metadata: CustomMetadata,
13
14
  ) => {
14
15
  const updated_custom_metadata: UpdatedCustomMetadata = { ...custom_metadata }
16
+ const output = getOutput()
15
17
 
16
18
  const displayCurrentCustomMetadata = () => {
17
- console.log('custom_metadata:')
19
+ output.info('custom_metadata:')
18
20
  if (Object.keys(updated_custom_metadata).length > 0) {
19
21
  Object.keys(updated_custom_metadata).forEach((key, index) => {
20
- console.log(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
22
+ output.info(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
21
23
  })
22
24
  } else {
23
- console.log('The custom metadata param is empty.')
25
+ output.info('The custom metadata param is empty.')
24
26
  }
25
27
  }
26
28
 
@@ -29,7 +31,7 @@ export const interactForCustomMetadata = async (
29
31
  do {
30
32
  displayCurrentCustomMetadata()
31
33
 
32
- const response = await prompts({
34
+ const response = await prompt({
33
35
  type: 'select',
34
36
  name: 'action',
35
37
  message: 'Choose an action:',
@@ -43,13 +45,13 @@ export const interactForCustomMetadata = async (
43
45
  action = response.action
44
46
 
45
47
  if (action === 'add') {
46
- const { newKey } = await prompts({
48
+ const { newKey } = await prompt({
47
49
  type: 'text',
48
50
  name: 'newKey',
49
51
  message: 'Enter a key to add or edit:',
50
52
  })
51
53
 
52
- let { newValue } = await prompts({
54
+ let { newValue } = await prompt({
53
55
  type: 'text',
54
56
  name: 'newValue',
55
57
  message: 'Enter the new value to add or edit (or null to delete):',
@@ -65,7 +67,7 @@ export const interactForCustomMetadata = async (
65
67
  }
66
68
  }
67
69
  } else if (action === 'remove') {
68
- const { custom_key_to_remove } = await prompts({
70
+ const { custom_key_to_remove } = await prompt({
69
71
  type: 'select',
70
72
  name: 'custom_key_to_remove',
71
73
  message: 'Choose a key-value pair to remove from params:',
@@ -1,33 +1,35 @@
1
1
  import { isApiKey, isPersonalAccessToken } from '@seamapi/http/connect'
2
2
  import chalk from 'chalk'
3
- import prompts from 'prompts'
4
3
 
5
4
  import { getConfigStore } from './config/index.js'
6
5
  import { getServer } from './get-server.js'
7
6
  import { interactForWorkspaceId } from './interact-for-workspace-id.js'
7
+ import { getOutput } from './output/get-output.js'
8
+ import { prompt } from './util/prompt.js'
8
9
  import { withLoading } from './util/with-loading.js'
9
10
  import { validateToken } from './validate-token.js'
10
11
 
11
12
  export const interactForLogin = async () => {
12
13
  const config = await getConfigStore()
14
+ const output = getOutput()
13
15
 
14
16
  if (getServer().includes('localhost')) {
15
- console.log(
17
+ output.info(
16
18
  `You're using a local Seam Connect instance, you can enter the API Key to your local user, you can create a new user from:\n\n${getServer()}/admin/create_user_with_api_key`,
17
19
  )
18
20
  } else {
19
- console.log(
21
+ output.info(
20
22
  `To login, navigate to the URL below and create a new Personal Access Token (PAT) and paste the PAT in the provided box:\n\nhttps://console.seam.co/settings/access-tokens\n\n`,
21
23
  )
22
24
  }
23
25
 
24
- console.log(
26
+ output.info(
25
27
  chalk.gray(
26
28
  '> Note: You can enter an API Key here for single-workspace access',
27
29
  ),
28
30
  )
29
31
 
30
- const { pat } = await prompts({
32
+ const { pat } = await prompt({
31
33
  name: 'pat',
32
34
  type: 'text',
33
35
  message: 'Personal Access Token:',
@@ -49,5 +51,5 @@ export const interactForLogin = async () => {
49
51
  }
50
52
 
51
53
  config.set(`${getServer()}.pat`, token)
52
- console.log(`Token saved! You may begin using the CLI!`)
54
+ output.info(`Token saved! You may begin using the CLI!`)
53
55
  }