@lanes-sh/link 0.5.3 → 0.6.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/instructions/agents/lanes-link-scout.md +1 -1
- package/instructions/skills/lanes-link/SKILL.md +33 -29
- package/package.json +2 -1
- package/src/cli/commands/connect/custom/index.ts +2 -0
- package/src/cli/commands/connect/custom/spec.ts +1 -0
- package/src/cli/commands/connect/declare.ts +76 -0
- package/src/cli/commands/connect/family.ts +19 -3
- package/src/cli/commands/connect/index.ts +23 -24
- package/src/cli/commands/connect/outcome.ts +8 -0
- package/src/cli/commands/connect/settle.ts +94 -22
- package/src/cli/commands/connect/target-note.ts +2 -2
- package/src/cli/commands/connection.ts +18 -7
- package/src/cli/commands/knowledge/index.ts +14 -34
- package/src/cli/commands/mcp/register.ts +1 -1
- package/src/cli/commands/operate/dashboard.ts +2 -2
- package/src/cli/commands/operate/inspect.ts +5 -1
- package/src/cli/commands/operate/migrate.ts +85 -1
- package/src/cli/commands/operate/outputs.ts +7 -22
- package/src/cli/commands/operate/status.ts +95 -70
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/profile/removal.ts +36 -25
- package/src/cli/commands/profile/remove.ts +5 -2
- package/src/cli/commands/profile.ts +56 -40
- package/src/cli/commands/sync.ts +94 -162
- package/src/cli/commands/target.ts +115 -74
- package/src/cli/commands/update.ts +56 -1
- package/src/cli/config-edit.ts +47 -16
- package/src/cli/dashboard-page.ts +10 -1
- package/src/cli/endpoint-url.ts +3 -3
- package/src/cli/main.ts +11 -7
- package/src/cli/publish.ts +4 -2
- package/src/cli/runtime/open.ts +20 -10
- package/src/cli/runtime/select.ts +69 -22
- package/src/cli/selection-require.ts +79 -0
- package/src/cli/selection.ts +45 -92
- package/src/cli/usage.ts +3 -0
- package/src/cli/workspace-migrate.ts +385 -0
- package/src/deployments/bootstrap.ts +31 -11
- package/src/deployments/deploy.ts +54 -27
- package/src/deployments/knowledge.ts +5 -2
- package/src/deployments/prepare.ts +3 -1
- package/src/deployments/serving.ts +19 -15
- package/src/deployments/upload.ts +10 -1
- package/src/profile/deployments.ts +64 -53
- package/src/profile/index.ts +22 -6
- package/src/profile/legacy.ts +92 -0
- package/src/profile/load.ts +12 -28
- package/src/profile/registry.ts +182 -0
- package/src/profile/schema.ts +171 -110
- package/src/profile/targets.ts +62 -91
- package/src/profile/testing.ts +78 -0
- package/src/profile/workspace.ts +11 -25
- package/src/providers/setup/provider.ts +10 -2
- package/src/server/dashboard.ts +5 -1
- package/src/server/harness.ts +1 -6
- package/src/cli/commands/profile/declare.ts +0 -154
- package/src/deployments/servable.ts +0 -82
- package/src/deployments/sync-apply.ts +0 -330
- package/src/deployments/sync.ts +0 -164
package/src/cli/runtime/open.ts
CHANGED
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
KNOWLEDGE_LAYOUT,
|
|
10
10
|
layout,
|
|
11
11
|
listProfiles,
|
|
12
|
-
undeclaredTarget,
|
|
13
12
|
workspacePath,
|
|
14
13
|
type Config,
|
|
15
14
|
type Resolution,
|
|
15
|
+
type TargetConfig,
|
|
16
16
|
} from '#profile';
|
|
17
17
|
import { ProviderRegistry, toPolicyDocument } from '#registry';
|
|
18
18
|
import { Dispatcher, createConsoleLogger } from '#dispatch';
|
|
@@ -48,6 +48,15 @@ export interface Runtime {
|
|
|
48
48
|
readonly resolution: Resolution;
|
|
49
49
|
readonly config: Config;
|
|
50
50
|
readonly target: string;
|
|
51
|
+
/**
|
|
52
|
+
* The target's adapter set, from the workspace that declares it.
|
|
53
|
+
*
|
|
54
|
+
* Here because the config no longer carries it. Every caller that used to
|
|
55
|
+
* reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
|
|
56
|
+
* knowledge repository — reads this instead, and reads it without following
|
|
57
|
+
* the pointer a second time (ADR-052).
|
|
58
|
+
*/
|
|
59
|
+
readonly declared: TargetConfig;
|
|
51
60
|
readonly state: RuntimeState;
|
|
52
61
|
/** The durable log, for reading. Copies, if any, are write-only and not here. */
|
|
53
62
|
readonly audit: AuditReader;
|
|
@@ -133,16 +142,15 @@ export async function openRuntime(
|
|
|
133
142
|
flags: GlobalFlags,
|
|
134
143
|
options: OpenOptions = {},
|
|
135
144
|
): Promise<Runtime> {
|
|
136
|
-
const { resolution, config, target } = await resolveProfile(flags);
|
|
137
|
-
|
|
138
|
-
// `
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
// says why one copy is the most that survives.
|
|
143
|
-
if (!declared) throw undeclaredTarget(target, config, resolution.profile);
|
|
145
|
+
const { resolution, config, target, resolved } = await resolveProfile(flags);
|
|
146
|
+
// `resolveProfile` returns this for every caller that did not ask to create
|
|
147
|
+
// the target, and `openRuntime` never does — a runtime for a target that does
|
|
148
|
+
// not exist yet has nothing to open. The check is what makes that readable at
|
|
149
|
+
// the type level rather than a comment.
|
|
150
|
+
if (!resolved) throw new Error(`Target "${target}" has nothing to open yet`);
|
|
144
151
|
|
|
145
|
-
const
|
|
152
|
+
const declared = resolved.declared;
|
|
153
|
+
const root = resolved.workspaceRoot;
|
|
146
154
|
const adapters: TargetInput = { declared, config, root, target };
|
|
147
155
|
|
|
148
156
|
// Credentials first: an S3 key pair is itself a credential reference, so the
|
|
@@ -240,6 +248,7 @@ export async function openRuntime(
|
|
|
240
248
|
key: `${connection.provider}.${connection.id}`,
|
|
241
249
|
provider: connection.provider,
|
|
242
250
|
account: connection.account,
|
|
251
|
+
...(connection.label ? { label: connection.label } : {}),
|
|
243
252
|
}));
|
|
244
253
|
|
|
245
254
|
// Read before the registry is built, so a store that cannot be reached is a
|
|
@@ -340,6 +349,7 @@ export async function openRuntime(
|
|
|
340
349
|
resolution,
|
|
341
350
|
config,
|
|
342
351
|
target,
|
|
352
|
+
declared,
|
|
343
353
|
state,
|
|
344
354
|
audit,
|
|
345
355
|
credentials,
|
|
@@ -3,11 +3,15 @@ import type { SecretStore } from '#secrets';
|
|
|
3
3
|
import type { BlobStore } from '#stores/blobs';
|
|
4
4
|
import {
|
|
5
5
|
loadProfileConfig,
|
|
6
|
+
openTarget,
|
|
7
|
+
readRegistry,
|
|
6
8
|
resolveSelection,
|
|
9
|
+
resolveTargetWorkspace,
|
|
10
|
+
resolveWorkspaceRoot,
|
|
7
11
|
requireTarget,
|
|
8
|
-
undeclaredTarget,
|
|
9
12
|
type Config,
|
|
10
13
|
type ProfileSelection,
|
|
14
|
+
type ResolvedTarget,
|
|
11
15
|
type Resolution,
|
|
12
16
|
} from '#profile';
|
|
13
17
|
import { openSecrets, openStorage } from '#deployments/target.ts';
|
|
@@ -40,41 +44,78 @@ export async function resolveProfile(
|
|
|
40
44
|
resolution: Resolution;
|
|
41
45
|
config: Config;
|
|
42
46
|
target: string;
|
|
47
|
+
/**
|
|
48
|
+
* The adapter set, already followed to whichever workspace declares it.
|
|
49
|
+
*
|
|
50
|
+
* `undefined` only under `allowUndeclaredTarget`, which is `deploy` on a first
|
|
51
|
+
* run: the target does not exist yet, so there is nothing to follow and
|
|
52
|
+
* nothing to open. Every other caller can rely on it.
|
|
53
|
+
*/
|
|
54
|
+
resolved: ResolvedTarget | undefined;
|
|
43
55
|
}> {
|
|
44
56
|
// Spread rather than assigned: `exactOptionalPropertyTypes` makes an explicit
|
|
45
57
|
// `env: undefined` a different type from an absent one, and the absent one is
|
|
46
58
|
// what means "read the real environment".
|
|
47
59
|
const env = options.env !== undefined ? { env: options.env } : {};
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
// **Target first, and the order is the change.** It used to find the profile,
|
|
62
|
+
// read its config, and ask that config which targets existed — which is why
|
|
63
|
+
// "is `cloud` declared" had a different answer per profile, and why a profile
|
|
64
|
+
// rewritten without its cloud block reported a running deployment as gone.
|
|
65
|
+
//
|
|
66
|
+
// A target is a workspace now (ADR-052), so it has to be resolved before there
|
|
67
|
+
// is anywhere to look for a profile: `personal` on `local` and `personal` on
|
|
68
|
+
// `cloud` are two files, in two workspaces, and only the target says which one
|
|
69
|
+
// this command means.
|
|
70
|
+
const localRoot = resolveWorkspaceRoot(env);
|
|
71
|
+
const registry = await readRegistry(localRoot);
|
|
72
|
+
const target = requireTarget(registry, flags.target, {
|
|
53
73
|
allowUndeclared: options.allowUndeclaredTarget === true,
|
|
54
|
-
|
|
74
|
+
root: localRoot,
|
|
55
75
|
});
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
// `deploy` on a first run names a target nothing declares yet, and there is no
|
|
78
|
+
// workspace to follow. It resolves its own adapters from the flags it was
|
|
79
|
+
// given; everything else follows the pointer here, once.
|
|
80
|
+
const resolved = options.allowUndeclaredTarget === true && !(target in registry)
|
|
81
|
+
? undefined
|
|
82
|
+
: await openTarget(localRoot, target);
|
|
83
|
+
|
|
84
|
+
const root = resolved?.workspaceRoot ?? localRoot;
|
|
85
|
+
const selection = await resolveSelection({ profileFlag: flags.profile, root, ...env });
|
|
86
|
+
const { config } = await loadProfileConfig(root, selection.profile);
|
|
87
|
+
|
|
88
|
+
return { resolution: { ...selection, target }, config, target, resolved };
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
/**
|
|
61
|
-
* A profile without
|
|
92
|
+
* A profile, without opening any of its stores.
|
|
62
93
|
*
|
|
63
94
|
* `check` validates a YAML file, `config show` prints the whole of it, and
|
|
64
|
-
* `policy list` reads a block that is
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
95
|
+
* `policy list` reads a block that is the same wherever the profile runs. None
|
|
96
|
+
* of them needs a credential store or a bucket, and opening one would make all
|
|
97
|
+
* three fail on a target that is merely unreachable.
|
|
98
|
+
*
|
|
99
|
+
* It still needs `--target`, which it did not before. That is not ceremony: a
|
|
100
|
+
* profile lives in exactly one target's workspace now (ADR-052), so without one
|
|
101
|
+
* there is no file to validate — `personal` on `local` and `personal` on `cloud`
|
|
102
|
+
* are different documents. What the flag buys here is finding the file; what it
|
|
103
|
+
* still does not buy is opening anything.
|
|
68
104
|
*/
|
|
69
105
|
export async function resolveProfileOnly(
|
|
70
106
|
flags: GlobalFlags,
|
|
71
107
|
options: { env?: Record<string, string | undefined> } = {},
|
|
72
|
-
): Promise<{ selection: ProfileSelection; config: Config }> {
|
|
108
|
+
): Promise<{ selection: ProfileSelection; config: Config; target: string }> {
|
|
73
109
|
const env = options.env !== undefined ? { env: options.env } : {};
|
|
74
|
-
const
|
|
75
|
-
const
|
|
110
|
+
const localRoot = resolveWorkspaceRoot(env);
|
|
111
|
+
const registry = await readRegistry(localRoot);
|
|
112
|
+
const target = requireTarget(registry, flags.target, { root: localRoot });
|
|
113
|
+
const root = await resolveTargetWorkspace(localRoot, target);
|
|
114
|
+
|
|
115
|
+
const selection = await resolveSelection({ profileFlag: flags.profile, root, ...env });
|
|
116
|
+
const { config } = await loadProfileConfig(root, selection.profile);
|
|
76
117
|
|
|
77
|
-
return { selection, config };
|
|
118
|
+
return { selection, config, target };
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
/**
|
|
@@ -90,9 +131,16 @@ export async function openSecretStoreFor(
|
|
|
90
131
|
root: string,
|
|
91
132
|
target: string,
|
|
92
133
|
): Promise<SecretStore> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
// Resolved here rather than taken from the caller, so `secrets push --from
|
|
135
|
+
// local --to cloud` can hold two targets that live in two different workspaces
|
|
136
|
+
// without the caller having to follow either pointer itself.
|
|
137
|
+
const resolved = await openTarget(root, target);
|
|
138
|
+
return openSecrets({
|
|
139
|
+
declared: resolved.declared,
|
|
140
|
+
config,
|
|
141
|
+
root: resolved.workspaceRoot,
|
|
142
|
+
target,
|
|
143
|
+
});
|
|
96
144
|
}
|
|
97
145
|
|
|
98
146
|
/**
|
|
@@ -113,10 +161,9 @@ export async function openBlobStoreFor(
|
|
|
113
161
|
target: string,
|
|
114
162
|
area?: string,
|
|
115
163
|
): Promise<BlobStore> {
|
|
116
|
-
const
|
|
117
|
-
if (!declared) throw undeclaredTarget(target, config);
|
|
164
|
+
const resolved = await openTarget(root, target);
|
|
118
165
|
|
|
119
|
-
const input = { declared, config, root, target };
|
|
166
|
+
const input = { declared: resolved.declared, config, root: resolved.workspaceRoot, target };
|
|
120
167
|
const storage = await openStorage(input, await openSecrets(input));
|
|
121
168
|
return area === undefined ? storage() : storage(area);
|
|
122
169
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listProfiles,
|
|
3
|
+
noProfileNamed,
|
|
4
|
+
noTargetNamed,
|
|
5
|
+
readRegistry,
|
|
6
|
+
resolveTargetWorkspace,
|
|
7
|
+
resolveWorkspaceRoot,
|
|
8
|
+
} from '#profile';
|
|
9
|
+
import type { Flags } from './argv.ts';
|
|
10
|
+
import { dispatchWillRefuse, requirementFor } from './selection.ts';
|
|
11
|
+
import { refuseIfUnmigrated } from './workspace-migrate.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Refusing a command that has not said what it acts on.
|
|
15
|
+
*
|
|
16
|
+
* Apart from `selection.ts`, which holds the two tables — what each command
|
|
17
|
+
* requires, and which flags it accepts — and argues for keeping *those two*
|
|
18
|
+
* together. This is the third thing: actually resolving a selection, which under
|
|
19
|
+
* ADR-052 means reading the registry, following a pointer to the workspace that
|
|
20
|
+
* declares the target, and listing the profiles that live there. Table lookups
|
|
21
|
+
* and network reads are not the same job, and the file-size budget noticed
|
|
22
|
+
* before anyone else did.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Refuse before the command runs, naming what it wants and what there is.
|
|
27
|
+
*
|
|
28
|
+
* Async, and it reads the workspace — but only on the way to throwing. The
|
|
29
|
+
* useful half of "which profile did you mean" is the list of them, and the same
|
|
30
|
+
* for targets; a refusal that only restates the flag name leaves someone to go
|
|
31
|
+
* and look it up. Both messages come from `#profile` so this file and the
|
|
32
|
+
* resolver cannot describe the same refusal differently, and both name an
|
|
33
|
+
* exported variable that no longer counts — the shell still configured for the
|
|
34
|
+
* old world is the state hardest to diagnose from the inside.
|
|
35
|
+
*/
|
|
36
|
+
export async function requireSelection(
|
|
37
|
+
first: string,
|
|
38
|
+
second: string | undefined,
|
|
39
|
+
flags: Flags,
|
|
40
|
+
env?: Record<string, string | undefined>,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (dispatchWillRefuse(first, second)) return;
|
|
43
|
+
|
|
44
|
+
const needs = requirementFor(first, second);
|
|
45
|
+
if (needs === 'none') return;
|
|
46
|
+
|
|
47
|
+
const root = resolveWorkspaceRoot(env ? { env } : {});
|
|
48
|
+
|
|
49
|
+
// **Target before profile, for both levels.** It used to ask for the profile
|
|
50
|
+
// and read that profile's own target list — the ordering ADR-052 inverted,
|
|
51
|
+
// since the profile lives inside the target and there is nowhere to look for
|
|
52
|
+
// one until the target is known. `refuseIfUnmigrated` is on the refusal path
|
|
53
|
+
// only, so it costs nothing when the flag is present.
|
|
54
|
+
if (typeof flags['target'] !== 'string') {
|
|
55
|
+
await refuseIfUnmigrated(root);
|
|
56
|
+
throw noTargetNamed(await readRegistry(root), root, env);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A named target the registry cannot know, because it does not exist yet.
|
|
60
|
+
if (!(flags['target'] in (await readRegistry(root)))) await refuseIfUnmigrated(root);
|
|
61
|
+
|
|
62
|
+
if (needs === 'target') return;
|
|
63
|
+
|
|
64
|
+
if (typeof flags['profile'] !== 'string') {
|
|
65
|
+
// Listed from the target's own workspace, so the names offered are ones that
|
|
66
|
+
// command could act on. An unreachable pointer degrades to the empty list
|
|
67
|
+
// rather than failing — "which profile" is still the question being asked.
|
|
68
|
+
const where = await resolveTargetWorkspace(root, flags['target']).catch(() => root);
|
|
69
|
+
throw noProfileNamed(where, await listProfiles(where).catch(() => []), env);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Flags every command accepts, whatever it does.
|
|
75
|
+
*
|
|
76
|
+
* `--help` short-circuits before dispatch, and `--json` is offered widely enough
|
|
77
|
+
* that listing it per command would be noise. `--quiet` is read by `announce`
|
|
78
|
+
* rather than by any one command.
|
|
79
|
+
*/
|
package/src/cli/selection.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConfigError,
|
|
3
|
-
listProfiles,
|
|
4
|
-
loadProfileConfig,
|
|
5
|
-
loadWorkspaceProfiles,
|
|
6
|
-
noProfileNamed,
|
|
7
|
-
noTargetInWorkspace,
|
|
8
|
-
noTargetNamed,
|
|
9
|
-
resolveWorkspaceRoot,
|
|
10
|
-
targetsByName,
|
|
11
|
-
} from '#profile';
|
|
1
|
+
import { ConfigError } from '#profile';
|
|
12
2
|
import type { Flags } from './argv.ts';
|
|
13
3
|
import { CONNECT_CUSTOM_FLAGS } from './commands/connect/custom/spec.ts';
|
|
14
4
|
import { nearest } from './nearest.ts';
|
|
@@ -39,11 +29,17 @@ import { nearest } from './nearest.ts';
|
|
|
39
29
|
* What a command must be told before it can act.
|
|
40
30
|
*
|
|
41
31
|
* `target` is not a weaker `profile+target`. It says the command's subject *is*
|
|
42
|
-
* the target, and that the profiles behind it are every profile
|
|
43
|
-
*
|
|
32
|
+
* the target, and that the profiles behind it are every profile *in* it rather
|
|
33
|
+
* than one the operator picks (ADR-043, ADR-052). `--profile` stays accepted
|
|
44
34
|
* there, as a filter.
|
|
35
|
+
*
|
|
36
|
+
* There is no `profile`-alone level any more. Five commands sat there — the ones
|
|
37
|
+
* that read a profile's file and open nothing — and it stopped being reachable
|
|
38
|
+
* when a profile came to live inside one target's workspace: without a target
|
|
39
|
+
* there is no file to read. The level is gone rather than left empty, so nobody
|
|
40
|
+
* adds a sixth command to a level that cannot resolve.
|
|
45
41
|
*/
|
|
46
|
-
export type Requires = 'none' | '
|
|
42
|
+
export type Requires = 'none' | 'target' | 'profile+target';
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
45
|
* The rule, per command path.
|
|
@@ -87,28 +83,33 @@ export const SELECTION: Record<string, Requires> = {
|
|
|
87
83
|
'mcp list': 'none',
|
|
88
84
|
// The bare forms, which each dispatch to a `case undefined` in `main.ts`.
|
|
89
85
|
// `lanes link profile` is `profile list`, and needs the same as it.
|
|
90
|
-
profile: 'none',
|
|
91
86
|
mcp: 'none',
|
|
92
|
-
'profile list': 'none',
|
|
93
|
-
'profile add': 'none',
|
|
94
87
|
'profile default': 'none',
|
|
95
88
|
'target use': 'none',
|
|
96
89
|
'vault key': 'none',
|
|
90
|
+
// Listing the registry is what you run to find out what `--target` accepts, so
|
|
91
|
+
// requiring the answer as input would be circular (ADR-052).
|
|
92
|
+
target: 'none',
|
|
93
|
+
'target list': 'none',
|
|
94
|
+
|
|
95
|
+
// A profile lives in one target's workspace, so listing or creating one names
|
|
96
|
+
// which workspace. `target show` follows the pointer, which `list` does not.
|
|
97
|
+
profile: 'target',
|
|
98
|
+
'profile list': 'target',
|
|
99
|
+
'profile add': 'target',
|
|
100
|
+
'profile remove': 'target',
|
|
101
|
+
'target show': 'target',
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'config show': 'profile',
|
|
103
|
-
|
|
104
|
-
'
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
// Target-independent for the same reason `policy list` is: the block is
|
|
109
|
-
// declared once in the YAML and applies to every target the profile has.
|
|
110
|
-
identity: 'profile',
|
|
111
|
-
'identity list': 'profile',
|
|
103
|
+
// These read one profile's file and open nothing. The target is what says
|
|
104
|
+
// which workspace holds it — required to *locate* the profile, not to open it.
|
|
105
|
+
check: 'profile+target',
|
|
106
|
+
config: 'profile+target',
|
|
107
|
+
'config show': 'profile+target',
|
|
108
|
+
policy: 'profile+target',
|
|
109
|
+
'policy list': 'profile+target',
|
|
110
|
+
identity: 'profile+target',
|
|
111
|
+
'identity list': 'profile+target',
|
|
112
|
+
'secrets push': 'profile+target',
|
|
112
113
|
|
|
113
114
|
connect: 'profile+target',
|
|
114
115
|
// Both edit the profile config, and `disconnect` also opens the target's
|
|
@@ -205,7 +206,7 @@ const SUBCOMMANDS: Record<string, readonly string[]> = {
|
|
|
205
206
|
* bogus" is the useful sentence, and a complaint about `--profile` on a command
|
|
206
207
|
* that does not exist sends someone off to fix the wrong thing.
|
|
207
208
|
*/
|
|
208
|
-
function dispatchWillRefuse(first: string, second: string | undefined): boolean {
|
|
209
|
+
export function dispatchWillRefuse(first: string, second: string | undefined): boolean {
|
|
209
210
|
const known = SUBCOMMANDS[first];
|
|
210
211
|
if (!known || second === undefined) return false;
|
|
211
212
|
return !known.includes(second);
|
|
@@ -228,65 +229,6 @@ export function requirementFor(first: string, second: string | undefined): Requi
|
|
|
228
229
|
return SELECTION[selectionKey(first, second)] ?? 'profile+target';
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
/**
|
|
232
|
-
* Refuse before the command runs, naming what it wants and what there is.
|
|
233
|
-
*
|
|
234
|
-
* Async, and it reads the workspace — but only on the way to throwing. The
|
|
235
|
-
* useful half of "which profile did you mean" is the list of them, and the same
|
|
236
|
-
* for targets; a refusal that only restates the flag name leaves someone to go
|
|
237
|
-
* and look it up. Both messages come from `#profile` so this file and the
|
|
238
|
-
* resolver cannot describe the same refusal differently, and both name an
|
|
239
|
-
* exported variable that no longer counts — the shell still configured for the
|
|
240
|
-
* old world is the state hardest to diagnose from the inside.
|
|
241
|
-
*/
|
|
242
|
-
export async function requireSelection(
|
|
243
|
-
first: string,
|
|
244
|
-
second: string | undefined,
|
|
245
|
-
flags: Flags,
|
|
246
|
-
env?: Record<string, string | undefined>,
|
|
247
|
-
): Promise<void> {
|
|
248
|
-
if (dispatchWillRefuse(first, second)) return;
|
|
249
|
-
|
|
250
|
-
const needs = requirementFor(first, second);
|
|
251
|
-
if (needs === 'none') return;
|
|
252
|
-
|
|
253
|
-
// Asked before the profile requirement, because for these there is none. The
|
|
254
|
-
// refusal has to describe the workspace rather than one profile's targets,
|
|
255
|
-
// since the command was never going to act on only one.
|
|
256
|
-
if (needs === 'target') {
|
|
257
|
-
if (typeof flags['target'] === 'string') return;
|
|
258
|
-
const root = resolveWorkspaceRoot(env ? { env } : {});
|
|
259
|
-
throw noTargetInWorkspace(targetsByName(await loadWorkspaceProfiles(root)), root, env);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const profile = flags['profile'];
|
|
263
|
-
if (typeof profile !== 'string') {
|
|
264
|
-
const root = resolveWorkspaceRoot(env ? { env } : {});
|
|
265
|
-
throw noProfileNamed(root, await listProfiles(root), env);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (needs !== 'profile+target' || typeof flags['target'] === 'string') return;
|
|
269
|
-
|
|
270
|
-
// The profile is known by here, so the target list is the one belonging to it
|
|
271
|
-
// rather than a guess. A profile that does not exist is a different refusal,
|
|
272
|
-
// and `resolveSelection` gives it a better one a moment later.
|
|
273
|
-
const root = resolveWorkspaceRoot(env ? { env } : {});
|
|
274
|
-
try {
|
|
275
|
-
const { config } = await loadProfileConfig(root, profile);
|
|
276
|
-
throw noTargetNamed(config, profile, env);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
if (error instanceof ConfigError) throw error;
|
|
279
|
-
throw new ConfigError(`--target is required for "${[first, second].filter(Boolean).join(' ')}".`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Flags every command accepts, whatever it does.
|
|
285
|
-
*
|
|
286
|
-
* `--help` short-circuits before dispatch, and `--json` is offered widely enough
|
|
287
|
-
* that listing it per command would be noise. `--quiet` is read by `announce`
|
|
288
|
-
* rather than by any one command.
|
|
289
|
-
*/
|
|
290
232
|
const UNIVERSAL = ['help', 'json', 'quiet'];
|
|
291
233
|
|
|
292
234
|
/**
|
|
@@ -306,6 +248,7 @@ const ACCEPTS: Record<string, readonly string[]> = {
|
|
|
306
248
|
connect: [
|
|
307
249
|
'id',
|
|
308
250
|
'display-name',
|
|
251
|
+
'label',
|
|
309
252
|
'replace',
|
|
310
253
|
'non-interactive',
|
|
311
254
|
'accept-broad-scopes',
|
|
@@ -366,6 +309,16 @@ const ACCEPTS: Record<string, readonly string[]> = {
|
|
|
366
309
|
* `parseArgv` returns every `--anything` it sees and no command ever inspected
|
|
367
310
|
* the leftovers. A typo was swallowed the same way on every command in the CLI.
|
|
368
311
|
*/
|
|
312
|
+
/**
|
|
313
|
+
* Commands that name their profile as an argument, and so refuse the flag.
|
|
314
|
+
*
|
|
315
|
+
* A `--profile` here could only name a *second* profile and disagree with the
|
|
316
|
+
* positional one. They needed no exception while both sat at `none`; ADR-052
|
|
317
|
+
* moved them to `target`, which made them inherit `--profile` from the rule
|
|
318
|
+
* below.
|
|
319
|
+
*/
|
|
320
|
+
const POSITIONAL_PROFILE = new Set(['profile add', 'profile remove']);
|
|
321
|
+
|
|
369
322
|
export function assertKnownFlags(first: string, second: string | undefined, flags: Flags): void {
|
|
370
323
|
if (dispatchWillRefuse(first, second)) return;
|
|
371
324
|
|
|
@@ -377,7 +330,7 @@ export function assertKnownFlags(first: string, second: string | undefined, flag
|
|
|
377
330
|
...(ACCEPTS[key] ?? []),
|
|
378
331
|
// `target` accepts both: the target is what it acts on, and `--profile`
|
|
379
332
|
// narrows it to one of the profiles behind it.
|
|
380
|
-
...(needs !== 'none' ? ['profile'] : []),
|
|
333
|
+
...(needs !== 'none' && !POSITIONAL_PROFILE.has(key) ? ['profile'] : []),
|
|
381
334
|
...(needs === 'target' || needs === 'profile+target' ? ['target'] : []),
|
|
382
335
|
]);
|
|
383
336
|
|
package/src/cli/usage.ts
CHANGED
|
@@ -154,6 +154,9 @@ ${style.bold('Other flags')}
|
|
|
154
154
|
--json machine-readable output, where a command offers it
|
|
155
155
|
--non-interactive never prompt: connect refuses with what to store,
|
|
156
156
|
deploy takes the answers its config already holds
|
|
157
|
+
--label <text> what to call a connection, instead of being asked at the
|
|
158
|
+
end of connect. A display name only: nothing addresses
|
|
159
|
+
a connection by it. Use relabel to change one later
|
|
157
160
|
--accept-broad-scopes agree in advance to scopes broader than a provider needs
|
|
158
161
|
--own-client register your own OAuth client instead of using the
|
|
159
162
|
one this project operates (connect only)
|