@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.
Files changed (59) hide show
  1. package/instructions/agents/lanes-link-scout.md +1 -1
  2. package/instructions/skills/lanes-link/SKILL.md +33 -29
  3. package/package.json +2 -1
  4. package/src/cli/commands/connect/custom/index.ts +2 -0
  5. package/src/cli/commands/connect/custom/spec.ts +1 -0
  6. package/src/cli/commands/connect/declare.ts +76 -0
  7. package/src/cli/commands/connect/family.ts +19 -3
  8. package/src/cli/commands/connect/index.ts +23 -24
  9. package/src/cli/commands/connect/outcome.ts +8 -0
  10. package/src/cli/commands/connect/settle.ts +94 -22
  11. package/src/cli/commands/connect/target-note.ts +2 -2
  12. package/src/cli/commands/connection.ts +18 -7
  13. package/src/cli/commands/knowledge/index.ts +14 -34
  14. package/src/cli/commands/mcp/register.ts +1 -1
  15. package/src/cli/commands/operate/dashboard.ts +2 -2
  16. package/src/cli/commands/operate/inspect.ts +5 -1
  17. package/src/cli/commands/operate/migrate.ts +85 -1
  18. package/src/cli/commands/operate/outputs.ts +7 -22
  19. package/src/cli/commands/operate/status.ts +95 -70
  20. package/src/cli/commands/operate/tools.ts +1 -1
  21. package/src/cli/commands/profile/removal.ts +36 -25
  22. package/src/cli/commands/profile/remove.ts +5 -2
  23. package/src/cli/commands/profile.ts +56 -40
  24. package/src/cli/commands/sync.ts +94 -162
  25. package/src/cli/commands/target.ts +115 -74
  26. package/src/cli/commands/update.ts +56 -1
  27. package/src/cli/config-edit.ts +47 -16
  28. package/src/cli/dashboard-page.ts +10 -1
  29. package/src/cli/endpoint-url.ts +3 -3
  30. package/src/cli/main.ts +11 -7
  31. package/src/cli/publish.ts +4 -2
  32. package/src/cli/runtime/open.ts +20 -10
  33. package/src/cli/runtime/select.ts +69 -22
  34. package/src/cli/selection-require.ts +79 -0
  35. package/src/cli/selection.ts +45 -92
  36. package/src/cli/usage.ts +3 -0
  37. package/src/cli/workspace-migrate.ts +385 -0
  38. package/src/deployments/bootstrap.ts +31 -11
  39. package/src/deployments/deploy.ts +54 -27
  40. package/src/deployments/knowledge.ts +5 -2
  41. package/src/deployments/prepare.ts +3 -1
  42. package/src/deployments/serving.ts +19 -15
  43. package/src/deployments/upload.ts +10 -1
  44. package/src/profile/deployments.ts +64 -53
  45. package/src/profile/index.ts +22 -6
  46. package/src/profile/legacy.ts +92 -0
  47. package/src/profile/load.ts +12 -28
  48. package/src/profile/registry.ts +182 -0
  49. package/src/profile/schema.ts +171 -110
  50. package/src/profile/targets.ts +62 -91
  51. package/src/profile/testing.ts +78 -0
  52. package/src/profile/workspace.ts +11 -25
  53. package/src/providers/setup/provider.ts +10 -2
  54. package/src/server/dashboard.ts +5 -1
  55. package/src/server/harness.ts +1 -6
  56. package/src/cli/commands/profile/declare.ts +0 -154
  57. package/src/deployments/servable.ts +0 -82
  58. package/src/deployments/sync-apply.ts +0 -330
  59. package/src/deployments/sync.ts +0 -164
@@ -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
- const declared = config.targets[target];
138
- // `resolveProfile` has already checked this, so reaching it means a caller
139
- // passed `allowUndeclaredTarget`. Through the shared refusal all the same:
140
- // this was a fourth spelling of that sentence with the profile name and the
141
- // list of what exists both missing, and the comment on `undeclaredTarget`
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 root = resolution.workspaceRoot;
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
- const selection = await resolveSelection({ profileFlag: flags.profile, ...env });
50
-
51
- const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
52
- const target = requireTarget(config, flags.target, {
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
- profile: selection.profile,
74
+ root: localRoot,
55
75
  });
56
76
 
57
- return { resolution: { ...selection, target }, config, target };
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 a target, for the commands that do not open one.
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 target-independent by construction. Making
65
- * those three demand a `--target` would be the ceremony that teaches people to
66
- * type `--target local` without reading it, which is how a required flag stops
67
- * being a guard.
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 selection = await resolveSelection({ profileFlag: flags.profile, ...env });
75
- const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
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
- const declared = config.targets[target];
94
- if (!declared) throw undeclaredTarget(target, config);
95
- return openSecrets({ declared, config, root, target });
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 declared = config.targets[target];
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
+ */
@@ -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 declaring it
43
- * rather than one the operator picks (ADR-043). `--profile` stays accepted
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' | 'profile' | 'target' | 'profile+target';
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
- check: 'profile',
99
- config: 'profile',
100
- policy: 'profile',
101
- target: 'profile',
102
- 'config show': 'profile',
103
- 'policy list': 'profile',
104
- 'target list': 'profile',
105
- 'target show': 'profile',
106
- 'secrets push': 'profile',
107
- 'profile remove': 'none',
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)