@seamapi/cli 0.13.1 → 0.15.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 +65 -1
- package/bin/cli.js +105 -33
- package/bin/cli.js.map +1 -1
- package/lib/command-spec.d.ts +8 -0
- package/lib/command-spec.js +8 -0
- package/lib/command-spec.js.map +1 -1
- package/lib/env.d.ts +29 -0
- package/lib/env.js +41 -0
- package/lib/env.js.map +1 -0
- package/lib/get-credentials.d.ts +15 -0
- package/lib/get-credentials.js +35 -0
- package/lib/get-credentials.js.map +1 -0
- package/lib/get-current-workspace-id.js +3 -4
- package/lib/get-current-workspace-id.js.map +1 -1
- package/lib/get-seam.js +21 -9
- package/lib/get-seam.js.map +1 -1
- package/lib/get-server.d.ts +5 -0
- package/lib/get-server.js +11 -1
- package/lib/get-server.js.map +1 -1
- package/lib/interact-for-access-code.d.ts +1 -1
- package/lib/interact-for-access-code.js +7 -4
- package/lib/interact-for-access-code.js.map +1 -1
- package/lib/interact-for-acs-user.js +2 -2
- package/lib/interact-for-acs-user.js.map +1 -1
- package/lib/interact-for-action-attempt-poll.d.ts +1 -1
- package/lib/interact-for-action-attempt-poll.js +8 -8
- package/lib/interact-for-action-attempt-poll.js.map +1 -1
- package/lib/interact-for-blueprint-object.js +6 -8
- package/lib/interact-for-blueprint-object.js.map +1 -1
- package/lib/interact-for-custom-metadata.d.ts +1 -1
- package/lib/interact-for-custom-metadata.js +15 -15
- package/lib/interact-for-custom-metadata.js.map +1 -1
- package/lib/interact-for-login.js +2 -0
- package/lib/interact-for-login.js.map +1 -1
- package/lib/interact-for-server-selection.js +3 -0
- package/lib/interact-for-server-selection.js.map +1 -1
- package/lib/interact-for-use-remote-api-defs.js +4 -4
- package/lib/interact-for-use-remote-api-defs.js.map +1 -1
- package/lib/interact-for-workspace-id.js +2 -0
- package/lib/interact-for-workspace-id.js.map +1 -1
- package/lib/render-help.js +4 -0
- package/lib/render-help.js.map +1 -1
- package/lib/util/cli-args.d.ts +21 -0
- package/lib/util/cli-args.js +26 -1
- package/lib/util/cli-args.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/bin/cli.ts +148 -32
- package/src/lib/command-spec.ts +12 -0
- package/src/lib/env.d.ts +12 -0
- package/src/lib/env.ts +63 -0
- package/src/lib/get-credentials.ts +38 -0
- package/src/lib/get-current-workspace-id.ts +3 -5
- package/src/lib/get-seam.ts +39 -11
- package/src/lib/get-server.ts +12 -1
- package/src/lib/interact-for-access-code.ts +6 -4
- package/src/lib/interact-for-acs-user.ts +2 -2
- package/src/lib/interact-for-action-attempt-poll.ts +9 -9
- package/src/lib/interact-for-blueprint-object.ts +6 -8
- package/src/lib/interact-for-custom-metadata.ts +18 -20
- package/src/lib/interact-for-login.ts +3 -0
- package/src/lib/interact-for-server-selection.ts +10 -0
- package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
- package/src/lib/interact-for-workspace-id.ts +12 -0
- package/src/lib/render-help.ts +4 -0
- package/src/lib/util/cli-args.ts +33 -1
- package/src/lib/version.ts +1 -1
package/lib/version.js
CHANGED
package/package.json
CHANGED
package/src/bin/cli.ts
CHANGED
|
@@ -5,14 +5,26 @@ import { isDeepStrictEqual as isEqual } from 'node:util'
|
|
|
5
5
|
import chalk from 'chalk'
|
|
6
6
|
import type { ParsedArgs } from 'minimist'
|
|
7
7
|
|
|
8
|
-
import { getCommandSpec } from 'lib/command-spec.js'
|
|
8
|
+
import { findLocalCommand, getCommandSpec } from 'lib/command-spec.js'
|
|
9
9
|
import {
|
|
10
10
|
completionShells,
|
|
11
11
|
isCompletionShell,
|
|
12
12
|
renderCompletion,
|
|
13
13
|
} from 'lib/completion/index.js'
|
|
14
14
|
import { getConfigStore } from 'lib/config/index.js'
|
|
15
|
+
import {
|
|
16
|
+
assertEnvVarUnset,
|
|
17
|
+
endpointEnvVar,
|
|
18
|
+
EnvVarOverrideError,
|
|
19
|
+
getEndpointFromEnv,
|
|
20
|
+
getTokenFromEnv,
|
|
21
|
+
getWorkspaceIdFromEnv,
|
|
22
|
+
tokenEnvVar,
|
|
23
|
+
workspaceIdEnvVar,
|
|
24
|
+
} from 'lib/env.js'
|
|
15
25
|
import { getApiBlueprint } from 'lib/get-api-blueprint.js'
|
|
26
|
+
import { getCommandBlueprintDef } from 'lib/get-command-blueprint-def.js'
|
|
27
|
+
import { getToken } from 'lib/get-credentials.js'
|
|
16
28
|
import { getResponseKey } from 'lib/get-response-key.js'
|
|
17
29
|
import { getServer } from 'lib/get-server.js'
|
|
18
30
|
import { interactForActionAttemptPoll } from 'lib/interact-for-action-attempt-poll.js'
|
|
@@ -33,6 +45,9 @@ import {
|
|
|
33
45
|
type Interactivity,
|
|
34
46
|
NonInteractiveError,
|
|
35
47
|
parseCliArgs,
|
|
48
|
+
toGivenArgName,
|
|
49
|
+
toParameterName,
|
|
50
|
+
UsageError,
|
|
36
51
|
} from 'lib/util/cli-args.js'
|
|
37
52
|
import { canPrompt, prompt } from 'lib/util/prompt.js'
|
|
38
53
|
import { readStdinJson } from 'lib/util/read-stdin-json.js'
|
|
@@ -77,6 +92,29 @@ async function cli(args: ParsedArgs) {
|
|
|
77
92
|
return
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
args._ = args._.map(toCommandWord)
|
|
96
|
+
|
|
97
|
+
// Argument keys name parameters however they are written, so normalize each
|
|
98
|
+
// one to the name the API gives it. Replace the key rather than adding the
|
|
99
|
+
// normalized form alongside it, or an argument would be sent twice: once as
|
|
100
|
+
// written and once as the API names it.
|
|
101
|
+
for (const key of Object.keys(args)) {
|
|
102
|
+
if (key === '_') continue
|
|
103
|
+
const name = toParameterName(key)
|
|
104
|
+
if (name === key) continue
|
|
105
|
+
args[name] = args[key]
|
|
106
|
+
delete args[key]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Params given as arguments, kept apart from the params read from stdin so
|
|
110
|
+
// that only the arguments are held to what the command accepts.
|
|
111
|
+
const argParams: Record<string, any> = {}
|
|
112
|
+
for (const [key, value] of Object.entries(args)) {
|
|
113
|
+
if (key === '_') continue
|
|
114
|
+
if (cliFlags.includes(key)) continue
|
|
115
|
+
argParams[key] = value
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
if (args._[0] === 'completion') {
|
|
81
119
|
const shell = args._[1]
|
|
82
120
|
|
|
@@ -86,6 +124,8 @@ async function cli(args: ParsedArgs) {
|
|
|
86
124
|
return
|
|
87
125
|
}
|
|
88
126
|
|
|
127
|
+
assertKnownArgs(argParams, ['completion', shell])
|
|
128
|
+
|
|
89
129
|
// Completions always come from the cached API definitions so that they
|
|
90
130
|
// can be generated without logging in. They may lag the definitions
|
|
91
131
|
// served by Seam when config use-remote-api-defs is enabled.
|
|
@@ -100,6 +140,9 @@ async function cli(args: ParsedArgs) {
|
|
|
100
140
|
args._[1] === 'set' &&
|
|
101
141
|
args._[2] === 'fake-server'
|
|
102
142
|
) {
|
|
143
|
+
assertEnvVarUnset(endpointEnvVar, getEndpointFromEnv(), 'select a server')
|
|
144
|
+
assertEnvVarUnset(tokenEnvVar, getTokenFromEnv(), 'log in')
|
|
145
|
+
|
|
103
146
|
const randomstring = randomBytes(5).toString('hex')
|
|
104
147
|
const fakeApiUrl = `https://${randomstring}.fakeseamconnect.seam.vc`
|
|
105
148
|
|
|
@@ -112,24 +155,19 @@ async function cli(args: ParsedArgs) {
|
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
if (
|
|
115
|
-
|
|
158
|
+
getToken() == null &&
|
|
116
159
|
args._[0] !== 'login' &&
|
|
117
160
|
!isEqual(args._, ['select', 'server'])
|
|
118
161
|
) {
|
|
119
|
-
output.error(`Not logged in. Please run "seam login"`)
|
|
162
|
+
output.error(`Not logged in. Please run "seam login" or set ${tokenEnvVar}`)
|
|
120
163
|
process.exitCode = 1
|
|
121
164
|
return
|
|
122
165
|
}
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
for (const k in args) {
|
|
126
|
-
args[k.toLowerCase().replace(/-/g, '_')] = args[k]
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const use_remote_api_defs =
|
|
167
|
+
const useRemoteApiDefs =
|
|
130
168
|
args['remote_api_defs'] ?? config.get('use_remote_api_defs')
|
|
131
169
|
|
|
132
|
-
const blueprint = await getApiBlueprint(
|
|
170
|
+
const blueprint = await getApiBlueprint(useRemoteApiDefs ?? false, {
|
|
133
171
|
update,
|
|
134
172
|
})
|
|
135
173
|
|
|
@@ -144,18 +182,36 @@ async function cli(args: ParsedArgs) {
|
|
|
144
182
|
|
|
145
183
|
const isNonInteractive = ctx.interactivity === 'non-interactive'
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
if (k === '_') continue
|
|
149
|
-
const v = args[k]
|
|
150
|
-
delete args[k]
|
|
151
|
-
const key = k.replace(/-/g, '_')
|
|
152
|
-
args[key] = v
|
|
153
|
-
if (cliFlags.includes(key)) continue
|
|
154
|
-
commandParams[key] = v
|
|
155
|
-
}
|
|
185
|
+
Object.assign(commandParams, argParams)
|
|
156
186
|
|
|
157
187
|
const selectedCommand = await interactForCommandSelection(args._, ctx)
|
|
188
|
+
|
|
189
|
+
// Hit 'back' on a top-level command path, so we start again
|
|
190
|
+
if (selectedCommand.slice(-1)[0] === '[Back]') {
|
|
191
|
+
return await cli({
|
|
192
|
+
...args,
|
|
193
|
+
_: [],
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check the arguments before the command acts on any of them, so a mistake
|
|
198
|
+
// is reported rather than half applied.
|
|
199
|
+
assertKnownArgs(argParams, selectedCommand, ctx)
|
|
200
|
+
|
|
158
201
|
if (isEqual(selectedCommand, ['login'])) {
|
|
202
|
+
// Nothing is stored while the environment overrides it, so refuse before
|
|
203
|
+
// storing anything rather than part way through.
|
|
204
|
+
assertEnvVarUnset(tokenEnvVar, getTokenFromEnv(), 'log in')
|
|
205
|
+
if (args['server']) {
|
|
206
|
+
assertEnvVarUnset(endpointEnvVar, getEndpointFromEnv(), 'select a server')
|
|
207
|
+
}
|
|
208
|
+
if (args['workspace_id']) {
|
|
209
|
+
assertEnvVarUnset(
|
|
210
|
+
workspaceIdEnvVar,
|
|
211
|
+
getWorkspaceIdFromEnv(),
|
|
212
|
+
'select a workspace',
|
|
213
|
+
)
|
|
214
|
+
}
|
|
159
215
|
if (args['server']) {
|
|
160
216
|
config.set('server', args['server'])
|
|
161
217
|
config.delete('current_workspace_id')
|
|
@@ -180,6 +236,7 @@ async function cli(args: ParsedArgs) {
|
|
|
180
236
|
await interactForLogin()
|
|
181
237
|
return
|
|
182
238
|
} else if (isEqual(selectedCommand, ['logout'])) {
|
|
239
|
+
assertEnvVarUnset(tokenEnvVar, getTokenFromEnv(), 'log out')
|
|
183
240
|
config.delete('pat')
|
|
184
241
|
output.info('Logged out!')
|
|
185
242
|
return
|
|
@@ -195,6 +252,11 @@ async function cli(args: ParsedArgs) {
|
|
|
195
252
|
await interactForUseRemoteApiDefs()
|
|
196
253
|
return
|
|
197
254
|
} else if (isEqual(selectedCommand, ['select', 'workspace'])) {
|
|
255
|
+
assertEnvVarUnset(
|
|
256
|
+
workspaceIdEnvVar,
|
|
257
|
+
getWorkspaceIdFromEnv(),
|
|
258
|
+
'select a workspace',
|
|
259
|
+
)
|
|
198
260
|
if (isNonInteractive) {
|
|
199
261
|
throw new NonInteractiveError(
|
|
200
262
|
'Cannot select a workspace in non-interactive mode: pass --workspace-id to "seam login"',
|
|
@@ -209,6 +271,7 @@ async function cli(args: ParsedArgs) {
|
|
|
209
271
|
commandParams['since'] = date.toISOString()
|
|
210
272
|
}
|
|
211
273
|
} else if (isEqual(selectedCommand, ['select', 'server'])) {
|
|
274
|
+
assertEnvVarUnset(endpointEnvVar, getEndpointFromEnv(), 'select a server')
|
|
212
275
|
if (args['server']) {
|
|
213
276
|
config.set('server', args['server'])
|
|
214
277
|
config.delete('current_workspace_id')
|
|
@@ -236,14 +299,7 @@ async function cli(args: ParsedArgs) {
|
|
|
236
299
|
commandParams['accepted_providers'].split(',')
|
|
237
300
|
}
|
|
238
301
|
|
|
239
|
-
|
|
240
|
-
const lastCommandPath = selectedCommand.slice(-1)[0]
|
|
241
|
-
if (lastCommandPath === '[Back]') {
|
|
242
|
-
return await cli({
|
|
243
|
-
...args,
|
|
244
|
-
_: [],
|
|
245
|
-
})
|
|
246
|
-
}
|
|
302
|
+
const apiPath = `/${selectedCommand.join('/').replace(/-/g, '_')}`
|
|
247
303
|
|
|
248
304
|
const params = await interactForCommandParams(
|
|
249
305
|
{ command: selectedCommand, params: commandParams },
|
|
@@ -259,8 +315,6 @@ async function cli(args: ParsedArgs) {
|
|
|
259
315
|
})
|
|
260
316
|
}
|
|
261
317
|
|
|
262
|
-
const apiPath = `/${selectedCommand.join('/').replace(/-/g, '_')}`
|
|
263
|
-
|
|
264
318
|
if (apiPath.includes('/events/list') && params.between) {
|
|
265
319
|
delete params.since
|
|
266
320
|
}
|
|
@@ -286,11 +340,67 @@ async function cli(args: ParsedArgs) {
|
|
|
286
340
|
const toCommandWord = (arg: string): string =>
|
|
287
341
|
arg.toLowerCase().replace(/_/g, '-')
|
|
288
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Report any argument the command does not accept, rather than acting on it.
|
|
345
|
+
* An unrecognized argument is a mistake: forwarded to the API it would fail
|
|
346
|
+
* somewhere less obvious or be quietly ignored, and on a command the CLI
|
|
347
|
+
* handles itself it would go nowhere at all.
|
|
348
|
+
*
|
|
349
|
+
* Only arguments are checked. Params read from stdin are passed through as
|
|
350
|
+
* given, so a caller may send whatever the API itself accepts.
|
|
351
|
+
*
|
|
352
|
+
* `ctx` is only needed to look up an endpoint's parameters, so commands the
|
|
353
|
+
* CLI declares itself can be checked before any blueprint is loaded.
|
|
354
|
+
*/
|
|
355
|
+
const assertKnownArgs = (
|
|
356
|
+
argParams: Record<string, any>,
|
|
357
|
+
command: string[],
|
|
358
|
+
ctx?: ContextHelpers,
|
|
359
|
+
): void => {
|
|
360
|
+
const local = findLocalCommand(command)
|
|
361
|
+
|
|
362
|
+
let accepted: Set<string>
|
|
363
|
+
if (local != null) {
|
|
364
|
+
accepted = new Set(
|
|
365
|
+
local.flags.flatMap(({ long }) =>
|
|
366
|
+
long == null ? [] : [toParameterName(long)],
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
} else if (ctx != null) {
|
|
370
|
+
accepted = new Set(
|
|
371
|
+
getCommandBlueprintDef(command, ctx).request.parameters.map(
|
|
372
|
+
({ name }) => name,
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
} else {
|
|
376
|
+
throw new Error(`No definition for command seam ${command.join(' ')}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const unknown = Object.keys(argParams).filter((key) => !accepted.has(key))
|
|
380
|
+
if (unknown.length === 0) return
|
|
381
|
+
|
|
382
|
+
// Name an endpoint command by its path, as missing params are named, and a
|
|
383
|
+
// command the CLI handles itself by the words that run it.
|
|
384
|
+
const target =
|
|
385
|
+
local == null
|
|
386
|
+
? `/${command.join('/').replace(/-/g, '_')}`
|
|
387
|
+
: command.join(' ')
|
|
388
|
+
|
|
389
|
+
throw new UsageError(
|
|
390
|
+
`Unknown ${
|
|
391
|
+
unknown.length === 1 ? 'parameter' : 'parameters'
|
|
392
|
+
} for ${target}: ${unknown.map(toGivenArgName).join(' ')}`,
|
|
393
|
+
{
|
|
394
|
+
hint: `Run 'seam ${command.join(' ')} --help' to see what it accepts.`,
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
289
399
|
const handleConnectWebviewResponse = async (
|
|
290
|
-
|
|
400
|
+
connectWebview: any,
|
|
291
401
|
interactivity: Interactivity,
|
|
292
402
|
) => {
|
|
293
|
-
const url =
|
|
403
|
+
const url = connectWebview.url
|
|
294
404
|
|
|
295
405
|
if (
|
|
296
406
|
interactivity !== 'non-interactive' &&
|
|
@@ -337,7 +447,13 @@ run(process.argv.slice(2)).catch((e: unknown) => {
|
|
|
337
447
|
const output = getOutput()
|
|
338
448
|
process.exitCode = 1
|
|
339
449
|
|
|
340
|
-
if (e instanceof
|
|
450
|
+
if (e instanceof UsageError) {
|
|
451
|
+
output.error(chalk.red(e.message))
|
|
452
|
+
if (e.hint !== '') output.error(e.hint)
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (e instanceof NonInteractiveError || e instanceof EnvVarOverrideError) {
|
|
341
457
|
output.error(chalk.red(e.message))
|
|
342
458
|
return
|
|
343
459
|
}
|
package/src/lib/command-spec.ts
CHANGED
|
@@ -151,6 +151,18 @@ export const findGroup = (
|
|
|
151
151
|
): CommandGroup | undefined =>
|
|
152
152
|
spec.groups.find((group) => isSamePath(group.path, path))
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The definition of a command the CLI handles itself, or `undefined` when the
|
|
156
|
+
* path is an endpoint or no command at all.
|
|
157
|
+
*
|
|
158
|
+
* Unlike {@link findCommand} this needs no blueprint, since these commands are
|
|
159
|
+
* declared by the CLI rather than derived from the API definitions.
|
|
160
|
+
*/
|
|
161
|
+
export const findLocalCommand = (
|
|
162
|
+
path: string[],
|
|
163
|
+
): CommandDefinition | undefined =>
|
|
164
|
+
localCommands.find((command) => isSamePath(command.path, path))
|
|
165
|
+
|
|
154
166
|
const isSamePath = (a: string[], b: string[]): boolean =>
|
|
155
167
|
a.length === b.length && a.every((word, index) => word === b[index])
|
|
156
168
|
|
package/src/lib/env.d.ts
ADDED
package/src/lib/env.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials and the server may be given in the environment.
|
|
3
|
+
*
|
|
4
|
+
* Each variable overrides the corresponding stored value for as long as it
|
|
5
|
+
* is set, so any of them may be used per command or per shell. Commands that
|
|
6
|
+
* would store an overridden value fail instead: see {@link assertEnvVarUnset}.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Overrides the token stored by `seam login`. */
|
|
10
|
+
export const tokenEnvVar = 'SEAM_CLI_TOKEN'
|
|
11
|
+
|
|
12
|
+
/** Overrides the workspace stored by `seam select workspace`. */
|
|
13
|
+
export const workspaceIdEnvVar = 'SEAM_CLI_WORKSPACE_ID'
|
|
14
|
+
|
|
15
|
+
/** Overrides the server stored by `seam select server`. */
|
|
16
|
+
export const endpointEnvVar = 'SEAM_CLI_ENDPOINT'
|
|
17
|
+
|
|
18
|
+
/** Every variable read here is declared on `ProcessEnv` in `env.d.ts`. */
|
|
19
|
+
type SeamCliEnvVar =
|
|
20
|
+
typeof endpointEnvVar | typeof tokenEnvVar | typeof workspaceIdEnvVar
|
|
21
|
+
|
|
22
|
+
export const getTokenFromEnv = (): string | null => readEnvVar(tokenEnvVar)
|
|
23
|
+
|
|
24
|
+
export const getWorkspaceIdFromEnv = (): string | null =>
|
|
25
|
+
readEnvVar(workspaceIdEnvVar)
|
|
26
|
+
|
|
27
|
+
export const getEndpointFromEnv = (): string | null =>
|
|
28
|
+
readEnvVar(endpointEnvVar)
|
|
29
|
+
|
|
30
|
+
/** Reported without a stack trace: the environment is at fault, not the CLI. */
|
|
31
|
+
export class EnvVarOverrideError extends Error {
|
|
32
|
+
override name = 'EnvVarOverrideError'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Refuse to store a value the environment overrides.
|
|
37
|
+
*
|
|
38
|
+
* Storing it would have no effect while the variable is set, so a command
|
|
39
|
+
* that appears to succeed would leave the CLI using something else.
|
|
40
|
+
*
|
|
41
|
+
* @param action What the command does, e.g., `log in`.
|
|
42
|
+
*/
|
|
43
|
+
export const assertEnvVarUnset = (
|
|
44
|
+
envVar: string,
|
|
45
|
+
envValue: string | null,
|
|
46
|
+
action: string,
|
|
47
|
+
): void => {
|
|
48
|
+
if (envValue == null) return
|
|
49
|
+
|
|
50
|
+
throw new EnvVarOverrideError(
|
|
51
|
+
`Cannot ${action} while ${envVar} is set: it overrides what would be stored. Unset ${envVar} to ${action}.`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const readEnvVar = (envVar: SeamCliEnvVar): string | null => {
|
|
56
|
+
const value = process.env[envVar]
|
|
57
|
+
|
|
58
|
+
if (value == null) return null
|
|
59
|
+
|
|
60
|
+
const trimmedValue = value.trim()
|
|
61
|
+
|
|
62
|
+
return trimmedValue === '' ? null : trimmedValue
|
|
63
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getConfigStore } from './config/index.js'
|
|
2
|
+
import { getTokenFromEnv, getWorkspaceIdFromEnv } from './env.js'
|
|
3
|
+
import { getServer } from './get-server.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The token used to authenticate requests.
|
|
7
|
+
*
|
|
8
|
+
* `SEAM_CLI_TOKEN` wins over the token stored by `seam login`,
|
|
9
|
+
* so a token may be given per command or per shell without logging in.
|
|
10
|
+
*/
|
|
11
|
+
export const getToken = (): string | null => {
|
|
12
|
+
const token = getTokenFromEnv()
|
|
13
|
+
if (token != null) return token
|
|
14
|
+
|
|
15
|
+
return readString(getConfigStore().get(`${getServer()}.pat`))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The workspace requests are made against.
|
|
20
|
+
*
|
|
21
|
+
* `SEAM_CLI_WORKSPACE_ID` wins over the workspace stored by
|
|
22
|
+
* `seam select workspace`. Returns `null` when neither is set: a token
|
|
23
|
+
* scoped to a single workspace does not need one.
|
|
24
|
+
*/
|
|
25
|
+
export const getWorkspaceId = (): string | null => {
|
|
26
|
+
const workspaceId = getWorkspaceIdFromEnv()
|
|
27
|
+
if (workspaceId != null) return workspaceId
|
|
28
|
+
|
|
29
|
+
return readString(getConfigStore().get('current_workspace_id'))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const readString = (value: unknown): string | null => {
|
|
33
|
+
if (typeof value !== 'string') return null
|
|
34
|
+
|
|
35
|
+
const trimmedValue = value.trim()
|
|
36
|
+
|
|
37
|
+
return trimmedValue === '' ? null : trimmedValue
|
|
38
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getWorkspaceId } from './get-credentials.js'
|
|
2
2
|
import { interactForWorkspaceId } from './interact-for-workspace-id.js'
|
|
3
3
|
|
|
4
4
|
export const getCurrentWorkspaceId = async (): Promise<string> => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const currentWorkspaceId = configStore.get('current_workspace_id')
|
|
8
|
-
if (typeof currentWorkspaceId === 'string') return currentWorkspaceId
|
|
5
|
+
const currentWorkspaceId = getWorkspaceId()
|
|
6
|
+
if (currentWorkspaceId != null) return currentWorkspaceId
|
|
9
7
|
|
|
10
8
|
return await interactForWorkspaceId()
|
|
11
9
|
}
|
package/src/lib/get-seam.ts
CHANGED
|
@@ -5,24 +5,29 @@ import {
|
|
|
5
5
|
SeamHttpWithoutWorkspace,
|
|
6
6
|
} from '@seamapi/http/connect'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { tokenEnvVar, workspaceIdEnvVar } from './env.js'
|
|
9
|
+
import { getToken, getWorkspaceId } from './get-credentials.js'
|
|
9
10
|
import { getServer } from './get-server.js'
|
|
10
11
|
|
|
11
12
|
export const getSeam = async (): Promise<SeamHttp> => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const token = config.get(`${getServer()}.pat`) as string
|
|
15
|
-
|
|
16
|
-
const workspaceId = config.get('current_workspace_id') as string
|
|
13
|
+
const token = getRequiredToken()
|
|
17
14
|
|
|
18
15
|
const options = { endpoint: getServer() }
|
|
19
16
|
|
|
20
17
|
if (isPersonalAccessToken(token)) {
|
|
21
|
-
return SeamHttp.fromPersonalAccessToken(
|
|
18
|
+
return SeamHttp.fromPersonalAccessToken(
|
|
19
|
+
token,
|
|
20
|
+
getRequiredWorkspaceId(),
|
|
21
|
+
options,
|
|
22
|
+
)
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
if (isConsoleSessionToken(token)) {
|
|
25
|
-
return SeamHttp.fromConsoleSessionToken(
|
|
26
|
+
return SeamHttp.fromConsoleSessionToken(
|
|
27
|
+
token,
|
|
28
|
+
getRequiredWorkspaceId(),
|
|
29
|
+
options,
|
|
30
|
+
)
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
return SeamHttp.fromApiKey(token, options)
|
|
@@ -31,13 +36,36 @@ export const getSeam = async (): Promise<SeamHttp> => {
|
|
|
31
36
|
export const getSeamMultiWorkspace = async (): Promise<
|
|
32
37
|
SeamHttpWithoutWorkspace | SeamHttp
|
|
33
38
|
> => {
|
|
34
|
-
const
|
|
35
|
-
const token = config.get(`${getServer()}.pat`) as string
|
|
39
|
+
const token = getRequiredToken()
|
|
36
40
|
const options = { endpoint: getServer() }
|
|
37
41
|
|
|
38
42
|
if (isPersonalAccessToken(token)) {
|
|
39
43
|
return SeamHttpWithoutWorkspace.fromPersonalAccessToken(token, options)
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
return getSeam()
|
|
46
|
+
return await getSeam()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getRequiredToken = (): string => {
|
|
50
|
+
const token = getToken()
|
|
51
|
+
|
|
52
|
+
if (token == null) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Not logged in: run "seam login" or set the ${tokenEnvVar} environment variable`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return token
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getRequiredWorkspaceId = (): string => {
|
|
62
|
+
const workspaceId = getWorkspaceId()
|
|
63
|
+
|
|
64
|
+
if (workspaceId == null) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`No workspace selected: run "seam select workspace" or set the ${workspaceIdEnvVar} environment variable`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return workspaceId
|
|
43
71
|
}
|
package/src/lib/get-server.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { getConfigStore } from './config/index.js'
|
|
2
|
+
import { getEndpointFromEnv } from './env.js'
|
|
2
3
|
|
|
4
|
+
const defaultServer = 'https://connect.getseam.com'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Seam API server requests are made against.
|
|
8
|
+
*
|
|
9
|
+
* `SEAM_CLI_ENDPOINT` wins over the server stored by `seam select server`.
|
|
10
|
+
*/
|
|
3
11
|
export const getServer = (): string => {
|
|
12
|
+
const endpoint = getEndpointFromEnv()
|
|
13
|
+
if (endpoint != null) return endpoint
|
|
14
|
+
|
|
4
15
|
const config = getConfigStore()
|
|
5
16
|
|
|
6
17
|
const server = config.get('server')
|
|
7
18
|
|
|
8
|
-
return typeof server === 'string' ? server :
|
|
19
|
+
return typeof server === 'string' ? server : defaultServer
|
|
9
20
|
}
|
|
@@ -3,19 +3,21 @@ import { interactForDevice } from './interact-for-device.js'
|
|
|
3
3
|
import { interactForResource } from './interact-for-resource.js'
|
|
4
4
|
|
|
5
5
|
export const interactForAccessCode = async ({
|
|
6
|
-
|
|
6
|
+
// The key is a Seam API parameter name: callers pass the blueprint params
|
|
7
|
+
// bag through as-is, so only the binding may be camelCase.
|
|
8
|
+
device_id: deviceId,
|
|
7
9
|
}: {
|
|
8
10
|
device_id?: string
|
|
9
11
|
}) => {
|
|
10
12
|
const seam = await getSeam()
|
|
11
13
|
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
+
if (!deviceId) {
|
|
15
|
+
deviceId = await interactForDevice()
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
return interactForResource({
|
|
17
19
|
resourceName: 'access_code',
|
|
18
|
-
fetchResources: () => seam.accessCodes.list({ device_id }),
|
|
20
|
+
fetchResources: () => seam.accessCodes.list({ device_id: deviceId }),
|
|
19
21
|
toChoice: (accessCode) => ({
|
|
20
22
|
title: accessCode.name ?? '<No Name>',
|
|
21
23
|
value: accessCode.access_code_id,
|
|
@@ -5,13 +5,13 @@ import { interactForResource } from './interact-for-resource.js'
|
|
|
5
5
|
export const interactForAcsUser = async () => {
|
|
6
6
|
const seam = await getSeam()
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const acsSystemId = await interactForAcsSystem(
|
|
9
9
|
'What acs_system does the acs_user belong to?',
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
return interactForResource({
|
|
13
13
|
resourceName: 'ACS user',
|
|
14
|
-
fetchResources: () => seam.acs.users.list({ acs_system_id }),
|
|
14
|
+
fetchResources: () => seam.acs.users.list({ acs_system_id: acsSystemId }),
|
|
15
15
|
toChoice: (user) => ({
|
|
16
16
|
title: `${user.display_name} ${user.email_address}`,
|
|
17
17
|
value: user.acs_user_id,
|
|
@@ -6,11 +6,11 @@ import { prompt } from './util/prompt.js'
|
|
|
6
6
|
import { withLoading } from './util/with-loading.js'
|
|
7
7
|
|
|
8
8
|
export const interactForActionAttemptPoll = async (
|
|
9
|
-
|
|
9
|
+
actionAttempt: ActionAttemptsGetResponse['action_attempt'],
|
|
10
10
|
) => {
|
|
11
|
-
if (
|
|
12
|
-
const {
|
|
13
|
-
name: '
|
|
11
|
+
if (actionAttempt.status === 'pending') {
|
|
12
|
+
const { pollForActionAttempt } = await prompt({
|
|
13
|
+
name: 'pollForActionAttempt',
|
|
14
14
|
message: "Would you like to poll the action attempt until it's ready?",
|
|
15
15
|
type: 'toggle',
|
|
16
16
|
initial: true,
|
|
@@ -18,19 +18,19 @@ export const interactForActionAttemptPoll = async (
|
|
|
18
18
|
inactive: 'no',
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
if (
|
|
21
|
+
if (pollForActionAttempt) {
|
|
22
22
|
const seam = await getSeam()
|
|
23
|
-
const { action_attempt_id } =
|
|
23
|
+
const { action_attempt_id: actionAttemptId } = actionAttempt
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const updatedActionAttempt = await withLoading(
|
|
26
26
|
'Polling action attempt...',
|
|
27
27
|
() =>
|
|
28
28
|
seam.actionAttempts.get(
|
|
29
|
-
{ action_attempt_id },
|
|
29
|
+
{ action_attempt_id: actionAttemptId },
|
|
30
30
|
{ waitForActionAttempt: { pollingInterval: 240, timeout: 10_000 } },
|
|
31
31
|
),
|
|
32
32
|
)
|
|
33
|
-
getOutput().data({ action_attempt:
|
|
33
|
+
getOutput().data({ action_attempt: updatedActionAttempt })
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|