@lanes-sh/link 0.7.2 → 0.8.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 (156) hide show
  1. package/README.md +20 -10
  2. package/instructions/agents/lanes-link-scout.md +2 -2
  3. package/instructions/skills/lanes-link/SKILL.md +136 -61
  4. package/package.json +2 -1
  5. package/src/audit/index.ts +8 -1
  6. package/src/auth/index.ts +58 -2
  7. package/src/auth/lanes/assertion.ts +256 -0
  8. package/src/auth/lanes/callback.ts +135 -0
  9. package/src/auth/lanes/federation.ts +50 -0
  10. package/src/auth/lanes/login.ts +294 -0
  11. package/src/auth/lanes/members.ts +103 -0
  12. package/src/auth/lanes/session.ts +97 -0
  13. package/src/auth/oauth/grant.ts +183 -0
  14. package/src/auth/oauth/result.ts +27 -0
  15. package/src/auth/oauth/server.ts +176 -203
  16. package/src/auth/oauth/store.ts +65 -0
  17. package/src/auth/remote.ts +32 -9
  18. package/src/cli/accepts.ts +108 -0
  19. package/src/cli/argv.ts +57 -3
  20. package/src/cli/audit-change.ts +140 -0
  21. package/src/cli/callback-page.ts +36 -115
  22. package/src/cli/commands/auth-dispatch.ts +48 -0
  23. package/src/cli/commands/auth.ts +229 -0
  24. package/src/cli/commands/connect/accounts.ts +4 -4
  25. package/src/cli/commands/connect/authorise.ts +4 -4
  26. package/src/cli/commands/connect/bind-credential.ts +2 -1
  27. package/src/cli/commands/connect/custom/index.ts +1 -1
  28. package/src/cli/commands/connect/custom/write.ts +2 -2
  29. package/src/cli/commands/connect/grant.ts +29 -14
  30. package/src/cli/commands/connect/index.ts +88 -87
  31. package/src/cli/commands/connect/options.ts +83 -0
  32. package/src/cli/commands/connect/registration.ts +50 -0
  33. package/src/cli/commands/connect/requirements.ts +1 -1
  34. package/src/cli/commands/connect/settle.ts +4 -2
  35. package/src/cli/commands/connect/target-note.ts +7 -2
  36. package/src/cli/commands/connect/unknown.ts +1 -1
  37. package/src/cli/commands/connect/variables.ts +3 -2
  38. package/src/cli/commands/connection-list.ts +116 -0
  39. package/src/cli/commands/connection.ts +182 -165
  40. package/src/cli/commands/grant.ts +140 -0
  41. package/src/cli/commands/identity.ts +21 -9
  42. package/src/cli/commands/knowledge/index.ts +46 -79
  43. package/src/cli/commands/knowledge/migrate.ts +74 -13
  44. package/src/cli/commands/knowledge/show.ts +92 -0
  45. package/src/cli/commands/knowledge.ts +2 -1
  46. package/src/cli/commands/mcp/harnesses.ts +30 -8
  47. package/src/cli/commands/mcp/onboarding.ts +86 -0
  48. package/src/cli/commands/mcp/register.ts +16 -2
  49. package/src/cli/commands/mcp.ts +1 -0
  50. package/src/cli/commands/members.ts +288 -0
  51. package/src/cli/commands/operate/attach.ts +3 -3
  52. package/src/cli/commands/operate/audit.ts +11 -7
  53. package/src/cli/commands/operate/auth.ts +28 -11
  54. package/src/cli/commands/operate/findings.ts +2 -1
  55. package/src/cli/commands/operate/inspect.ts +37 -19
  56. package/src/cli/commands/operate/migrate.ts +29 -12
  57. package/src/cli/commands/operate/outputs.ts +3 -3
  58. package/src/cli/commands/operate/pair-certificate.ts +141 -0
  59. package/src/cli/commands/operate/pair.ts +324 -0
  60. package/src/cli/commands/operate/policy.ts +73 -22
  61. package/src/cli/commands/operate/serve.ts +52 -4
  62. package/src/cli/commands/operate/status.ts +18 -10
  63. package/src/cli/commands/operate/tools.ts +2 -2
  64. package/src/cli/commands/operate.ts +2 -0
  65. package/src/cli/commands/owner/shared.ts +13 -2
  66. package/src/cli/commands/owner/skills.ts +28 -8
  67. package/src/cli/commands/profile/removal.ts +79 -76
  68. package/src/cli/commands/profile/remove.ts +16 -1
  69. package/src/cli/commands/profile.ts +46 -10
  70. package/src/cli/commands/relabel.ts +112 -0
  71. package/src/cli/commands/secrets.ts +34 -12
  72. package/src/cli/commands/set-workspace.ts +96 -0
  73. package/src/cli/commands/setup.ts +2 -2
  74. package/src/cli/commands/sync.ts +8 -8
  75. package/src/cli/commands/target.ts +9 -7
  76. package/src/cli/commands/update.ts +58 -17
  77. package/src/cli/config-edit.ts +75 -140
  78. package/src/cli/config-migrate.ts +82 -64
  79. package/src/cli/config-repair.ts +89 -32
  80. package/src/cli/config-templates.ts +198 -0
  81. package/src/cli/contract3-data.ts +328 -0
  82. package/src/cli/contract3-shape.ts +186 -0
  83. package/src/cli/contract3.ts +282 -0
  84. package/src/cli/endpoint-url.ts +1 -1
  85. package/src/cli/lanes.ts +25 -1
  86. package/src/cli/main.ts +89 -14
  87. package/src/cli/migrate-plan.ts +12 -6
  88. package/src/cli/output.ts +34 -1
  89. package/src/cli/publish.ts +5 -2
  90. package/src/cli/runtime/open.ts +63 -98
  91. package/src/cli/runtime/registry.ts +6 -7
  92. package/src/cli/runtime/stores.ts +53 -0
  93. package/src/cli/runtime/types.ts +106 -0
  94. package/src/cli/runtime/vault.ts +19 -4
  95. package/src/cli/runtime/workspace.ts +60 -0
  96. package/src/cli/runtime.ts +2 -1
  97. package/src/cli/selection-require.ts +44 -13
  98. package/src/cli/selection.ts +127 -145
  99. package/src/cli/usage.ts +34 -18
  100. package/src/cli/workspace-migrate.ts +125 -16
  101. package/src/connectivity/manifest/provider.ts +3 -1
  102. package/src/connectivity/manifest/requirements.ts +1 -1
  103. package/src/deployments/bind.ts +1 -1
  104. package/src/deployments/deploy.ts +36 -27
  105. package/src/deployments/gcp/bucket.ts +18 -7
  106. package/src/deployments/gcp/provision.ts +7 -7
  107. package/src/deployments/prepare.ts +72 -24
  108. package/src/deployments/record.ts +1 -1
  109. package/src/deployments/report.ts +2 -2
  110. package/src/deployments/serving.ts +15 -74
  111. package/src/deployments/target.ts +15 -15
  112. package/src/deployments/upload.ts +46 -22
  113. package/src/dispatch/deps.ts +88 -0
  114. package/src/dispatch/dispatch.ts +21 -62
  115. package/src/policy/index.ts +47 -15
  116. package/src/profile/connections.ts +183 -0
  117. package/src/profile/deployments.ts +3 -3
  118. package/src/profile/index.ts +30 -5
  119. package/src/profile/layout.ts +86 -89
  120. package/src/profile/load.ts +80 -47
  121. package/src/profile/pairing.ts +32 -0
  122. package/src/profile/primitives.ts +35 -1
  123. package/src/profile/registry.ts +6 -6
  124. package/src/profile/schema.ts +172 -21
  125. package/src/profile/targets.ts +21 -9
  126. package/src/profile/testing.ts +69 -2
  127. package/src/profile/workspace.ts +58 -3
  128. package/src/providers/custom/index.ts +1 -1
  129. package/src/providers/custom/load.ts +2 -3
  130. package/src/providers/identity/provider.ts +1 -1
  131. package/src/providers/memory/provider.ts +20 -2
  132. package/src/providers/setup/plan.ts +1 -1
  133. package/src/providers/slack/index.ts +2 -2
  134. package/src/registry/policy-bridge.ts +33 -11
  135. package/src/registry/reconcile.ts +4 -4
  136. package/src/server/authorization.ts +94 -0
  137. package/src/server/edge.ts +14 -1
  138. package/src/server/endpoint.ts +85 -104
  139. package/src/server/generation.ts +10 -1
  140. package/src/server/harness.ts +71 -13
  141. package/src/server/index.ts +31 -0
  142. package/src/server/mcp/build.ts +20 -1
  143. package/src/server/mcp/client-info.ts +54 -0
  144. package/src/server/mcp/guide.ts +120 -0
  145. package/src/server/mcp/instructions.ts +1 -1
  146. package/src/server/mcp/prompts.ts +7 -3
  147. package/src/server/mcp/resources.ts +16 -8
  148. package/src/server/mcp/tools.ts +9 -3
  149. package/src/server/mcp/visibility.ts +18 -3
  150. package/src/server/oauth.ts +29 -109
  151. package/src/server/read/credential.ts +134 -0
  152. package/src/server/read/deployed.ts +56 -0
  153. package/src/server/read/listener.ts +54 -0
  154. package/src/server/read/open.ts +101 -0
  155. package/src/server/read/routes.ts +247 -0
  156. package/src/server/read/state.ts +171 -0
@@ -0,0 +1,53 @@
1
+ import { layout } from '#profile';
2
+ import type { BlobStore } from '#stores/blobs';
3
+ import type { StorageFactory } from '#deployments/target.ts';
4
+
5
+ /**
6
+ * Where the owner layer's bytes are, for a runtime being assembled.
7
+ *
8
+ * Split from `open.ts` so that file stays inside the size budget, on the seam
9
+ * it already had: `open.ts` decides what a runtime *is* and this decides where
10
+ * one store lives. Both roots are explicit areas rather than derived from
11
+ * `storage.path`, matching `openState` and `openAudit` — a workspace that moves
12
+ * its provider blobs does not move the reserved roots beside them.
13
+ */
14
+
15
+ /**
16
+ * A store with nothing in it, for a profile that grants no skills connection.
17
+ *
18
+ * Written out rather than imported from `#stores/blobs/testing.ts`: that module
19
+ * exists for tests, and production code reaching into it is how a test double
20
+ * ends up on a real path. Five methods, none of which can fail, is cheaper than
21
+ * the import would be to justify.
22
+ */
23
+ export const EMPTY_SKILL_STORE: BlobStore = {
24
+ get: async () => null,
25
+ put: async () => {
26
+ throw new Error('this profile grants no skills connection');
27
+ },
28
+ has: async () => false,
29
+ delete: async () => {},
30
+ list: async () => [],
31
+ };
32
+
33
+ /**
34
+ * Where a connection's skills live, in either workspace.
35
+ *
36
+ * `data/skills.d/<connection>/`, and deployed the same key under the bucket
37
+ * prefix. Going through the store rather than a filesystem path is what gives a
38
+ * deployment skills at all — a path is baked into a container image at build
39
+ * time and an object key is not, so before ADR-014 a deployed instance could
40
+ * only ever serve the skills that existed when its image was built.
41
+ *
42
+ * **Per connection**, which is the third answer this question has had. Policy
43
+ * gating `skills.<name>` was the whole isolation story while the bytes were
44
+ * shared (ADR-012 §1), and it is a weak one: it decides who may *run* a
45
+ * procedure, not who may read that it exists or what it says. ADR-030 made the
46
+ * bytes per profile; ADR-059 made them per connection, so two profiles granting
47
+ * one skills connection share a set and two granting different ones share
48
+ * nothing.
49
+ */
50
+ export function skillStore(storage: StorageFactory, connection: string): BlobStore {
51
+ return storage(layout.skills(connection));
52
+ }
53
+
@@ -0,0 +1,106 @@
1
+ import type { BearerAuthenticator } from '#auth';
2
+ import type { SecretStore } from '#secrets';
3
+ import type { AuditReader } from '#audit';
4
+ import type { RuntimeState } from '#stores/state';
5
+ import type { BlobStore } from '#stores/blobs';
6
+ import type { AnyConnector, ProviderManifest } from '#connectivity';
7
+ import type {
8
+ Config,
9
+ ConnectionConfig,
10
+ Resolution,
11
+ SelectedConnection,
12
+ TargetConfig,
13
+ } from '#profile';
14
+ import type { ProviderRegistry } from '#registry';
15
+ import type { Dispatcher } from '#dispatch';
16
+ import type { VaultStore } from '#providers/owner.ts';
17
+ import type { KnowledgeStores } from '#deployments/knowledge.ts';
18
+
19
+ /**
20
+ * What a command is handed once a profile has been opened.
21
+ *
22
+ * Its own file so `open.ts` stays inside the size budget, and because this is
23
+ * the thing every command in the CLI is written against — a reader looking for
24
+ * "what do I get" should not have to read the assembly to find it.
25
+ */
26
+
27
+ export interface Runtime {
28
+ readonly resolution: Resolution;
29
+ readonly config: Config;
30
+ readonly target: string;
31
+ /**
32
+ * The target's adapter set, from the workspace that declares it.
33
+ *
34
+ * Here because the config no longer carries it. Every caller that used to
35
+ * reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
36
+ * knowledge repository — reads this instead, and reads it without following
37
+ * the pointer a second time (ADR-052).
38
+ */
39
+ readonly declared: TargetConfig;
40
+ /**
41
+ * The accounts this profile selects, joined to the grants that govern them.
42
+ *
43
+ * The join is done once, here, because everything that asks — status, doctor,
44
+ * reconcile, setup — wants the same answer and three implementations of it is
45
+ * three chances to disagree (`profile/connections.ts`).
46
+ */
47
+ readonly connections: readonly SelectedConnection[];
48
+ /** Every account the workspace holds, granted or not. `connection list` reads this. */
49
+ readonly workspaceConnections: readonly ConnectionConfig[];
50
+ /** Vendors this workspace registered a client of its own for (`oauth_apps`). */
51
+ readonly ownClients: readonly string[];
52
+ readonly state: RuntimeState;
53
+ /** The durable log, for reading. Copies, if any, are write-only and not here. */
54
+ readonly audit: AuditReader;
55
+ readonly credentials: SecretStore;
56
+ readonly storage: BlobStore;
57
+ /**
58
+ * Where this profile's skills are kept — `data/skills.d/<connection>/` locally.
59
+ *
60
+ * `undefined` when the profile grants no `skills` connection. That is a real
61
+ * state rather than an empty one, and it is worth the optionality: the owner
62
+ * layer arrives granted (ADR-050), so a profile without it was denied it on
63
+ * purpose, and handing back an empty store would report "no skills" for a
64
+ * profile that is not allowed to have any — the same answer for two different
65
+ * facts.
66
+ */
67
+ readonly skills: BlobStore | undefined;
68
+ /**
69
+ * Present only when this target keeps memory and skills in a repository.
70
+ *
71
+ * Nothing on the dispatch path reads it: `storage` and `skills` above are
72
+ * already pointed at the right bytes, which is the whole design. It is here
73
+ * so `target show` and `doctor` can say where those bytes are without
74
+ * re-reading the config, and so the migration can reach the repository it is
75
+ * committing to.
76
+ */
77
+ readonly knowledge?: KnowledgeStores | undefined;
78
+ /** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
79
+ readonly vault: VaultStore;
80
+ readonly registry: ProviderRegistry;
81
+ /**
82
+ * Re-read the skills into the registry when they have changed on the store.
83
+ *
84
+ * Cheap and idempotent — one `list()`, and a rebuild only when the listing
85
+ * differs from the last one. The caller decides how often to ask.
86
+ */
87
+ refreshSkills(): Promise<void>;
88
+ readonly dispatcher: Dispatcher;
89
+ readonly authenticator: BearerAuthenticator;
90
+ /** Same factory the dispatcher uses, exposed for commands that probe upstream. */
91
+ connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
92
+ /**
93
+ * Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
94
+ *
95
+ * A command that probes upstream needs the credential on the request, and it
96
+ * must not learn how to put it there — that is one switch on the resolved
97
+ * shape (`connectivity/auth/authorize.ts`) and a second copy would be a
98
+ * second answer. `settleIdentity` is the caller: it asks a provider whose
99
+ * account was just authorised, over whatever method that provider declares.
100
+ */
101
+ authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
102
+ /** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
103
+ manifestFor(providerId: string): ProviderManifest | undefined;
104
+ close(): Promise<void>;
105
+ }
106
+
@@ -1,4 +1,5 @@
1
- import { layout, workspacePath } from '#profile';
1
+ import {
2
+ soleGrantFor, layout, vaultRef, workspacePath } from '#profile';
2
3
  import type { SecretStore } from '#secrets';
3
4
  import {
4
5
  createBlobVaultStore,
@@ -36,10 +37,15 @@ export function openVault(
36
37
  const { declared, config, root } = input;
37
38
  const vault = declared.vault ?? { adapter: 'file' as const };
38
39
 
40
+ // The vault connection this profile grants (ADR-059). `main` when it grants
41
+ // none, which keeps a profile that denied the vault opening against the same
42
+ // document every other profile uses rather than inventing a second one.
43
+ const connection = soleGrantFor(config, 'vault') ?? 'main';
44
+
39
45
  switch (vault.adapter) {
40
46
  case 'file':
41
47
  return createFileVaultStore({
42
- path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile)),
48
+ path: workspacePath(root, vault.path ?? layout.vault(connection)),
43
49
  });
44
50
 
45
51
  case 'secret':
@@ -47,15 +53,24 @@ export function openVault(
47
53
  // so the credential store holds ciphertext it cannot read. Separate
48
54
  // document, separate key, separate environment variable — the backend
49
55
  // was never what kept the two stores apart. ADR-022.
56
+ //
57
+ // Named per connection, like the file adapter. A `ref` written by hand
58
+ // still wins, because a deployment that already seals under one name has
59
+ // to keep opening it — but the default carries the instance, so two
60
+ // profiles granting different vaults share nothing. Without this only
61
+ // `file` honoured ADR-059 and every deployed workspace, which uses this
62
+ // adapter or `blob`, had one document behind every vault connection:
63
+ // ADR-059 calls that the worst of the three collisions, because the wrong
64
+ // answer is a credential.
50
65
  return createSecretVaultStore({
51
66
  store: credentials,
52
- ...(vault.ref !== undefined ? { ref: vault.ref } : {}),
67
+ ref: vaultRef(declared, config),
53
68
  });
54
69
 
55
70
  case 'blob':
56
71
  return createBlobVaultStore({
57
72
  store: storage(),
58
- ...(vault.path !== undefined ? { key: vault.path } : {}),
73
+ key: vault.path ?? layout.vaultKey(connection),
59
74
  });
60
75
  }
61
76
  }
@@ -0,0 +1,60 @@
1
+ import { ConfigError, listProfiles, openTarget, resolveWorkspaceRoot } from '#profile';
2
+ import { openRuntime, type Runtime } from './open.ts';
3
+ import type { GlobalFlags } from './select.ts';
4
+
5
+ /**
6
+ * A runtime for a workspace, when the command does not act on one profile.
7
+ *
8
+ * Contract 3 moved the credential store, the audit log, the state store and the
9
+ * blob root up to the workspace, so a command that opens any of them opens the
10
+ * same thing whichever profile it was handed. `openRuntime` still wants one,
11
+ * because a `Runtime` carries a resolved profile — so this picks one rather
12
+ * than making the caller ask a question with no answer.
13
+ *
14
+ * `--profile` still narrows where narrowing means something: the audit log is
15
+ * one chain and the flag filters it, rather than selecting which log to read.
16
+ * What it must never do is *decide* anything, which is why the profile chosen
17
+ * here when none is given is simply the first and is never reported as though
18
+ * it were the subject.
19
+ */
20
+ export async function openWorkspaceRuntime(flags: GlobalFlags): Promise<Runtime> {
21
+ if (flags.profile !== undefined) return openRuntime(flags);
22
+
23
+ const root = resolveWorkspaceRoot();
24
+ const resolved = await openTarget(root, flags.target!);
25
+ const names = await listProfiles(resolved.workspaceRoot);
26
+
27
+ if (names.length === 0) {
28
+ throw new ConfigError(
29
+ `Workspace "${flags.target}" holds no profiles, so there is nothing to open.\n` +
30
+ ` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
31
+ );
32
+ }
33
+
34
+ return openRuntime({ ...flags, profile: names[0]! });
35
+ }
36
+
37
+ /**
38
+ * Which profile a workspace-level command should act *as*.
39
+ *
40
+ * Distinct from `openWorkspaceRuntime` because the caller needs the name rather
41
+ * than a runtime — `start` puts it back into flags and hands them on. Same rule:
42
+ * `--profile` wins, and otherwise the first is taken without being reported as
43
+ * a choice, because for the things this is used for it is not one.
44
+ */
45
+ export async function primaryProfile(flags: GlobalFlags): Promise<string> {
46
+ if (flags.profile !== undefined) return flags.profile;
47
+
48
+ const root = resolveWorkspaceRoot();
49
+ const resolved = await openTarget(root, flags.target!);
50
+ const names = await listProfiles(resolved.workspaceRoot);
51
+
52
+ if (names.length === 0) {
53
+ throw new ConfigError(
54
+ `Workspace "${flags.target}" holds no profiles, so there is nothing to serve.\n` +
55
+ ` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
56
+ );
57
+ }
58
+
59
+ return names[0]!;
60
+ }
@@ -32,4 +32,5 @@ export {
32
32
  type OwnerLayerOptions,
33
33
  } from './runtime/registry.ts';
34
34
 
35
- export { openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
35
+ export { grantedConnections, openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
36
+ export { openWorkspaceRuntime, primaryProfile } from './runtime/workspace.ts';
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  listProfiles,
3
+ readWorkspace,
3
4
  noProfileNamed,
4
5
  noTargetNamed,
5
6
  readRegistry,
@@ -7,7 +8,7 @@ import {
7
8
  resolveWorkspaceRoot,
8
9
  } from '#profile';
9
10
  import type { Flags } from './argv.ts';
10
- import { dispatchWillRefuse, requirementFor } from './selection.ts';
11
+ import { EXPLICIT_WORKSPACE, dispatchWillRefuse, requirementFor, selectionKey } from './selection.ts';
11
12
  import { refuseIfUnmigrated } from './workspace-migrate.ts';
12
13
 
13
14
  /**
@@ -46,26 +47,40 @@ export async function requireSelection(
46
47
 
47
48
  const root = resolveWorkspaceRoot(env ? { env } : {});
48
49
 
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);
50
+ // **Workspace before profile, for both levels.** It used to ask for the
51
+ // profile and read that profile's own target list — the ordering ADR-052
52
+ // inverted, since a profile lives inside a workspace and there is nowhere to
53
+ // look for one until the workspace is known. `refuseIfUnmigrated` is on the
54
+ // refusal path only, so it costs nothing when the flag is present.
55
+ if (typeof flags['workspace'] !== 'string') {
56
+ const fallback = EXPLICIT_WORKSPACE.has(selectionKey(first, second))
57
+ ? undefined
58
+ : (await readWorkspace(root))?.default_workspace;
59
+
60
+ if (fallback === undefined) {
61
+ await refuseIfUnmigrated(root);
62
+ throw noTargetNamed(await readRegistry(root), root, env, {
63
+ refusedDefault: EXPLICIT_WORKSPACE.has(selectionKey(first, second)),
64
+ });
65
+ }
66
+
67
+ // Written back onto the flags so every command downstream reads one key and
68
+ // cannot tell a default from a typed flag — the difference belongs in the
69
+ // line printed above the output, not in thirty call sites (ADR-061).
70
+ flags['workspace'] = fallback;
71
+ defaulted.add(fallback);
57
72
  }
58
73
 
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);
74
+ // A named workspace the registry cannot know, because it does not exist yet.
75
+ if (!(flags['workspace'] in (await readRegistry(root)))) await refuseIfUnmigrated(root);
61
76
 
62
- if (needs === 'target') return;
77
+ if (needs === 'workspace') return;
63
78
 
64
79
  if (typeof flags['profile'] !== 'string') {
65
80
  // Listed from the target's own workspace, so the names offered are ones that
66
81
  // command could act on. An unreachable pointer degrades to the empty list
67
82
  // rather than failing — "which profile" is still the question being asked.
68
- const where = await resolveTargetWorkspace(root, flags['target']).catch(() => root);
83
+ const where = await resolveTargetWorkspace(root, flags['workspace'] as string).catch(() => root);
69
84
  throw noProfileNamed(where, await listProfiles(where).catch(() => []), env);
70
85
  }
71
86
  }
@@ -77,3 +92,19 @@ export async function requireSelection(
77
92
  * that listing it per command would be noise. `--quiet` is read by `announce`
78
93
  * rather than by any one command.
79
94
  */
95
+
96
+ /**
97
+ * Workspaces resolved from `default_workspace` rather than from a flag.
98
+ *
99
+ * A set rather than a boolean because `announce` is called from thirty places
100
+ * and none of them threads a resolution result through — and because the thing
101
+ * being remembered is small, true for one run, and read exactly once per line
102
+ * of output. If the echo is ever dropped for tidiness, ADR-061 has been
103
+ * reversed: the printed line is the whole of what makes a sticky default
104
+ * different from the dotfile nothing prints.
105
+ */
106
+ const defaulted = new Set<string>();
107
+
108
+ export function wasDefaulted(workspace: string): boolean {
109
+ return defaulted.has(workspace);
110
+ }