@lanes-sh/link 0.7.2 → 0.9.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 (190) 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 +148 -73
  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 +85 -4
  17. package/src/auth/remote.ts +32 -9
  18. package/src/cli/accepts.ts +109 -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 +90 -88
  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 +16 -6
  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 +183 -165
  40. package/src/cli/commands/grant.ts +140 -0
  41. package/src/cli/commands/identity.ts +24 -12
  42. package/src/cli/commands/knowledge/index.ts +49 -89
  43. package/src/cli/commands/knowledge/migrate.ts +79 -13
  44. package/src/cli/commands/knowledge/show.ts +97 -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 +33 -13
  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 +53 -5
  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/assets.ts +2 -2
  66. package/src/cli/commands/owner/entities.ts +2 -2
  67. package/src/cli/commands/owner/memory.ts +2 -2
  68. package/src/cli/commands/owner/shared.ts +13 -2
  69. package/src/cli/commands/owner/skills.ts +28 -8
  70. package/src/cli/commands/owner/tasks.ts +2 -2
  71. package/src/cli/commands/owner/vault.ts +3 -3
  72. package/src/cli/commands/profile/disposition.ts +236 -0
  73. package/src/cli/commands/profile/removal.ts +154 -64
  74. package/src/cli/commands/profile/remove.ts +83 -7
  75. package/src/cli/commands/profile.ts +79 -16
  76. package/src/cli/commands/relabel.ts +112 -0
  77. package/src/cli/commands/secrets.ts +39 -17
  78. package/src/cli/commands/set-workspace.ts +96 -0
  79. package/src/cli/commands/setup.ts +2 -2
  80. package/src/cli/commands/sync.ts +8 -8
  81. package/src/cli/commands/target.ts +9 -7
  82. package/src/cli/commands/update-migration.ts +54 -0
  83. package/src/cli/commands/update.ts +78 -24
  84. package/src/cli/config-edit.ts +99 -143
  85. package/src/cli/config-migrate.ts +82 -64
  86. package/src/cli/config-repair-sweep.ts +119 -0
  87. package/src/cli/config-repair.ts +131 -125
  88. package/src/cli/config-templates.ts +200 -0
  89. package/src/cli/contract3-credentials.ts +294 -0
  90. package/src/cli/contract3-data.ts +262 -0
  91. package/src/cli/contract3-layout.ts +46 -0
  92. package/src/cli/contract3-shape.ts +212 -0
  93. package/src/cli/contract3.ts +399 -0
  94. package/src/cli/contract4-credentials.ts +207 -0
  95. package/src/cli/contract4-data.ts +399 -0
  96. package/src/cli/contract4-rename.ts +73 -0
  97. package/src/cli/contract4-yaml.ts +223 -0
  98. package/src/cli/contract4.ts +342 -0
  99. package/src/cli/endpoint-url.ts +1 -1
  100. package/src/cli/identity.ts +44 -26
  101. package/src/cli/lanes.ts +25 -1
  102. package/src/cli/main.ts +94 -14
  103. package/src/cli/migrate-move.ts +166 -0
  104. package/src/cli/migrate-plan.ts +12 -6
  105. package/src/cli/output.ts +34 -1
  106. package/src/cli/publish.ts +6 -7
  107. package/src/cli/runtime/open.ts +64 -99
  108. package/src/cli/runtime/registry.ts +6 -7
  109. package/src/cli/runtime/select.ts +2 -11
  110. package/src/cli/runtime/stores.ts +58 -0
  111. package/src/cli/runtime/types.ts +106 -0
  112. package/src/cli/runtime/vault.ts +19 -4
  113. package/src/cli/runtime/workspace.ts +60 -0
  114. package/src/cli/runtime.ts +2 -1
  115. package/src/cli/selection-require.ts +44 -13
  116. package/src/cli/selection.ts +127 -145
  117. package/src/cli/usage.ts +40 -20
  118. package/src/cli/workspace-migrate.ts +152 -22
  119. package/src/connectivity/manifest/provider.ts +34 -13
  120. package/src/connectivity/manifest/requirements.ts +1 -1
  121. package/src/connectivity/transports/imap/parser.ts +70 -9
  122. package/src/deployments/adapters/filesystem.ts +18 -3
  123. package/src/deployments/bind.ts +1 -1
  124. package/src/deployments/deploy.ts +38 -29
  125. package/src/deployments/gcp/bucket.ts +58 -11
  126. package/src/deployments/gcp/provision.ts +7 -7
  127. package/src/deployments/knowledge.ts +9 -4
  128. package/src/deployments/prepare.ts +72 -24
  129. package/src/deployments/record.ts +1 -1
  130. package/src/deployments/report.ts +2 -2
  131. package/src/deployments/serving.ts +15 -74
  132. package/src/deployments/target.ts +34 -13
  133. package/src/deployments/upload.ts +60 -27
  134. package/src/dispatch/deps.ts +88 -0
  135. package/src/dispatch/dispatch.ts +21 -62
  136. package/src/policy/index.ts +47 -15
  137. package/src/profile/connections.ts +195 -0
  138. package/src/profile/deployments.ts +86 -8
  139. package/src/profile/index.ts +35 -6
  140. package/src/profile/knowledge.ts +18 -5
  141. package/src/profile/layout.ts +163 -90
  142. package/src/profile/load.ts +133 -64
  143. package/src/profile/pairing.ts +32 -0
  144. package/src/profile/primitives.ts +35 -1
  145. package/src/profile/registry.ts +6 -6
  146. package/src/profile/schema.ts +181 -21
  147. package/src/profile/targets.ts +21 -9
  148. package/src/profile/testing.ts +104 -2
  149. package/src/profile/workspace.ts +124 -33
  150. package/src/providers/assets/provider.ts +6 -6
  151. package/src/providers/custom/index.ts +1 -1
  152. package/src/providers/custom/load.ts +2 -3
  153. package/src/providers/entities/provider.ts +6 -6
  154. package/src/providers/entities/writes.ts +1 -1
  155. package/src/providers/identity/provider.ts +2 -2
  156. package/src/providers/memory/provider.ts +26 -8
  157. package/src/providers/setup/plan.ts +1 -1
  158. package/src/providers/setup/provider.ts +3 -3
  159. package/src/providers/skills/provider.ts +2 -2
  160. package/src/providers/slack/index.ts +2 -2
  161. package/src/providers/tasks/provider.ts +6 -6
  162. package/src/providers/vault/provider.ts +1 -1
  163. package/src/registry/policy-bridge.ts +33 -11
  164. package/src/registry/reconcile.ts +4 -4
  165. package/src/registry/registry.ts +1 -1
  166. package/src/server/authorization.ts +94 -0
  167. package/src/server/edge.ts +14 -1
  168. package/src/server/endpoint.ts +89 -104
  169. package/src/server/generation.ts +10 -1
  170. package/src/server/harness.ts +71 -13
  171. package/src/server/index.ts +31 -0
  172. package/src/server/mcp/build.ts +20 -1
  173. package/src/server/mcp/client-info.ts +54 -0
  174. package/src/server/mcp/guide.ts +120 -0
  175. package/src/server/mcp/instructions.ts +22 -22
  176. package/src/server/mcp/prompts.ts +7 -3
  177. package/src/server/mcp/resources.ts +16 -8
  178. package/src/server/mcp/routing.ts +3 -3
  179. package/src/server/mcp/tools.ts +25 -6
  180. package/src/server/mcp/visibility.ts +74 -7
  181. package/src/server/oauth.ts +29 -109
  182. package/src/server/read/credential.ts +134 -0
  183. package/src/server/read/deployed.ts +56 -0
  184. package/src/server/read/listener.ts +54 -0
  185. package/src/server/read/open.ts +101 -0
  186. package/src/server/read/routes.ts +247 -0
  187. package/src/server/read/state.ts +171 -0
  188. package/src/stores/blobs/conformance.ts +19 -0
  189. package/src/stores/state/index.ts +76 -10
  190. package/src/stores/state/testing.ts +5 -1
@@ -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
+ }
@@ -1,8 +1,10 @@
1
1
  import { ConfigError } from '#profile';
2
2
  import type { Flags } from './argv.ts';
3
- import { CONNECT_CUSTOM_FLAGS } from './commands/connect/custom/spec.ts';
3
+ import { ACCEPTS } from './accepts.ts';
4
4
  import { nearest } from './nearest.ts';
5
5
 
6
+ export { ACCEPTS } from './accepts.ts';
7
+
6
8
  /**
7
9
  * Which commands must name a profile and a target, and which flags each accepts.
8
10
  *
@@ -10,7 +12,7 @@ import { nearest } from './nearest.ts';
10
12
  * one makes the other legible.
11
13
  *
12
14
  * **A flag that is silently ignored is the defect.** `lanes link profile add
13
- * work --target cloud` printed `ok` and dropped the flag: `main.ts` built a
15
+ * work --workspace cloud` printed `ok` and dropped the flag: `main.ts` built a
14
16
  * literal for that command and never spread the global flags into it. Nothing
15
17
  * refused, because nothing had a list of what the command accepts. That is what
16
18
  * `assertKnownFlags` is — and it matters more than the requirement, because
@@ -39,7 +41,7 @@ import { nearest } from './nearest.ts';
39
41
  * there is no file to read. The level is gone rather than left empty, so nobody
40
42
  * adds a sixth command to a level that cannot resolve.
41
43
  */
42
- export type Requires = 'none' | 'target' | 'profile+target';
44
+ export type Requires = 'none' | 'workspace' | 'profile+workspace';
43
45
 
44
46
  /**
45
47
  * The rule, per command path.
@@ -54,7 +56,7 @@ export type Requires = 'none' | 'target' | 'profile+target';
54
56
  * `check`, `config show` and `policy list` take no `--target`. All three are
55
57
  * target-independent — a YAML file, the whole of it, and a policy block that is
56
58
  * declared once and applies everywhere. Demanding a target would be the
57
- * ceremony that teaches people to type `--target local` without reading it,
59
+ * ceremony that teaches people to type `--workspace local` without reading it,
58
60
  * which is how a required flag stops being a guard.
59
61
  *
60
62
  * `target list` takes no required `--target` either, and that is not an
@@ -85,55 +87,81 @@ export const SELECTION: Record<string, Requires> = {
85
87
  // `lanes link profile` is `profile list`, and needs the same as it.
86
88
  mcp: 'none',
87
89
  'profile default': 'none',
90
+ 'workspace use': 'none',
88
91
  'target use': 'none',
89
92
  'vault key': 'none',
90
93
  // Listing the registry is what you run to find out what `--target` accepts, so
91
94
  // requiring the answer as input would be circular (ADR-052).
95
+ workspace: 'none',
96
+ // The old word, kept for one minor (ADR-061).
92
97
  target: 'none',
98
+ 'workspace list': 'none',
93
99
  'target list': 'none',
94
100
 
95
101
  // A profile lives in one target's workspace, so listing or creating one names
96
102
  // 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',
103
+ profile: 'workspace',
104
+ 'profile list': 'workspace',
105
+ 'profile add': 'workspace',
106
+ 'profile remove': 'workspace',
107
+ // Editing who may consume one profile, so it names the profile.
108
+ 'profile members': 'profile+workspace',
109
+ 'target show': 'workspace',
102
110
 
103
111
  // These read one profile's file and open nothing. The target is what says
104
112
  // 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',
113
+ check: 'profile+workspace',
114
+ config: 'profile+workspace',
115
+ 'config show': 'profile+workspace',
116
+ policy: 'profile+workspace',
117
+ 'policy list': 'profile+workspace',
118
+ identity: 'profile+workspace',
119
+ 'identity list': 'profile+workspace',
120
+ 'secrets push': 'profile+workspace',
113
121
 
114
- connect: 'profile+target',
115
- // Both edit the profile config, and `disconnect` also opens the target's
116
- // credential store to delete from it. Same requirement as `connect` for the
117
- // same reasons.
118
- disconnect: 'profile+target',
119
- relabel: 'profile+target',
122
+ // A connection belongs to the workspace (ADR-057), so connecting does not
123
+ // name a profile. `--profile` is what *also* grants it there, which is the
124
+ // second of the two acts and the one that is a profile's — without it the
125
+ // account is authorised and granted to nobody, and `connect` says how to
126
+ // grant it.
127
+ connect: 'workspace',
128
+ // Workspace-scoped: it answers "what has this workspace authorised", and a
129
+ // profile would narrow the column rather than the rows.
130
+ connection: 'workspace',
131
+ 'connection list': 'workspace',
132
+ grant: 'profile+workspace',
133
+ revoke: 'profile+workspace',
134
+ // Both act on a connection, which belongs to the workspace (ADR-057).
135
+ // `disconnect` already reaches every profile that granted it and `label` is a
136
+ // field on the connection row, so neither had a profile to be told — naming
137
+ // one described a slice of a change that was never scoped to it. `--profile`
138
+ // still narrows what is listed back.
139
+ disconnect: 'workspace',
140
+ relabel: 'workspace',
120
141
  // Its own row rather than an inheritance from `connect`. Both need the same
121
142
  // two things, but the row is what makes `selectionKey` return the two-word
122
143
  // key — and that is what keeps thirty declaration flags off
123
144
  // `connect <provider>`, where a mistyped one would otherwise be accepted and
124
145
  // ignored, which is the defect this whole file exists for.
125
- 'connect custom': 'profile+target',
126
- setup: 'profile+target',
127
- token: 'profile+target',
128
- audit: 'profile+target',
129
- secrets: 'profile+target',
130
- plan: 'profile+target',
131
- doctor: 'profile+target',
132
- auth: 'profile+target',
146
+ 'connect custom': 'workspace',
147
+ setup: 'profile+workspace',
148
+ token: 'profile+workspace',
149
+ // One chain per workspace since contract 3, so the workspace is the subject
150
+ // and `--profile` filters the rows rather than choosing which log to read.
151
+ audit: 'workspace',
152
+ // One credential store per workspace since contract 3, so every profile
153
+ // opened the same one.
154
+ secrets: 'workspace',
155
+ plan: 'profile+workspace',
156
+ doctor: 'profile+workspace',
157
+ // Whether an account can still sign in is a fact about the account. Scoped to
158
+ // a profile's grants it could not check one that had just been connected,
159
+ // which is when you most want to.
160
+ auth: 'workspace',
133
161
  // Target-scoped: see the note above. `--profile` narrows each to one profile.
134
- status: 'target',
135
- outputs: 'profile+target',
136
- tools: 'profile+target',
162
+ status: 'workspace',
163
+ outputs: 'profile+workspace',
164
+ tools: 'profile+workspace',
137
165
  // It resolves nothing and opens nothing — it hands macOS a URL (ADR-053).
138
166
  // `target list` is the precedent for a `'none'` command that still takes a
139
167
  // flag of its own. Both spellings need a row: `selection.test.ts` reads
@@ -141,38 +169,48 @@ export const SELECTION: Record<string, Requires> = {
141
169
  // the `profile+target` default.
142
170
  dashboard: 'none',
143
171
  desktop: 'none',
144
- attach: 'profile+target',
145
- start: 'profile+target',
146
- deploy: 'target',
172
+ attach: 'profile+workspace',
173
+ // One endpoint serves every profile in the workspace (ADR-009), so naming one
174
+ // described a slice of what it does. `--profile` picks the primary, whose
175
+ // token opens it, and `--only` is what narrows what is served.
176
+ start: 'workspace',
177
+ deploy: 'workspace',
147
178
  // Both spellings: `sync` alone is `sync targets`, which is the only thing
148
179
  // there is to sync, and naming it leaves room for the next one.
149
- sync: 'target',
150
- 'sync targets': 'target',
151
- 'policy allow': 'profile+target',
152
- 'policy deny': 'profile+target',
180
+ sync: 'workspace',
181
+ 'sync targets': 'workspace',
182
+ 'sync workspaces': 'workspace',
183
+ 'policy allow': 'profile+workspace',
184
+ 'policy deny': 'profile+workspace',
153
185
  // Both, unlike `identity list`, and for the same reason the policy edits are:
154
186
  // each publishes the edit, which opens the target's credential store and
155
187
  // reaches that target's endpoint.
156
- 'identity add': 'profile+target',
157
- 'identity remove': 'profile+target',
158
- 'token show': 'profile+target',
159
- 'token rotate': 'profile+target',
160
- 'audit tail': 'profile+target',
161
- 'audit verify': 'profile+target',
162
- 'secrets set': 'profile+target',
163
- 'secrets list': 'profile+target',
164
- 'mcp add': 'profile+target',
165
- 'mcp stdio': 'profile+target',
166
- memory: 'profile+target',
167
- tasks: 'profile+target',
168
- assets: 'profile+target',
169
- skills: 'profile+target',
170
- vault: 'profile+target',
171
- entities: 'profile+target',
188
+ 'identity add': 'profile+workspace',
189
+ 'identity remove': 'profile+workspace',
190
+ 'mcp install-instructions': 'none',
191
+ // What it pairs is the workspace: the surface it opens lists every connection
192
+ // and profile there, and the credential it mints reads all of them. Asking
193
+ // which profile was a question with no answer. `--profile` still narrows, and
194
+ // is how a port is chosen when profiles disagree about one.
195
+ pair: 'workspace',
196
+ 'token show': 'profile+workspace',
197
+ 'token rotate': 'profile+workspace',
198
+ 'audit tail': 'workspace',
199
+ 'audit verify': 'workspace',
200
+ 'secrets set': 'workspace',
201
+ 'secrets list': 'workspace',
202
+ 'mcp add': 'profile+workspace',
203
+ 'mcp stdio': 'profile+workspace',
204
+ memory: 'profile+workspace',
205
+ tasks: 'profile+workspace',
206
+ assets: 'profile+workspace',
207
+ skills: 'profile+workspace',
208
+ vault: 'profile+workspace',
209
+ entities: 'profile+workspace',
172
210
  // Both halves open the target's adapters — `show` counts what is in the
173
211
  // stores, and `use` migrates between them — and both edit the profile's
174
212
  // config. Neither can be answered without being told which.
175
- knowledge: 'profile+target',
213
+ knowledge: 'profile+workspace',
176
214
  };
177
215
 
178
216
  /**
@@ -186,7 +224,9 @@ export const SELECTION: Record<string, Requires> = {
186
224
  * asserts this stays true.
187
225
  */
188
226
  const SUBCOMMANDS: Record<string, readonly string[]> = {
189
- profile: ['add', 'list', 'default', 'remove'],
227
+ profile: ['add', 'list', 'default', 'remove', 'members'],
228
+ connection: ['list'],
229
+ workspace: ['list', 'use', 'show'],
190
230
  target: ['list', 'use', 'show'],
191
231
  policy: ['list', 'allow', 'deny'],
192
232
  identity: ['add', 'list', 'remove'],
@@ -205,7 +245,7 @@ const SUBCOMMANDS: Record<string, readonly string[]> = {
205
245
  mcp: ['skill', 'add', 'stdio', 'list'],
206
246
  secrets: ['push', 'set', 'list'],
207
247
  knowledge: ['show', 'use'],
208
- sync: ['targets'],
248
+ sync: ['targets', 'workspaces'],
209
249
  };
210
250
 
211
251
  /**
@@ -235,99 +275,41 @@ export function selectionKey(first: string, second: string | undefined): string
235
275
 
236
276
  /** Whether this command needs a profile, a target, both, or neither. */
237
277
  export function requirementFor(first: string, second: string | undefined): Requires {
238
- return SELECTION[selectionKey(first, second)] ?? 'profile+target';
278
+ return SELECTION[selectionKey(first, second)] ?? 'profile+workspace';
239
279
  }
240
280
 
241
- const UNIVERSAL = ['help', 'json', 'quiet'];
242
-
243
281
  /**
244
- * What each command accepts beyond the universal set and its own selection.
282
+ * The commands that refuse `default_workspace` and make you type the name.
283
+ *
284
+ * ADR-037 removed every implicit selection, on reasoning worth keeping: a
285
+ * persisted context is the standard way an operator runs a destructive command
286
+ * against the wrong thing. ADR-061 gives the default back for the forty
287
+ * commands a day that only read, and keeps the refusal exactly where that
288
+ * argument bites — anything that publishes or destroys.
245
289
  *
246
- * Only commands with flags of their own appear. Anything absent accepts the
247
- * universal set plus whatever `SELECTION` says it must be told.
290
+ * `connect` is deliberately absent. It creates rather than destroys, it is the
291
+ * command someone runs while learning the tool, and it prints the workspace it
292
+ * wrote to. Requiring a flag there is the ceremony ADR-043 identified as the way
293
+ * a required flag stops being a guard.
248
294
  */
249
- const ACCEPTS: Record<string, readonly string[]> = {
250
- // Imported rather than written out. Thirty-odd entries here would take this
251
- // file past the size budget for a data literal, and the command's own
252
- // `spec.ts` already derives most of them from the per-kind field tables — so
253
- // a flag added there cannot be forgotten here.
254
- 'connect custom': CONNECT_CUSTOM_FLAGS,
255
- // `own-client` is the older spelling of one of the routes `auth` names, kept
256
- // because it is in scripts and a year of documentation (ADR-038).
257
- connect: [
258
- 'id',
259
- 'display-name',
260
- // Repeatable: `--set host=cloud.example.com`. The only way to give a
261
- // provider its address without a terminal to ask at.
262
- 'set',
263
- 'label',
264
- 'replace',
265
- 'non-interactive',
266
- 'accept-broad-scopes',
267
- 'own-client',
268
- 'auth',
269
- ],
270
- setup: ['id'],
271
- 'profile add': ['target', 'non-interactive'],
272
- // `--target` decommissions one target's stores and leaves the profile file in
273
- // place (`removal.ts`). It is documented in `usage.ts` and read by
274
- // `removalPlan`, and was refused here — the flag existed everywhere except in
275
- // the list that decides whether it may be typed.
276
- 'profile remove': ['dry-run', 'yes', 'target'],
277
- disconnect: ['yes', 'keep-credential'],
278
- // The one repair `doctor` can apply rather than only name. Narrow on purpose:
279
- // it undoes a provider rename this project shipped, and every other finding
280
- // there is something only the operator can decide.
281
- doctor: ['fix'],
282
- // A filter, not a second subject: it narrows the answer to one connection so
283
- // a caller can re-ask about the row it just repaired. Same shape as `attach`.
284
- auth: ['connection'],
285
- relabel: [],
286
- 'target list': ['urls', 'target'],
287
- 'target show': ['target'],
288
- 'token show': ['show', 'raw'],
289
- 'token rotate': ['show', 'raw', 'yes'],
290
- 'audit tail': ['limit', 'denied-only', 'format'],
291
- 'audit verify': ['limit', 'format'],
292
- attach: ['connection'],
293
- outputs: ['show'],
294
- start: ['port', 'only'],
295
- 'mcp stdio': ['only'],
296
- 'mcp add': ['name', 'scope', 'token-env', 'dry-run', 'force', 'no-skill'],
297
- 'mcp skill': ['print', 'force'],
298
- 'mcp list': ['name', 'scope'],
299
- // `--yes` because it installs the app when nothing answers the scheme, and
300
- // that is the one prompt in this CLI that puts an application on the machine.
301
- dashboard: ['print', 'yes'],
302
- desktop: ['print', 'yes'],
303
- skill: ['print', 'force'],
304
- deploy: ['dry-run', 'iam', 'access', 'service-account', 'tag', 'yes', 'non-interactive'],
305
- 'secrets push': ['from', 'to', 'overwrite', 'dry-run'],
306
- sync: ['dry-run', 'from', 'discover', 'prefer'],
307
- 'sync targets': ['dry-run', 'from', 'discover', 'prefer'],
308
- update: ['check'],
309
- 'identity add': ['note'],
310
- memory: ['connection', 'title', 'description', 'file', 'tag'],
311
- // `--yes` on both, because both have a delete that asks first.
312
- tasks: ['connection', 'title', 'status', 'due', 'tag', 'yes'],
313
- assets: ['connection', 'name', 'content-type', 'yes'],
314
- skills: ['connection', 'title', 'description', 'file'],
315
- vault: ['connection'],
316
- // `alias`, `attr` and `related` are repeatable — see `ownerFlags`. `name`
317
- // overrides the id derived from the positional name, which is how you get
318
- // `acme-bv` rather than `acme-b-v`.
319
- entities: ['connection', 'type', 'name', 'alias', 'attr', 'related', 'tag', 'yes'],
320
- // `no-migrate` is listed beside `migrate` because they are three states
321
- // rather than two: neither one asks, and a run with no terminal has to be
322
- // able to say which it meant (ADR-041).
323
- knowledge: ['repo', 'branch', 'path', 'migrate', 'no-migrate', 'keep', 'allow-public', 'replace', 'yes'],
324
- };
295
+ export const EXPLICIT_WORKSPACE = new Set([
296
+ 'deploy',
297
+ 'sync',
298
+ 'sync targets',
299
+ 'sync workspaces',
300
+ 'secrets push',
301
+ 'profile remove',
302
+ 'disconnect',
303
+ 'token rotate',
304
+ ]);
305
+
306
+ const UNIVERSAL = ['help', 'json', 'quiet'];
325
307
 
326
308
  /**
327
309
  * Refuse a flag this command does not read, and guess what was meant.
328
310
  *
329
311
  * This is the fix for the reported bug rather than a nicety. `profile add
330
- * --target cloud` was accepted and dropped, and nothing could refuse it because
312
+ * --workspace cloud` was accepted and dropped, and nothing could refuse it because
331
313
  * `parseArgv` returns every `--anything` it sees and no command ever inspected
332
314
  * the leftovers. A typo was swallowed the same way on every command in the CLI.
333
315
  */
@@ -345,7 +327,7 @@ export function assertKnownFlags(first: string, second: string | undefined, flag
345
327
  if (dispatchWillRefuse(first, second)) return;
346
328
 
347
329
  const key = selectionKey(first, second);
348
- const needs = SELECTION[key] ?? 'profile+target';
330
+ const needs = SELECTION[key] ?? 'profile+workspace';
349
331
 
350
332
  const allowed = new Set<string>([
351
333
  ...UNIVERSAL,
@@ -353,7 +335,7 @@ export function assertKnownFlags(first: string, second: string | undefined, flag
353
335
  // `target` accepts both: the target is what it acts on, and `--profile`
354
336
  // narrows it to one of the profiles behind it.
355
337
  ...(needs !== 'none' && !POSITIONAL_PROFILE.has(key) ? ['profile'] : []),
356
- ...(needs === 'target' || needs === 'profile+target' ? ['target'] : []),
338
+ ...(needs === 'workspace' || needs === 'profile+workspace' ? ['workspace'] : []),
357
339
  ]);
358
340
 
359
341
  const named = [first, second].filter(Boolean).join(' ');
package/src/cli/usage.ts CHANGED
@@ -21,7 +21,8 @@ export const USAGE = `${style.bold(PROGRAM)} — a self-hostable MCP gateway for
21
21
  ${style.bold('Everyday')}
22
22
  ${PROGRAM} setup plan [--json] what each provider needs, and which are connected
23
23
  ${PROGRAM} setup plan <provider> the console steps, the values, and the command
24
- ${PROGRAM} connect <provider> add an account (run once per account)
24
+ ${PROGRAM} connect <provider> authorise an account into this workspace
25
+ (once per account, not once per profile)
25
26
  ${PROGRAM} connect <provider>.<id> re-authorise one existing account
26
27
  ${PROGRAM} connect <...> --replace ask for the stored password or key again
27
28
  ${PROGRAM} connect <...> --auth <method> pick how, where there is a choice
@@ -35,6 +36,9 @@ ${style.bold('Everyday')}
35
36
  asked for; the manifest it writes is yours to edit
36
37
  answer nothing from a terminal: take every value
37
38
  from the credential store, or say what is missing
39
+ ${PROGRAM} connection list [--json] every account in this workspace, and who grants it
40
+ ${PROGRAM} grant add <provider>.<id> let a profile reach one, with nothing allowed yet
41
+ ${PROGRAM} grant remove <provider>.<id>
38
42
  ${PROGRAM} start [--only] reconcile and serve every profile on one endpoint
39
43
  ${PROGRAM} outputs [--show] [--json] the endpoint an agent needs
40
44
  ${PROGRAM} desktop [--print] [--yes] open the Lanes app on its Lanes Link page,
@@ -45,24 +49,31 @@ ${style.bold('Everyday')}
45
49
  ${PROGRAM} mcp list where it is registered, and whether the skill is current
46
50
  ${PROGRAM} mcp stdio serve on stdin/stdout, for a client that spawns it
47
51
  ${PROGRAM} mcp skill [--print] the bundled skill — its path, or the document itself
52
+ ${PROGRAM} mcp install-instructions [--client claude|chatgpt|codex|cursor]
53
+ a block to paste where a client keeps its own
54
+ standing instructions, so it knows to look here
55
+ ${PROGRAM} pair [--print] [--rotate] let the Lanes dashboard read this machine
48
56
  ${PROGRAM} status [--json] connections, reachable capabilities, endpoint
49
57
 
50
58
  ${style.bold('Profiles')}
51
- ${PROGRAM} profile add <name> --target <name> [--target <name>] [--json]
59
+ ${PROGRAM} profile add <name> --workspace <name> [--workspace <name>] [--json]
52
60
  a target per place it runs; local is derived, the
53
61
  rest are copied from a sibling profile
54
62
  ${PROGRAM} profile list [--json]
55
- ${PROGRAM} profile remove <name> [--target t] [--dry-run] [--yes] [--json]
56
- the profile, its credentials, and its data
57
-
58
- ${style.bold('Targets')}
59
- ${PROGRAM} target list [--urls] where this profile can run
60
- ${PROGRAM} target show <name> one target's adapters, and the address it answers on
61
- ${PROGRAM} sync targets --target t [--from gs://bucket] [--discover]
63
+ ${PROGRAM} profile remove <name> [--workspace <name>] [--dry-run] [--yes] [--json]
64
+ [--delete-data | --migrate-to <profile>]
65
+ the profile and its own token. Say which of
66
+ --delete-data or --migrate-to for its memory,
67
+ tasks, assets and skills there is no default.
68
+ Accounts outlive it; disconnect removes those.
69
+
70
+ ${style.bold('Workspaces')}
71
+ ${PROGRAM} workspace list [--urls] every workspace this one knows
72
+ ${PROGRAM} workspace show <name> one workspace's adapters, and its address
73
+ ${PROGRAM} sync workspaces --workspace <name> [--from gs://bucket] [--discover]
62
74
  [--prefer local|remote] [--dry-run]
63
75
  reconcile this workspace with the copy the
64
- deployment reads; recovers a target a profile
65
- has lost
76
+ deployment reads; recovers one a profile has lost
66
77
 
67
78
  ${style.bold('Who you are')}
68
79
  ${PROGRAM} identity add <kind> <value> [--note text] [--json]
@@ -72,10 +83,15 @@ ${style.bold('Who you are')}
72
83
 
73
84
  ${style.bold('Permissions')}
74
85
  ${PROGRAM} policy list
75
- ${PROGRAM} policy allow <capability> e.g. gmail.* or gmail.send_message
76
- ${PROGRAM} policy deny <capability>
77
- ${PROGRAM} token show [--show|--raw] --raw prints only the token, for $(…)
78
- ${PROGRAM} token rotate [--show]
86
+ ${PROGRAM} policy allow <capability> --connection <provider>.<id>
87
+ e.g. gmail.* or gmail.send_message. A rule lands
88
+ in one grant, so two accounts can differ
89
+ ${PROGRAM} policy deny <capability> --connection <provider>.<id>
90
+ ${PROGRAM} profile members list who may consume this profile, and who could
91
+ ${PROGRAM} profile members add <subject>|--me [--role owner|member]
92
+ ${PROGRAM} profile members remove <subject>
93
+ ${PROGRAM} token show [--show|--raw] CI only: --raw prints only the token, for $(…)
94
+ ${PROGRAM} token rotate [--show] also ends every session a member holds
79
95
 
80
96
  ${style.bold('Your own context')}
81
97
  ${PROGRAM} memory list [--tag t] what you have stored
@@ -126,10 +142,10 @@ ${style.bold('Your own context')}
126
142
  ${PROGRAM} vault key generate a fresh LANES_LINK_VAULT_KEY, printed once
127
143
 
128
144
  ${style.bold('Deploying')}
129
- ${PROGRAM} deploy --target t [--dry-run]
145
+ ${PROGRAM} deploy --workspace <name> [--dry-run]
130
146
  set up, build, and roll one revision serving
131
147
  every profile that declares the target
132
- ${PROGRAM} deploy --target t --profile a --profile b
148
+ ${PROGRAM} deploy --workspace <name> --profile a --profile b
133
149
  only these; the first owns the endpoint token
134
150
  ${PROGRAM} deploy --non-interactive take the stored answers, never prompt
135
151
  ${PROGRAM} deploy --access iam|public who gets past the platform's own door
@@ -158,9 +174,13 @@ ${style.bold('Attachments')}
158
174
 
159
175
  ${style.bold('Naming what a command acts on')}
160
176
  --profile <name> required by every command that reads or writes a profile
161
- --target <name> required by every command that opens a target's stores.
162
- There is no default and no environment variable: a
163
- command that names neither refuses and lists what exists.
177
+ --workspace <name> required by every command that opens a workspace's
178
+ stores. "lanes set-workspace <name>" writes a default;
179
+ every command that uses it prints the name it resolved,
180
+ and deploy, sync, secrets push, profile remove,
181
+ disconnect and token rotate refuse it and want the flag.
182
+ --target <name> the old spelling of --workspace. Accepted for one
183
+ minor, and it warns.
164
184
 
165
185
  ${style.bold('Other flags')}
166
186
  --connection <id> which memory/tasks/assets/skills/vault/entities