@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.
- package/README.md +83 -6
- package/bin/cli.js +106 -31
- package/bin/cli.js.map +1 -1
- package/lib/get-response-key.d.ts +9 -0
- package/lib/get-response-key.js +21 -0
- package/lib/get-response-key.js.map +1 -0
- package/lib/interact-for-action-attempt-poll.js +4 -3
- package/lib/interact-for-action-attempt-poll.js.map +1 -1
- package/lib/interact-for-array.js +9 -7
- package/lib/interact-for-array.js.map +1 -1
- package/lib/interact-for-blueprint-object.js +21 -10
- package/lib/interact-for-blueprint-object.js.map +1 -1
- package/lib/interact-for-command-selection.js +17 -2
- package/lib/interact-for-command-selection.js.map +1 -1
- package/lib/interact-for-custom-metadata.js +10 -8
- package/lib/interact-for-custom-metadata.js.map +1 -1
- package/lib/interact-for-login.js +8 -6
- package/lib/interact-for-login.js.map +1 -1
- package/lib/interact-for-resource.js +2 -2
- package/lib/interact-for-resource.js.map +1 -1
- package/lib/interact-for-server-selection.js +7 -5
- package/lib/interact-for-server-selection.js.map +1 -1
- package/lib/interact-for-timestamp.js +2 -2
- package/lib/interact-for-timestamp.js.map +1 -1
- package/lib/interact-for-use-remote-api-defs.js +4 -3
- package/lib/interact-for-use-remote-api-defs.js.map +1 -1
- package/lib/interact-for-workspace-id.js +2 -2
- package/lib/interact-for-workspace-id.js.map +1 -1
- package/lib/output/create-memory-output.d.ts +13 -0
- package/lib/output/create-memory-output.js +22 -0
- package/lib/output/create-memory-output.js.map +1 -0
- package/lib/output/create-output.d.ts +50 -0
- package/lib/output/create-output.js +34 -0
- package/lib/output/create-output.js.map +1 -0
- package/lib/output/get-output.d.ts +12 -0
- package/lib/output/get-output.js +21 -0
- package/lib/output/get-output.js.map +1 -0
- package/lib/output/resolve-output-format.d.ts +16 -0
- package/lib/output/resolve-output-format.js +23 -0
- package/lib/output/resolve-output-format.js.map +1 -0
- package/lib/output/select-response-payload.d.ts +16 -0
- package/lib/output/select-response-payload.js +33 -0
- package/lib/output/select-response-payload.js.map +1 -0
- package/lib/types.d.ts +2 -1
- package/lib/util/cli-args.d.ts +45 -0
- package/lib/util/cli-args.js +58 -0
- package/lib/util/cli-args.js.map +1 -0
- package/lib/util/prompt.d.ts +17 -0
- package/lib/util/prompt.js +25 -0
- package/lib/util/prompt.js.map +1 -0
- package/lib/util/read-stdin-json.d.ts +22 -0
- package/lib/util/read-stdin-json.js +42 -0
- package/lib/util/read-stdin-json.js.map +1 -0
- package/lib/util/request-seam-api.d.ts +5 -2
- package/lib/util/request-seam-api.js +13 -15
- package/lib/util/request-seam-api.js.map +1 -1
- package/lib/util/with-loading.js +6 -1
- package/lib/util/with-loading.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/cli.ts +139 -33
- package/src/lib/get-response-key.ts +25 -0
- package/src/lib/interact-for-action-attempt-poll.ts +4 -3
- package/src/lib/interact-for-array.ts +9 -7
- package/src/lib/interact-for-blueprint-object.ts +27 -10
- package/src/lib/interact-for-command-selection.ts +23 -3
- package/src/lib/interact-for-custom-metadata.ts +10 -8
- package/src/lib/interact-for-login.ts +8 -6
- package/src/lib/interact-for-resource.ts +2 -3
- package/src/lib/interact-for-server-selection.ts +7 -6
- package/src/lib/interact-for-timestamp.ts +2 -2
- package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
- package/src/lib/interact-for-workspace-id.ts +2 -2
- package/src/lib/output/create-memory-output.ts +39 -0
- package/src/lib/output/create-output.ts +104 -0
- package/src/lib/output/get-output.ts +24 -0
- package/src/lib/output/resolve-output-format.ts +33 -0
- package/src/lib/output/select-response-payload.ts +56 -0
- package/src/lib/types.ts +2 -1
- package/src/lib/util/cli-args.ts +92 -0
- package/src/lib/util/prompt.ts +38 -0
- package/src/lib/util/read-stdin-json.ts +59 -0
- package/src/lib/util/request-seam-api.ts +24 -20
- package/src/lib/util/with-loading.ts +9 -1
- 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
|
|
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
|
|
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
|
-
|
|
83
|
-
console.log(usage)
|
|
139
|
+
output.text(commandLineUsage(sections))
|
|
84
140
|
return
|
|
85
141
|
}
|
|
86
142
|
|
|
87
143
|
if (args['version']) {
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
157
|
+
output.info(`Server URL set to ${fakeApiUrl}`)
|
|
102
158
|
|
|
103
159
|
config.set(`${getServer()}.pat`, `seam_apikey1_token`)
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
process.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
237
|
+
output.info('Logged out!')
|
|
175
238
|
return
|
|
176
239
|
} else if (isEqual(selectedCommand, ['config', 'reveal-location'])) {
|
|
177
|
-
|
|
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(
|
|
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 (
|
|
339
|
+
const handleConnectWebviewResponse = async (
|
|
340
|
+
connect_webview: any,
|
|
341
|
+
interactivity: Interactivity,
|
|
342
|
+
) => {
|
|
258
343
|
const url = connect_webview.url
|
|
259
344
|
|
|
260
|
-
if (
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
-
|
|
33
|
+
getOutput().data({ action_attempt: updated_action_attempt })
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
12
|
+
output.info(`${message} Current list:`)
|
|
11
13
|
if (updatedArray.length > 0) {
|
|
12
14
|
updatedArray.forEach((item, index) => {
|
|
13
|
-
|
|
15
|
+
output.info(`${index + 1}: ${item}`)
|
|
14
16
|
})
|
|
15
17
|
} else {
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
const { paramToEdit } = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
const res = await prompt({
|
|
76
96
|
name: 'Command',
|
|
77
97
|
type: 'autocomplete',
|
|
78
98
|
choices: [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
output.info(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
|
|
21
23
|
})
|
|
22
24
|
} else {
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
54
|
+
output.info(`Token saved! You may begin using the CLI!`)
|
|
53
55
|
}
|