@seamapi/cli 0.21.0 → 0.23.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 +29 -9
- 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 +13 -15
- package/lib/auth/operations.js +25 -48
- package/lib/auth/operations.js.map +1 -1
- package/lib/blueprint/source-npm.js +2 -2
- package/lib/blueprint/source-npm.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/local/wizard.d.ts +7 -0
- package/lib/commands/local/wizard.js +31 -0
- package/lib/commands/local/wizard.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/config/cli-config.d.ts +19 -0
- package/lib/config/cli-config.js +51 -0
- package/lib/config/cli-config.js.map +1 -0
- package/lib/config/config-store.d.ts +8 -5
- package/lib/config/config-store.js +15 -13
- package/lib/config/config-store.js.map +1 -1
- package/lib/config/index.d.ts +3 -2
- package/lib/config/index.js +3 -2
- package/lib/config/index.js.map +1 -1
- package/lib/config/memory-config-store.d.ts +6 -0
- package/lib/config/memory-config-store.js +6 -0
- package/lib/config/memory-config-store.js.map +1 -1
- package/lib/context.d.ts +8 -7
- package/lib/context.js +26 -21
- package/lib/context.js.map +1 -1
- package/lib/interactions/endpoint-selection.js +2 -2
- package/lib/interactions/endpoint-selection.js.map +1 -1
- package/lib/interactions/login.js +2 -2
- package/lib/interactions/login.js.map +1 -1
- package/lib/interactions/workspace-id.js +2 -2
- package/lib/interactions/workspace-id.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 +2 -2
- package/src/bin/cli.ts +34 -9
- package/src/lib/args/parse.ts +47 -2
- package/src/lib/args/validate.ts +37 -0
- package/src/lib/auth/operations.ts +26 -56
- package/src/lib/blueprint/source-npm.ts +2 -3
- 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/local/wizard.ts +48 -0
- package/src/lib/commands/registry.ts +20 -0
- package/src/lib/commands/spec.ts +31 -0
- package/src/lib/config/cli-config.ts +83 -0
- package/src/lib/config/config-store.ts +16 -13
- package/src/lib/config/index.ts +9 -4
- package/src/lib/config/memory-config-store.ts +9 -0
- package/src/lib/context.ts +34 -33
- package/src/lib/interactions/endpoint-selection.ts +2 -2
- package/src/lib/interactions/login.ts +2 -2
- package/src/lib/interactions/workspace-id.ts +2 -2
- package/src/lib/overrides.ts +32 -0
- package/src/lib/render/help.ts +21 -1
- package/src/lib/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CliConfig, getConfig } from 'lib/config/index.js'
|
|
2
2
|
import { type AuthContext, resolveAuth } from 'lib/context.js'
|
|
3
3
|
import {
|
|
4
4
|
assertEnvVarUnset,
|
|
@@ -44,70 +44,51 @@ export const assertMutable = (
|
|
|
44
44
|
assertEnvVarUnset(envVar, value, action)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export interface LoginOptions {
|
|
48
|
-
endpoint?: string | undefined
|
|
49
|
-
token?: string | undefined
|
|
50
|
-
workspaceId?: string | undefined
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
/**
|
|
54
|
-
* Store
|
|
48
|
+
* Store a token, validating it first.
|
|
55
49
|
*
|
|
56
|
-
* The token is stored under the endpoint it will be used with,
|
|
57
|
-
* endpoint
|
|
50
|
+
* The token is stored under the endpoint it will be used with, which
|
|
51
|
+
* `--endpoint` or the environment may have pointed elsewhere for this one
|
|
52
|
+
* command: logging in to another endpoint stores a token for it without
|
|
53
|
+
* selecting it. The workspace is only what the token is validated against,
|
|
54
|
+
* as a Personal Access Token is meaningless without one.
|
|
58
55
|
*
|
|
59
56
|
* Validation reaches the network, so a test may inject its own `validate`.
|
|
60
57
|
*/
|
|
61
58
|
export const login = async (
|
|
62
|
-
|
|
63
|
-
config:
|
|
59
|
+
token: string,
|
|
60
|
+
config: CliConfig = getConfig(),
|
|
64
61
|
validate: typeof validateToken = validateToken,
|
|
65
62
|
): Promise<void> => {
|
|
66
|
-
|
|
63
|
+
const auth = resolveAuth(config)
|
|
67
64
|
|
|
68
65
|
// Nothing is stored while the environment overrides it, so refuse before
|
|
69
|
-
//
|
|
66
|
+
// validating rather than after reaching the network.
|
|
70
67
|
assertMutable(auth, 'token', 'log in')
|
|
71
|
-
if (endpoint != null) assertMutable(auth, 'endpoint', 'select an endpoint')
|
|
72
|
-
if (workspaceId != null) {
|
|
73
|
-
assertMutable(auth, 'workspaceId', 'select a workspace')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (endpoint != null) {
|
|
77
|
-
storeEndpoint(endpoint, config)
|
|
78
|
-
auth = resolveAuth(config)
|
|
79
|
-
}
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
await validate(token, workspaceId)
|
|
83
|
-
config.set(`${auth.endpoint}.pat`, token)
|
|
84
|
-
config.delete('current_workspace_id')
|
|
85
|
-
}
|
|
69
|
+
await validate(token, auth.workspaceId ?? undefined)
|
|
86
70
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
config.setToken(auth.endpoint, token)
|
|
72
|
+
// The selection belongs to whoever was logged in before.
|
|
73
|
+
config.unsetWorkspace()
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
/** Store the token for the current endpoint, e.g., one just prompted for. */
|
|
93
77
|
export const storeToken = (
|
|
94
78
|
token: string,
|
|
95
|
-
config:
|
|
79
|
+
config: CliConfig = getConfig(),
|
|
96
80
|
): void => {
|
|
97
81
|
const auth = resolveAuth(config)
|
|
98
82
|
assertMutable(auth, 'token', 'log in')
|
|
99
|
-
config.
|
|
83
|
+
config.setToken(auth.endpoint, token)
|
|
100
84
|
}
|
|
101
85
|
|
|
102
86
|
/** Remove the stored token and workspace selection. */
|
|
103
|
-
export const logout = (config:
|
|
87
|
+
export const logout = (config: CliConfig = getConfig()): void => {
|
|
104
88
|
const auth = resolveAuth(config)
|
|
105
89
|
assertMutable(auth, 'token', 'log out')
|
|
106
|
-
config.
|
|
107
|
-
|
|
108
|
-
// un-namespaced token, so drop that too.
|
|
109
|
-
config.delete('pat')
|
|
110
|
-
config.delete('current_workspace_id')
|
|
90
|
+
config.unsetToken(auth.endpoint)
|
|
91
|
+
config.unsetWorkspace()
|
|
111
92
|
}
|
|
112
93
|
|
|
113
94
|
/**
|
|
@@ -117,36 +98,25 @@ export const logout = (config: ConfigStore = getConfigStore()): void => {
|
|
|
117
98
|
*/
|
|
118
99
|
export const selectEndpoint = (
|
|
119
100
|
endpoint: string,
|
|
120
|
-
config:
|
|
101
|
+
config: CliConfig = getConfig(),
|
|
121
102
|
): void => {
|
|
122
103
|
assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint')
|
|
123
|
-
|
|
104
|
+
config.setEndpoint(endpoint)
|
|
124
105
|
}
|
|
125
106
|
|
|
126
107
|
/** Store the workspace requests are made against. */
|
|
127
108
|
export const selectWorkspace = (
|
|
128
109
|
workspaceId: string,
|
|
129
|
-
config:
|
|
110
|
+
config: CliConfig = getConfig(),
|
|
130
111
|
): void => {
|
|
131
112
|
assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace')
|
|
132
|
-
config.
|
|
113
|
+
config.setWorkspace(workspaceId)
|
|
133
114
|
}
|
|
134
115
|
|
|
135
116
|
/** Store whether API definitions come from the endpoint instead of npm. */
|
|
136
117
|
export const setUseRemoteApiDefs = (
|
|
137
118
|
useRemoteApiDefs: boolean,
|
|
138
|
-
config:
|
|
119
|
+
config: CliConfig = getConfig(),
|
|
139
120
|
): void => {
|
|
140
|
-
config.
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Write the endpoint, dropping what belonged to the previous one: the
|
|
145
|
-
* workspace selection, and any value left under the legacy `server` key that
|
|
146
|
-
* {@link resolveAuth} would otherwise still fall back to.
|
|
147
|
-
*/
|
|
148
|
-
const storeEndpoint = (endpoint: string, config: ConfigStore): void => {
|
|
149
|
-
config.set('endpoint', endpoint)
|
|
150
|
-
config.delete('server')
|
|
151
|
-
config.delete('current_workspace_id')
|
|
121
|
+
config.setUseRemoteApiDefs(useRemoteApiDefs)
|
|
152
122
|
}
|
|
@@ -3,9 +3,9 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
import type { Blueprint, TypesModuleInput } from '@seamapi/blueprint'
|
|
6
|
-
import envPaths from 'env-paths'
|
|
7
6
|
import { extract } from 'tar'
|
|
8
7
|
|
|
8
|
+
import { rootPaths } from 'lib/config/index.js'
|
|
9
9
|
import { withLoading } from 'lib/output/with-loading.js'
|
|
10
10
|
|
|
11
11
|
import {
|
|
@@ -38,8 +38,7 @@ export const getBlueprint = async (
|
|
|
38
38
|
options: GetBlueprintOptions = {},
|
|
39
39
|
): Promise<Blueprint> => {
|
|
40
40
|
const update = options.update ?? false
|
|
41
|
-
const cacheDirectory =
|
|
42
|
-
options.cacheDirectory ?? envPaths('seam', { suffix: '' }).cache
|
|
41
|
+
const cacheDirectory = options.cacheDirectory ?? rootPaths.cache
|
|
43
42
|
const cacheFile = getCacheFile(cacheDirectory)
|
|
44
43
|
const blueprintVersion = await getBlueprintVersion()
|
|
45
44
|
|
|
@@ -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()
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { isApiKey } from '@seamapi/http/connect'
|
|
4
|
+
import type { StorageAdapter, WizardAdapter, WizardAuth } from '@seamapi/wizard'
|
|
5
|
+
import Configstore from 'configstore'
|
|
6
|
+
|
|
1
7
|
import type { Command } from 'lib/commands/registry.js'
|
|
8
|
+
import { type CliConfig, getConfig, rootPaths } from 'lib/config/index.js'
|
|
9
|
+
import { resolveAuth } from 'lib/context.js'
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Run the Seam setup wizard.
|
|
@@ -11,6 +19,7 @@ export const runWizard = async (argv: string[]): Promise<void> => {
|
|
|
11
19
|
await wizard({
|
|
12
20
|
argv,
|
|
13
21
|
commandName: 'seam wizard',
|
|
22
|
+
adapter: createWizardAdapter(),
|
|
14
23
|
})
|
|
15
24
|
}
|
|
16
25
|
|
|
@@ -29,3 +38,42 @@ export const wizardCommand: Command = {
|
|
|
29
38
|
return { kind: 'done' }
|
|
30
39
|
},
|
|
31
40
|
}
|
|
41
|
+
|
|
42
|
+
const wizardFileName = 'wizard.json'
|
|
43
|
+
|
|
44
|
+
export const createWizardAdapter = ({
|
|
45
|
+
cliConfig = getConfig(),
|
|
46
|
+
configPath = join(rootPaths.config, wizardFileName),
|
|
47
|
+
statePath = join(rootPaths.log, wizardFileName),
|
|
48
|
+
}: {
|
|
49
|
+
cliConfig?: CliConfig
|
|
50
|
+
configPath?: string
|
|
51
|
+
statePath?: string
|
|
52
|
+
} = {}): WizardAdapter => ({
|
|
53
|
+
getAuth: async () => toWizardAuth(cliConfig),
|
|
54
|
+
config: createStorage(configPath),
|
|
55
|
+
state: createStorage(statePath),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Only a workspace-scoped key may go in a project, so a personal access
|
|
59
|
+
// token is not handed over at all.
|
|
60
|
+
const toWizardAuth = (cliConfig: CliConfig): WizardAuth => {
|
|
61
|
+
const { endpoint, token, workspaceId } = resolveAuth(cliConfig)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
endpoint,
|
|
65
|
+
apiKey: token != null && isApiKey(token) ? token : null,
|
|
66
|
+
workspaceId,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const createStorage = (path: string): StorageAdapter => {
|
|
71
|
+
const store = new Configstore('seam-cli', undefined, { configPath: path })
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
get: async (key) => store.get(key),
|
|
75
|
+
set: async (key, value) => {
|
|
76
|
+
store.set(key, value)
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -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[] => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ConfigStore } from './config-store.js'
|
|
2
|
+
|
|
3
|
+
/** Tokens are stored per endpoint, e.g. `https://connect.getseam.com.pat`. */
|
|
4
|
+
const tokenKey = (endpoint: string): `${string}.pat` => `${endpoint}.pat`
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The CLI's configuration, in the terms the CLI thinks in. Keys are named
|
|
8
|
+
* here alone: a caller says what it wants stored, not where it goes.
|
|
9
|
+
*/
|
|
10
|
+
export interface CliConfig {
|
|
11
|
+
readonly path: string
|
|
12
|
+
getEndpoint: () => string | null
|
|
13
|
+
setEndpoint: (endpoint: string) => void
|
|
14
|
+
getToken: (endpoint: string) => string | null
|
|
15
|
+
setToken: (endpoint: string, token: string) => void
|
|
16
|
+
unsetToken: (endpoint: string) => void
|
|
17
|
+
getWorkspace: () => string | null
|
|
18
|
+
setWorkspace: (workspaceId: string) => void
|
|
19
|
+
unsetWorkspace: () => void
|
|
20
|
+
getUseRemoteApiDefs: () => boolean | null
|
|
21
|
+
setUseRemoteApiDefs: (useRemoteApiDefs: boolean) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createCliConfig = (store: ConfigStore): CliConfig => ({
|
|
25
|
+
get path() {
|
|
26
|
+
return store.path
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
getEndpoint: () =>
|
|
30
|
+
// `server` is what an older CLI called the endpoint.
|
|
31
|
+
readString(store.get('endpoint')) ?? readString(store.get('server')),
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Store the endpoint, dropping what belonged to the previous one: the
|
|
35
|
+
* workspace selection, and any value left under the legacy key that
|
|
36
|
+
* {@link CliConfig.getEndpoint} would otherwise still fall back to.
|
|
37
|
+
*/
|
|
38
|
+
setEndpoint: (endpoint) => {
|
|
39
|
+
store.set('endpoint', endpoint)
|
|
40
|
+
store.delete('server')
|
|
41
|
+
store.delete('current_workspace_id')
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getToken: (endpoint) => readString(store.get(tokenKey(endpoint))),
|
|
45
|
+
|
|
46
|
+
setToken: (endpoint, token) => {
|
|
47
|
+
store.set(tokenKey(endpoint), token)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
unsetToken: (endpoint) => {
|
|
51
|
+
store.delete(tokenKey(endpoint))
|
|
52
|
+
// Configs written before tokens were stored per endpoint may still hold
|
|
53
|
+
// an un-namespaced one.
|
|
54
|
+
store.delete('pat')
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
getWorkspace: () => readString(store.get('current_workspace_id')),
|
|
58
|
+
|
|
59
|
+
setWorkspace: (workspaceId) => {
|
|
60
|
+
store.set('current_workspace_id', workspaceId)
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
unsetWorkspace: () => {
|
|
64
|
+
store.delete('current_workspace_id')
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
getUseRemoteApiDefs: () => {
|
|
68
|
+
const useRemoteApiDefs = store.get('use_remote_api_defs')
|
|
69
|
+
return typeof useRemoteApiDefs === 'boolean' ? useRemoteApiDefs : null
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
setUseRemoteApiDefs: (useRemoteApiDefs) => {
|
|
73
|
+
store.set('use_remote_api_defs', useRemoteApiDefs)
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const readString = (value: unknown): string | null => {
|
|
78
|
+
if (typeof value !== 'string') return null
|
|
79
|
+
|
|
80
|
+
const trimmedValue = value.trim()
|
|
81
|
+
|
|
82
|
+
return trimmedValue === '' ? null : trimmedValue
|
|
83
|
+
}
|
|
@@ -3,12 +3,15 @@ import { join } from 'node:path'
|
|
|
3
3
|
import Configstore from 'configstore'
|
|
4
4
|
import envPaths from 'env-paths'
|
|
5
5
|
|
|
6
|
+
import { type CliConfig, createCliConfig } from './cli-config.js'
|
|
6
7
|
import { migrateConfigStore } from './migrate.js'
|
|
7
8
|
import { isStateKey, mergeConfig, splitConfig } from './values.js'
|
|
8
9
|
|
|
9
10
|
const configFileName = 'cli.json'
|
|
10
11
|
const legacyConfigStoreId = 'seam-cli'
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
/** Every directory the CLI keeps files in, and what it mounts keeps its own. */
|
|
14
|
+
export const rootPaths = envPaths('seam', { suffix: '' })
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* What a config store can do, regardless of where it keeps the values.
|
|
@@ -28,21 +31,21 @@ export interface ConfigStore {
|
|
|
28
31
|
clear: () => void
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
let
|
|
34
|
+
let config: CliConfig | null = null
|
|
32
35
|
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
+
export const getConfig = (): CliConfig => {
|
|
37
|
+
config ??= createCliConfig(createConfigStore())
|
|
38
|
+
return config
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
/** Replace the
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
+
/** Replace the config, e.g., with an in-memory one for a test. */
|
|
42
|
+
export const setConfig = (nextConfig: CliConfig): void => {
|
|
43
|
+
config = nextConfig
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
/** Drop the current
|
|
44
|
-
export const
|
|
45
|
-
|
|
46
|
+
/** Drop the current config so the next read builds the real one. */
|
|
47
|
+
export const resetConfig = (): void => {
|
|
48
|
+
config = null
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const createConfigStore = (): PersistentConfigStore => {
|
|
@@ -121,11 +124,11 @@ export class PersistentConfigStore implements ConfigStore {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
const getConfigPath = (): string => {
|
|
124
|
-
return join(
|
|
127
|
+
return join(rootPaths.config, configFileName)
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
const getStateConfigPath = (): string => {
|
|
128
|
-
return join(
|
|
131
|
+
return join(rootPaths.log, configFileName)
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
package/src/lib/config/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export { type CliConfig, createCliConfig } from './cli-config.js'
|
|
1
2
|
export {
|
|
2
3
|
type ConfigStore,
|
|
3
|
-
|
|
4
|
+
getConfig,
|
|
4
5
|
type PersistentConfigStore,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
resetConfig,
|
|
7
|
+
rootPaths,
|
|
8
|
+
setConfig,
|
|
7
9
|
} from './config-store.js'
|
|
8
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createMemoryConfig,
|
|
12
|
+
createMemoryConfigStore,
|
|
13
|
+
} from './memory-config-store.js'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CliConfig, createCliConfig } from './cli-config.js'
|
|
1
2
|
import type { ConfigStore } from './config-store.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -60,3 +61,11 @@ export class MemoryConfigStore implements ConfigStore {
|
|
|
60
61
|
export const createMemoryConfigStore = (
|
|
61
62
|
initialValues: Record<string, unknown> = {},
|
|
62
63
|
): ConfigStore => new MemoryConfigStore(initialValues)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A config held in memory, for tests. Values may be seeded by key, e.g., to
|
|
67
|
+
* stand for a config an older CLI wrote.
|
|
68
|
+
*/
|
|
69
|
+
export const createMemoryConfig = (
|
|
70
|
+
initialValues: Record<string, unknown> = {},
|
|
71
|
+
): CliConfig => createCliConfig(createMemoryConfigStore(initialValues))
|