@seamapi/cli 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -4
- package/bin/cli.js +26 -6
- package/bin/cli.js.map +1 -1
- package/lib/args/parse.d.ts +9 -0
- package/lib/args/parse.js +42 -2
- package/lib/args/parse.js.map +1 -1
- package/lib/args/validate.d.ts +10 -0
- package/lib/args/validate.js +23 -0
- package/lib/args/validate.js.map +1 -1
- package/lib/auth/operations.d.ts +7 -9
- package/lib/auth/operations.js +13 -23
- package/lib/auth/operations.js.map +1 -1
- package/lib/commands/local/login.js +4 -12
- package/lib/commands/local/login.js.map +1 -1
- package/lib/commands/local/select-endpoint.js +10 -7
- package/lib/commands/local/select-endpoint.js.map +1 -1
- package/lib/commands/local/select-workspace.js +12 -4
- package/lib/commands/local/select-workspace.js.map +1 -1
- package/lib/commands/registry.d.ts +11 -0
- package/lib/commands/registry.js +11 -0
- package/lib/commands/registry.js.map +1 -1
- package/lib/commands/spec.d.ts +12 -0
- package/lib/commands/spec.js +16 -0
- package/lib/commands/spec.js.map +1 -1
- package/lib/context.d.ts +5 -4
- package/lib/context.js +23 -8
- package/lib/context.js.map +1 -1
- package/lib/overrides.d.ts +20 -0
- package/lib/overrides.js +23 -0
- package/lib/overrides.js.map +1 -0
- package/lib/render/help.js +20 -1
- package/lib/render/help.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 +31 -6
- package/src/lib/args/parse.ts +47 -2
- package/src/lib/args/validate.ts +37 -0
- package/src/lib/auth/operations.ts +13 -29
- package/src/lib/commands/local/login.ts +4 -15
- package/src/lib/commands/local/select-endpoint.ts +11 -7
- package/src/lib/commands/local/select-workspace.ts +13 -4
- package/src/lib/commands/registry.ts +20 -0
- package/src/lib/commands/spec.ts +31 -0
- package/src/lib/context.ts +30 -12
- package/src/lib/overrides.ts +32 -0
- package/src/lib/render/help.ts +21 -1
- package/src/lib/version.ts +1 -1
|
@@ -10,24 +10,13 @@ export const loginCommand: Command = {
|
|
|
10
10
|
kind: 'cli',
|
|
11
11
|
title: 'Log in to Seam.',
|
|
12
12
|
description:
|
|
13
|
-
'Prompts for a personal access token unless one is passed with --token.',
|
|
14
|
-
flags: [
|
|
15
|
-
stringFlag('endpoint', 'Seam API endpoint to log in to.'),
|
|
16
|
-
stringFlag('token', 'Personal access token to log in with.'),
|
|
17
|
-
stringFlag('workspace-id', 'Workspace to select after logging in.'),
|
|
18
|
-
],
|
|
13
|
+
'Prompts for a personal access token unless one is passed with --token. The token is stored for the selected endpoint, or for the one --endpoint names.',
|
|
14
|
+
flags: [stringFlag('token', 'Personal access token to log in with.')],
|
|
19
15
|
},
|
|
20
16
|
requiresAuth: false,
|
|
21
17
|
execute: async ({ args }, ctx) => {
|
|
22
|
-
if (args['token']
|
|
23
|
-
await login(
|
|
24
|
-
{
|
|
25
|
-
endpoint: args['endpoint'] ? args['endpoint'] : undefined,
|
|
26
|
-
token: args['token'] ? String(args['token']).trim() : undefined,
|
|
27
|
-
workspaceId: args['workspace_id'] ? args['workspace_id'] : undefined,
|
|
28
|
-
},
|
|
29
|
-
ctx.config,
|
|
30
|
-
)
|
|
18
|
+
if (args['token']) {
|
|
19
|
+
await login(String(args['token']).trim(), ctx.config)
|
|
31
20
|
return { kind: 'done' }
|
|
32
21
|
}
|
|
33
22
|
assertMutable(ctx.auth, 'token', 'log in')
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { assertMutable, selectEndpoint } from 'lib/auth/operations.js'
|
|
2
2
|
import type { Command } from 'lib/commands/registry.js'
|
|
3
|
-
import { stringFlag } from 'lib/commands/spec.js'
|
|
4
3
|
import { NonInteractiveError } from 'lib/errors.js'
|
|
5
4
|
import { interactForEndpointSelection } from 'lib/interactions/index.js'
|
|
6
5
|
|
|
@@ -9,19 +8,24 @@ export const selectEndpointCommand: Command = {
|
|
|
9
8
|
path: ['select', 'endpoint'],
|
|
10
9
|
kind: 'cli',
|
|
11
10
|
title: 'Select the Seam API endpoint.',
|
|
12
|
-
description:
|
|
13
|
-
|
|
11
|
+
description:
|
|
12
|
+
'Stores the endpoint every later command runs against. To use one for a single command instead, pass --endpoint to that command.',
|
|
13
|
+
flags: [],
|
|
14
|
+
positional: {
|
|
15
|
+
name: 'url',
|
|
16
|
+
description: 'Seam API endpoint to select.',
|
|
17
|
+
},
|
|
14
18
|
},
|
|
15
19
|
requiresAuth: false,
|
|
16
|
-
execute: async ({
|
|
20
|
+
execute: async ({ positional }, ctx) => {
|
|
17
21
|
assertMutable(ctx.auth, 'endpoint', 'select an endpoint')
|
|
18
|
-
if (
|
|
19
|
-
selectEndpoint(
|
|
22
|
+
if (positional != null) {
|
|
23
|
+
selectEndpoint(positional, ctx.config)
|
|
20
24
|
return { kind: 'done' }
|
|
21
25
|
}
|
|
22
26
|
if (ctx.interactivity === 'non-interactive') {
|
|
23
27
|
throw new NonInteractiveError(
|
|
24
|
-
'Missing required
|
|
28
|
+
'Missing required argument for select endpoint: <url>',
|
|
25
29
|
)
|
|
26
30
|
}
|
|
27
31
|
await interactForEndpointSelection()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertMutable } from 'lib/auth/operations.js'
|
|
1
|
+
import { assertMutable, selectWorkspace } from 'lib/auth/operations.js'
|
|
2
2
|
import type { Command } from 'lib/commands/registry.js'
|
|
3
3
|
import { NonInteractiveError } from 'lib/errors.js'
|
|
4
4
|
import { interactForWorkspaceId } from 'lib/interactions/index.js'
|
|
@@ -8,15 +8,24 @@ export const selectWorkspaceCommand: Command = {
|
|
|
8
8
|
path: ['select', 'workspace'],
|
|
9
9
|
kind: 'cli',
|
|
10
10
|
title: 'Select the current workspace.',
|
|
11
|
-
description:
|
|
11
|
+
description:
|
|
12
|
+
'Stores the workspace every later command runs against. To use one for a single command instead, pass --workspace-id to that command.',
|
|
12
13
|
flags: [],
|
|
14
|
+
positional: {
|
|
15
|
+
name: 'workspace-id',
|
|
16
|
+
description: 'Workspace to select.',
|
|
17
|
+
},
|
|
13
18
|
},
|
|
14
19
|
requiresAuth: true,
|
|
15
|
-
execute: async (
|
|
20
|
+
execute: async ({ positional }, ctx) => {
|
|
16
21
|
assertMutable(ctx.auth, 'workspaceId', 'select a workspace')
|
|
22
|
+
if (positional != null) {
|
|
23
|
+
selectWorkspace(positional, ctx.config)
|
|
24
|
+
return { kind: 'done' }
|
|
25
|
+
}
|
|
17
26
|
if (ctx.interactivity === 'non-interactive') {
|
|
18
27
|
throw new NonInteractiveError(
|
|
19
|
-
'
|
|
28
|
+
'Missing required argument for select workspace: <workspace-id>',
|
|
20
29
|
)
|
|
21
30
|
}
|
|
22
31
|
await interactForWorkspaceId()
|
|
@@ -38,6 +38,8 @@ export interface Command {
|
|
|
38
38
|
/** Everything a single run of a command was given. */
|
|
39
39
|
export interface Invocation {
|
|
40
40
|
path: string[]
|
|
41
|
+
/** The value written after the command path, when the command takes one. */
|
|
42
|
+
positional?: string | undefined
|
|
41
43
|
/** Params given as arguments, held to what the command accepts. */
|
|
42
44
|
argParams: Record<string, unknown>
|
|
43
45
|
/** Params piped in as JSON, passed through as given. */
|
|
@@ -93,6 +95,24 @@ export const localCommandDefinitions: CommandDefinition[] = localCommands
|
|
|
93
95
|
export const findLocalCommand = (path: string[]): Command | undefined =>
|
|
94
96
|
localCommands.find((command) => isSamePath(command.definition.path, path))
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The local command the given words invoke with a value after its path, e.g.,
|
|
100
|
+
* `select endpoint` for `select endpoint https://connect.getseam.com`, or
|
|
101
|
+
* `undefined` when the words are not a command taking one.
|
|
102
|
+
*
|
|
103
|
+
* Only commands declaring a positional match, so a stray word after any other
|
|
104
|
+
* command stays what it has always been: no command at all.
|
|
105
|
+
*/
|
|
106
|
+
export const findLocalCommandTakingPositional = (
|
|
107
|
+
words: string[],
|
|
108
|
+
): Command | undefined =>
|
|
109
|
+
localCommands.find(
|
|
110
|
+
({ definition }) =>
|
|
111
|
+
definition.positional != null &&
|
|
112
|
+
words.length === definition.path.length + 1 &&
|
|
113
|
+
isSamePath(definition.path, words.slice(0, -1)),
|
|
114
|
+
)
|
|
115
|
+
|
|
96
116
|
/** Parameter names a command accepts as arguments. */
|
|
97
117
|
export const acceptedParamsOf = (definition: CommandDefinition): Set<string> =>
|
|
98
118
|
new Set(
|
package/src/lib/commands/spec.ts
CHANGED
|
@@ -23,6 +23,17 @@ export interface CommandFlag {
|
|
|
23
23
|
*/
|
|
24
24
|
export type CommandKind = 'cli' | 'api'
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* A value written after the command rather than behind a flag, e.g., the URL
|
|
28
|
+
* in `seam select endpoint <url>`. At most one, always required: it is the
|
|
29
|
+
* one thing the command is about.
|
|
30
|
+
*/
|
|
31
|
+
export interface CommandPositional {
|
|
32
|
+
/** Name shown in the usage line, without the angle brackets. */
|
|
33
|
+
name: string
|
|
34
|
+
description: string
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export interface CommandDefinition {
|
|
27
38
|
path: string[]
|
|
28
39
|
kind: CommandKind
|
|
@@ -31,6 +42,8 @@ export interface CommandDefinition {
|
|
|
31
42
|
/** Longer prose about the command, empty when there is none to add. */
|
|
32
43
|
description: string
|
|
33
44
|
flags: CommandFlag[]
|
|
45
|
+
/** The value the command takes after its path, when it takes one. */
|
|
46
|
+
positional?: CommandPositional
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export interface Subcommand {
|
|
@@ -56,6 +69,15 @@ export interface CommandSpec {
|
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
export const globalFlags: CommandFlag[] = [
|
|
72
|
+
{
|
|
73
|
+
long: 'endpoint',
|
|
74
|
+
short: null,
|
|
75
|
+
description:
|
|
76
|
+
'Seam API endpoint to run this one command against, instead of the selected one.',
|
|
77
|
+
values: [],
|
|
78
|
+
takesValue: true,
|
|
79
|
+
isRequired: false,
|
|
80
|
+
},
|
|
59
81
|
{
|
|
60
82
|
long: 'help',
|
|
61
83
|
short: 'h',
|
|
@@ -115,6 +137,15 @@ export const globalFlags: CommandFlag[] = [
|
|
|
115
137
|
takesValue: false,
|
|
116
138
|
isRequired: false,
|
|
117
139
|
},
|
|
140
|
+
{
|
|
141
|
+
long: 'workspace-id',
|
|
142
|
+
short: null,
|
|
143
|
+
description:
|
|
144
|
+
'Workspace to run this one command against, instead of the selected one.',
|
|
145
|
+
values: [],
|
|
146
|
+
takesValue: true,
|
|
147
|
+
isRequired: false,
|
|
148
|
+
},
|
|
118
149
|
]
|
|
119
150
|
|
|
120
151
|
export const flagTokens = (flag: CommandFlag): string[] => {
|
package/src/lib/context.ts
CHANGED
|
@@ -8,18 +8,20 @@ import {
|
|
|
8
8
|
} from './env.js'
|
|
9
9
|
import type { SeamApi } from './http/api.js'
|
|
10
10
|
import type { Output } from './output/output.js'
|
|
11
|
+
import { getAuthOverrides } from './overrides.js'
|
|
11
12
|
|
|
12
13
|
export const defaultEndpoint = 'https://connect.getseam.com'
|
|
13
14
|
|
|
14
15
|
/** Where a resolved value came from, e.g., to refuse writes the env shadows. */
|
|
15
|
-
export type ValueSource = 'env' | 'config' | 'default'
|
|
16
|
+
export type ValueSource = 'flag' | 'env' | 'config' | 'default'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The endpoint, token, and workspace requests are made with.
|
|
19
20
|
*
|
|
20
|
-
* Resolved in one place so the precedence rule exists once:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Resolved in one place so the precedence rule exists once: a flag given for
|
|
22
|
+
* the one command wins over an environment variable, which wins over the
|
|
23
|
+
* stored value, and the endpoint falls back to Seam. The source tags say
|
|
24
|
+
* where each value came from.
|
|
23
25
|
*/
|
|
24
26
|
export interface AuthContext {
|
|
25
27
|
endpoint: string
|
|
@@ -33,35 +35,51 @@ export interface AuthContext {
|
|
|
33
35
|
export const resolveAuth = (
|
|
34
36
|
config: ConfigStore = getConfigStore(),
|
|
35
37
|
): AuthContext => {
|
|
38
|
+
const { endpoint: flagEndpoint, workspaceId: flagWorkspaceId } =
|
|
39
|
+
getAuthOverrides()
|
|
40
|
+
|
|
36
41
|
const envEndpoint = getEndpointFromEnv()
|
|
37
42
|
// Configs written before the endpoint was called one still hold it under
|
|
38
43
|
// `server`, so fall back to that key rather than silently resetting them.
|
|
39
44
|
const storedEndpoint = config.get('endpoint') ?? config.get('server')
|
|
40
45
|
const endpoint =
|
|
41
|
-
|
|
46
|
+
flagEndpoint ??
|
|
47
|
+
envEndpoint ??
|
|
48
|
+
(typeof storedEndpoint === 'string' ? storedEndpoint : null)
|
|
42
49
|
|
|
43
50
|
const envToken = getTokenFromEnv()
|
|
51
|
+
// The token is stored per endpoint, so an overridden endpoint is read with
|
|
52
|
+
// the token belonging to it rather than the one it replaced.
|
|
44
53
|
const storedToken = readString(
|
|
45
54
|
config.get(`${endpoint ?? defaultEndpoint}.pat`),
|
|
46
55
|
)
|
|
47
56
|
|
|
48
57
|
const envWorkspaceId = getWorkspaceIdFromEnv()
|
|
49
58
|
const storedWorkspaceId = readString(config.get('current_workspace_id'))
|
|
59
|
+
const workspaceId = flagWorkspaceId ?? envWorkspaceId ?? storedWorkspaceId
|
|
50
60
|
|
|
51
61
|
return {
|
|
52
62
|
endpoint: endpoint ?? defaultEndpoint,
|
|
53
63
|
endpointSource:
|
|
54
|
-
|
|
64
|
+
flagEndpoint != null
|
|
65
|
+
? 'flag'
|
|
66
|
+
: envEndpoint != null
|
|
67
|
+
? 'env'
|
|
68
|
+
: endpoint != null
|
|
69
|
+
? 'config'
|
|
70
|
+
: 'default',
|
|
55
71
|
token: envToken ?? storedToken,
|
|
56
72
|
tokenSource:
|
|
57
73
|
envToken != null ? 'env' : storedToken != null ? 'config' : null,
|
|
58
|
-
workspaceId
|
|
74
|
+
workspaceId,
|
|
59
75
|
workspaceIdSource:
|
|
60
|
-
|
|
61
|
-
? '
|
|
62
|
-
:
|
|
63
|
-
? '
|
|
64
|
-
: null
|
|
76
|
+
flagWorkspaceId != null
|
|
77
|
+
? 'flag'
|
|
78
|
+
: envWorkspaceId != null
|
|
79
|
+
? 'env'
|
|
80
|
+
: storedWorkspaceId != null
|
|
81
|
+
? 'config'
|
|
82
|
+
: null,
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint and workspace may be overridden for a single command.
|
|
3
|
+
*
|
|
4
|
+
* `--endpoint` and `--workspace-id` change what one invocation resolves to
|
|
5
|
+
* and are never stored: only `seam select endpoint` and `seam select
|
|
6
|
+
* workspace` write those settings. They are held here rather than threaded
|
|
7
|
+
* through every call because auth resolves ambiently, exactly as the
|
|
8
|
+
* environment variables they shadow do (see `env.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Set once from the parsed arguments before anything resolves auth, and
|
|
11
|
+
* reset between tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface AuthOverrides {
|
|
15
|
+
endpoint: string | null
|
|
16
|
+
workspaceId: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const noOverrides: AuthOverrides = { endpoint: null, workspaceId: null }
|
|
20
|
+
|
|
21
|
+
let overrides: AuthOverrides = noOverrides
|
|
22
|
+
|
|
23
|
+
export const getAuthOverrides = (): AuthOverrides => overrides
|
|
24
|
+
|
|
25
|
+
export const setAuthOverrides = (next: AuthOverrides): void => {
|
|
26
|
+
overrides = next
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Drop the overrides, e.g., between tests sharing a process. */
|
|
30
|
+
export const resetAuthOverrides = (): void => {
|
|
31
|
+
overrides = noOverrides
|
|
32
|
+
}
|
package/src/lib/render/help.ts
CHANGED
|
@@ -131,6 +131,7 @@ const commandSections = (
|
|
|
131
131
|
): Section[] => {
|
|
132
132
|
const name = ['seam', ...command.path].join(' ')
|
|
133
133
|
const hasFlags = command.flags.length > 0
|
|
134
|
+
const { positional } = command
|
|
134
135
|
|
|
135
136
|
return [
|
|
136
137
|
{
|
|
@@ -139,7 +140,26 @@ const commandSections = (
|
|
|
139
140
|
(line) => line !== '',
|
|
140
141
|
),
|
|
141
142
|
},
|
|
142
|
-
{
|
|
143
|
+
{
|
|
144
|
+
header: 'Usage',
|
|
145
|
+
content:
|
|
146
|
+
positional == null
|
|
147
|
+
? `${name} [options]`
|
|
148
|
+
: `${name} <${positional.name}> [options]`,
|
|
149
|
+
},
|
|
150
|
+
...(positional == null
|
|
151
|
+
? []
|
|
152
|
+
: [
|
|
153
|
+
{
|
|
154
|
+
header: 'Arguments',
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
name: `{underline <${positional.name}>}`,
|
|
158
|
+
summary: positional.description,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
]),
|
|
143
163
|
// The command's own parameters are what the request is made of, so keep
|
|
144
164
|
// them apart from the options every seam command takes.
|
|
145
165
|
...(hasFlags ? [optionSection(command.flags, 'Parameters')] : []),
|
package/src/lib/version.ts
CHANGED