@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.
Files changed (68) hide show
  1. package/README.md +65 -1
  2. package/bin/cli.js +105 -33
  3. package/bin/cli.js.map +1 -1
  4. package/lib/command-spec.d.ts +8 -0
  5. package/lib/command-spec.js +8 -0
  6. package/lib/command-spec.js.map +1 -1
  7. package/lib/env.d.ts +29 -0
  8. package/lib/env.js +41 -0
  9. package/lib/env.js.map +1 -0
  10. package/lib/get-credentials.d.ts +15 -0
  11. package/lib/get-credentials.js +35 -0
  12. package/lib/get-credentials.js.map +1 -0
  13. package/lib/get-current-workspace-id.js +3 -4
  14. package/lib/get-current-workspace-id.js.map +1 -1
  15. package/lib/get-seam.js +21 -9
  16. package/lib/get-seam.js.map +1 -1
  17. package/lib/get-server.d.ts +5 -0
  18. package/lib/get-server.js +11 -1
  19. package/lib/get-server.js.map +1 -1
  20. package/lib/interact-for-access-code.d.ts +1 -1
  21. package/lib/interact-for-access-code.js +7 -4
  22. package/lib/interact-for-access-code.js.map +1 -1
  23. package/lib/interact-for-acs-user.js +2 -2
  24. package/lib/interact-for-acs-user.js.map +1 -1
  25. package/lib/interact-for-action-attempt-poll.d.ts +1 -1
  26. package/lib/interact-for-action-attempt-poll.js +8 -8
  27. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  28. package/lib/interact-for-blueprint-object.js +6 -8
  29. package/lib/interact-for-blueprint-object.js.map +1 -1
  30. package/lib/interact-for-custom-metadata.d.ts +1 -1
  31. package/lib/interact-for-custom-metadata.js +15 -15
  32. package/lib/interact-for-custom-metadata.js.map +1 -1
  33. package/lib/interact-for-login.js +2 -0
  34. package/lib/interact-for-login.js.map +1 -1
  35. package/lib/interact-for-server-selection.js +3 -0
  36. package/lib/interact-for-server-selection.js.map +1 -1
  37. package/lib/interact-for-use-remote-api-defs.js +4 -4
  38. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  39. package/lib/interact-for-workspace-id.js +2 -0
  40. package/lib/interact-for-workspace-id.js.map +1 -1
  41. package/lib/render-help.js +4 -0
  42. package/lib/render-help.js.map +1 -1
  43. package/lib/util/cli-args.d.ts +21 -0
  44. package/lib/util/cli-args.js +26 -1
  45. package/lib/util/cli-args.js.map +1 -1
  46. package/lib/version.d.ts +1 -1
  47. package/lib/version.js +1 -1
  48. package/package.json +1 -1
  49. package/src/bin/cli.ts +148 -32
  50. package/src/lib/command-spec.ts +12 -0
  51. package/src/lib/env.d.ts +12 -0
  52. package/src/lib/env.ts +63 -0
  53. package/src/lib/get-credentials.ts +38 -0
  54. package/src/lib/get-current-workspace-id.ts +3 -5
  55. package/src/lib/get-seam.ts +39 -11
  56. package/src/lib/get-server.ts +12 -1
  57. package/src/lib/interact-for-access-code.ts +6 -4
  58. package/src/lib/interact-for-acs-user.ts +2 -2
  59. package/src/lib/interact-for-action-attempt-poll.ts +9 -9
  60. package/src/lib/interact-for-blueprint-object.ts +6 -8
  61. package/src/lib/interact-for-custom-metadata.ts +18 -20
  62. package/src/lib/interact-for-login.ts +3 -0
  63. package/src/lib/interact-for-server-selection.ts +10 -0
  64. package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
  65. package/src/lib/interact-for-workspace-id.ts +12 -0
  66. package/src/lib/render-help.ts +4 -0
  67. package/src/lib/util/cli-args.ts +33 -1
  68. package/src/lib/version.ts +1 -1
package/lib/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Versions are replaced with generated values when the package is packed.
2
- const seamapiCliVersion = '0.13.1';
2
+ const seamapiCliVersion = '0.15.0';
3
3
  const seamapiBlueprintVersion = '1.2.0';
4
4
  export { seamapiBlueprintVersion };
5
5
  export default seamapiCliVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamapi/cli",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "A command-line interface (CLI) for interacting with the Seam API.",
5
5
  "type": "module",
6
6
  "main": "index.js",
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
- !config.get(`${getServer()}.pat`) &&
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
- args._ = args._.map(toCommandWord)
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(use_remote_api_defs ?? false, {
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
- for (const k in args) {
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
- // Hit 'back' on a top-level command path, so we start again
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
- connect_webview: any,
400
+ connectWebview: any,
291
401
  interactivity: Interactivity,
292
402
  ) => {
293
- const url = connect_webview.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 NonInteractiveError) {
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
  }
@@ -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
 
@@ -0,0 +1,12 @@
1
+ declare global {
2
+ namespace NodeJS {
3
+ interface ProcessEnv {
4
+ INSIDE_WEB_BROWSER?: string
5
+ SEAM_CLI_ENDPOINT?: string
6
+ SEAM_CLI_TOKEN?: string
7
+ SEAM_CLI_WORKSPACE_ID?: string
8
+ }
9
+ }
10
+ }
11
+
12
+ export {}
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 { getConfigStore } from './config/index.js'
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 configStore = getConfigStore()
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
  }
@@ -5,24 +5,29 @@ import {
5
5
  SeamHttpWithoutWorkspace,
6
6
  } from '@seamapi/http/connect'
7
7
 
8
- import { getConfigStore } from './config/index.js'
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 config = getConfigStore()
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(token, workspaceId, options)
18
+ return SeamHttp.fromPersonalAccessToken(
19
+ token,
20
+ getRequiredWorkspaceId(),
21
+ options,
22
+ )
22
23
  }
23
24
 
24
25
  if (isConsoleSessionToken(token)) {
25
- return SeamHttp.fromConsoleSessionToken(token, workspaceId, options)
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 config = getConfigStore()
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
  }
@@ -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 : 'https://connect.getseam.com'
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
- device_id,
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 (!device_id) {
13
- device_id = await interactForDevice()
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 acs_system_id = await interactForAcsSystem(
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
- action_attempt: ActionAttemptsGetResponse['action_attempt'],
9
+ actionAttempt: ActionAttemptsGetResponse['action_attempt'],
10
10
  ) => {
11
- if (action_attempt.status === 'pending') {
12
- const { poll_for_action_attempt } = await prompt({
13
- name: 'poll_for_action_attempt',
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 (poll_for_action_attempt) {
21
+ if (pollForActionAttempt) {
22
22
  const seam = await getSeam()
23
- const { action_attempt_id } = action_attempt
23
+ const { action_attempt_id: actionAttemptId } = actionAttempt
24
24
 
25
- const updated_action_attempt = await withLoading(
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: updated_action_attempt })
33
+ getOutput().data({ action_attempt: updatedActionAttempt })
34
34
  }
35
35
  }
36
36
  }