@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.
- package/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +148 -73
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +85 -4
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +109 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +90 -88
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +16 -6
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +183 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +24 -12
- package/src/cli/commands/knowledge/index.ts +49 -89
- package/src/cli/commands/knowledge/migrate.ts +79 -13
- package/src/cli/commands/knowledge/show.ts +97 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +33 -13
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +53 -5
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/assets.ts +2 -2
- package/src/cli/commands/owner/entities.ts +2 -2
- package/src/cli/commands/owner/memory.ts +2 -2
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/owner/tasks.ts +2 -2
- package/src/cli/commands/owner/vault.ts +3 -3
- package/src/cli/commands/profile/disposition.ts +236 -0
- package/src/cli/commands/profile/removal.ts +154 -64
- package/src/cli/commands/profile/remove.ts +83 -7
- package/src/cli/commands/profile.ts +79 -16
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +39 -17
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update-migration.ts +54 -0
- package/src/cli/commands/update.ts +78 -24
- package/src/cli/config-edit.ts +99 -143
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair-sweep.ts +119 -0
- package/src/cli/config-repair.ts +131 -125
- package/src/cli/config-templates.ts +200 -0
- package/src/cli/contract3-credentials.ts +294 -0
- package/src/cli/contract3-data.ts +262 -0
- package/src/cli/contract3-layout.ts +46 -0
- package/src/cli/contract3-shape.ts +212 -0
- package/src/cli/contract3.ts +399 -0
- package/src/cli/contract4-credentials.ts +207 -0
- package/src/cli/contract4-data.ts +399 -0
- package/src/cli/contract4-rename.ts +73 -0
- package/src/cli/contract4-yaml.ts +223 -0
- package/src/cli/contract4.ts +342 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/identity.ts +44 -26
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +94 -14
- package/src/cli/migrate-move.ts +166 -0
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +6 -7
- package/src/cli/runtime/open.ts +64 -99
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/select.ts +2 -11
- package/src/cli/runtime/stores.ts +58 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +40 -20
- package/src/cli/workspace-migrate.ts +152 -22
- package/src/connectivity/manifest/provider.ts +34 -13
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/connectivity/transports/imap/parser.ts +70 -9
- package/src/deployments/adapters/filesystem.ts +18 -3
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +38 -29
- package/src/deployments/gcp/bucket.ts +58 -11
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/knowledge.ts +9 -4
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +34 -13
- package/src/deployments/upload.ts +60 -27
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +195 -0
- package/src/profile/deployments.ts +86 -8
- package/src/profile/index.ts +35 -6
- package/src/profile/knowledge.ts +18 -5
- package/src/profile/layout.ts +163 -90
- package/src/profile/load.ts +133 -64
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +181 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +104 -2
- package/src/profile/workspace.ts +124 -33
- package/src/providers/assets/provider.ts +6 -6
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/entities/provider.ts +6 -6
- package/src/providers/entities/writes.ts +1 -1
- package/src/providers/identity/provider.ts +2 -2
- package/src/providers/memory/provider.ts +26 -8
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/setup/provider.ts +3 -3
- package/src/providers/skills/provider.ts +2 -2
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/tasks/provider.ts +6 -6
- package/src/providers/vault/provider.ts +1 -1
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/registry/registry.ts +1 -1
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +89 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +22 -22
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/routing.ts +3 -3
- package/src/server/mcp/tools.ts +25 -6
- package/src/server/mcp/visibility.ts +74 -7
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
- package/src/stores/blobs/conformance.ts +19 -0
- package/src/stores/state/index.ts +76 -10
- package/src/stores/state/testing.ts +5 -1
package/src/cli/lanes.ts
CHANGED
|
@@ -21,13 +21,23 @@ const AREAS: Record<string, string> = {
|
|
|
21
21
|
link: 'a self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
/** Commands that belong to the binary rather than to any one area. */
|
|
25
|
+
const TOP_LEVEL: Record<string, string> = {
|
|
26
|
+
auth: 'sign in to Lanes, so a profile can say who may use it',
|
|
27
|
+
'set-workspace': 'which workspace a command means when it does not say',
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
function areasUsage(): string {
|
|
25
31
|
const rows = Object.entries(AREAS)
|
|
26
32
|
.map(([name, blurb]) => ` ${style.bold(`lanes ${name}`)} ${blurb}`)
|
|
27
33
|
.join('\n');
|
|
28
34
|
|
|
35
|
+
const top = Object.entries(TOP_LEVEL)
|
|
36
|
+
.map(([name, blurb]) => ` ${style.bold(`lanes ${name}`)} ${blurb}`)
|
|
37
|
+
.join('\n');
|
|
38
|
+
|
|
29
39
|
return (
|
|
30
|
-
`${style.bold('lanes')} — your own tools, wherever you work\n\n${rows}\n\n` +
|
|
40
|
+
`${style.bold('lanes')} — your own tools, wherever you work\n\n${rows}\n\n${top}\n\n` +
|
|
31
41
|
`Run ${style.bold('lanes <area> help')} for what an area can do, ` +
|
|
32
42
|
`or ${style.bold('lanes --version')} for which release this is.\n`
|
|
33
43
|
);
|
|
@@ -51,6 +61,20 @@ async function main(argv: readonly string[]): Promise<void> {
|
|
|
51
61
|
return;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
// A top-level command rather than an area, because it selects the workspace
|
|
65
|
+
// *the CLI* acts in — every area's commands read the same answer (ADR-061).
|
|
66
|
+
if (area === 'auth') {
|
|
67
|
+
const { runAuth } = await import('./commands/auth-dispatch.ts');
|
|
68
|
+
return await runAuth(rest);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (area === 'set-workspace') {
|
|
72
|
+
const { setWorkspace } = await import('./commands/set-workspace.ts');
|
|
73
|
+
const { parseArgv } = await import('./argv.ts');
|
|
74
|
+
const { command, flags } = parseArgv(rest);
|
|
75
|
+
return await setWorkspace(command[0], { json: flags['json'] === true });
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
if (area === 'link') {
|
|
55
79
|
// Loaded here rather than at the top of the file so that a failure while
|
|
56
80
|
// the area's module graph evaluates — a malformed environment variable read
|
package/src/cli/main.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { connect } from './commands/connect/index.ts';
|
|
2
2
|
import { connectCustom } from './commands/connect/custom/index.ts';
|
|
3
|
-
import { disconnect
|
|
3
|
+
import { disconnect } from './commands/connection.ts';
|
|
4
|
+
import { grantConnectionTo, revokeConnectionFrom } from './commands/grant.ts';
|
|
5
|
+
import { connectionList } from './commands/connection-list.ts';
|
|
6
|
+
import { membersAdd, membersList, membersRemove } from './commands/members.ts';
|
|
7
|
+
import { relabel } from './commands/relabel.ts';
|
|
4
8
|
import {
|
|
5
9
|
attachFile,
|
|
6
10
|
auditTail,
|
|
@@ -11,6 +15,7 @@ import {
|
|
|
11
15
|
desktop,
|
|
12
16
|
doctor,
|
|
13
17
|
outputs,
|
|
18
|
+
pair,
|
|
14
19
|
plan,
|
|
15
20
|
policyList,
|
|
16
21
|
policyRule,
|
|
@@ -26,13 +31,23 @@ import { syncTargets } from './commands/sync.ts';
|
|
|
26
31
|
import { targetList, targetShow, targetUse } from './commands/target.ts';
|
|
27
32
|
import { identityAdd, identityList, identityRemove } from './commands/identity.ts';
|
|
28
33
|
import { setupPlan } from './commands/setup.ts';
|
|
29
|
-
import { mcpAdd, mcpList, mcpStdio, skillDocument } from './commands/mcp.ts';
|
|
34
|
+
import { installInstructions, mcpAdd, mcpList, mcpStdio, skillDocument } from './commands/mcp.ts';
|
|
30
35
|
import { deploy } from '#deployments/deploy.ts';
|
|
31
36
|
import { secretsList, secretsPush, secretsSet } from './commands/secrets.ts';
|
|
32
37
|
import { knowledgeShow, knowledgeUse } from './commands/knowledge.ts';
|
|
33
38
|
import { dispatchOwner } from './dispatch-owner.ts';
|
|
34
39
|
import { update } from './commands/update.ts';
|
|
35
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
all,
|
|
42
|
+
customFlags,
|
|
43
|
+
globalFlags,
|
|
44
|
+
knowledgeFlags,
|
|
45
|
+
ownerFlags,
|
|
46
|
+
parseArgv,
|
|
47
|
+
text,
|
|
48
|
+
} from './argv.ts';
|
|
49
|
+
import type { GlobalFlags } from './runtime.ts';
|
|
50
|
+
import type { OwnerFlags } from './commands/owner/shared.ts';
|
|
36
51
|
import { assertKnownFlags } from './selection.ts';
|
|
37
52
|
import { requireSelection } from './selection-require.ts';
|
|
38
53
|
import { PROGRAM, USAGE } from './usage.ts';
|
|
@@ -57,8 +72,12 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
57
72
|
const { command, flags } = parseArgv(argv);
|
|
58
73
|
const [first, second, ...rest] = command;
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
// Both are read *after* `requireSelection`, which is what resolves
|
|
76
|
+
// `default_workspace` onto `flags` (ADR-061). Capturing them before it would
|
|
77
|
+
// hand every command an undefined workspace on the path the default exists to
|
|
78
|
+
// serve — the flag would be resolved and nothing would be looking.
|
|
79
|
+
let global: GlobalFlags;
|
|
80
|
+
let owner: OwnerFlags;
|
|
62
81
|
|
|
63
82
|
const show = flags['show'] === true;
|
|
64
83
|
const raw = flags['raw'] === true;
|
|
@@ -77,6 +96,9 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
77
96
|
assertKnownFlags(first, second, flags);
|
|
78
97
|
await requireSelection(first, second, flags);
|
|
79
98
|
|
|
99
|
+
global = globalFlags(flags);
|
|
100
|
+
owner = ownerFlags(flags, argv);
|
|
101
|
+
|
|
80
102
|
switch (first) {
|
|
81
103
|
case 'connect':
|
|
82
104
|
// A nested switch rather than an `if`, so `selection.test.ts` sees
|
|
@@ -117,6 +139,20 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
117
139
|
});
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
// The command connecting no longer implies. A connection belongs to the
|
|
143
|
+
// workspace (ADR-057), so which profiles may reach it is a separate say.
|
|
144
|
+
case 'connection':
|
|
145
|
+
if (second !== 'list' && second !== undefined) {
|
|
146
|
+
throw new Error(`Unknown: ${PROGRAM} connection ${second}`);
|
|
147
|
+
}
|
|
148
|
+
return connectionList({ ...global, json });
|
|
149
|
+
|
|
150
|
+
case 'grant':
|
|
151
|
+
return grantConnectionTo(second, { ...global, json });
|
|
152
|
+
|
|
153
|
+
case 'revoke':
|
|
154
|
+
return revokeConnectionFrom(second, { ...global, json });
|
|
155
|
+
|
|
120
156
|
case 'setup':
|
|
121
157
|
if (second !== 'plan' && second !== undefined) {
|
|
122
158
|
throw new Error(`Unknown: ${PROGRAM} setup ${second}`);
|
|
@@ -127,27 +163,47 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
127
163
|
switch (second) {
|
|
128
164
|
case 'add':
|
|
129
165
|
if (!rest[0]) {
|
|
130
|
-
throw new Error(`Usage: ${PROGRAM} profile add <name> --
|
|
166
|
+
throw new Error(`Usage: ${PROGRAM} profile add <name> --workspace <name>`);
|
|
131
167
|
}
|
|
132
168
|
return profileAdd(rest[0], {
|
|
133
169
|
// One target, not a list. A profile declared every target it could
|
|
134
170
|
// run on under contract 1, which is why this read the repeated flag
|
|
135
171
|
// out of argv; it lives in exactly one now (ADR-052), so a second
|
|
136
172
|
// --target would be naming a second place to put the same file.
|
|
137
|
-
targets: [text(flags, '
|
|
173
|
+
targets: [text(flags, 'workspace')!],
|
|
138
174
|
nonInteractive: flags['non-interactive'] === true,
|
|
139
175
|
json,
|
|
140
176
|
});
|
|
141
177
|
case 'list':
|
|
142
178
|
case undefined:
|
|
143
|
-
return profileList(text(flags, '
|
|
179
|
+
return profileList(text(flags, 'workspace')!, { json });
|
|
144
180
|
case 'default':
|
|
145
181
|
if (!rest[0]) throw new Error(`Usage: ${PROGRAM} profile default <name>`);
|
|
146
182
|
return profileDefault(rest[0]);
|
|
183
|
+
case 'members': {
|
|
184
|
+
const [action, subject] = rest;
|
|
185
|
+
switch (action) {
|
|
186
|
+
case 'add':
|
|
187
|
+
return membersAdd(subject, {
|
|
188
|
+
...global,
|
|
189
|
+
json,
|
|
190
|
+
me: flags['me'] === true,
|
|
191
|
+
role: text(flags, 'role'),
|
|
192
|
+
});
|
|
193
|
+
case 'remove':
|
|
194
|
+
return membersRemove(subject, { ...global, json });
|
|
195
|
+
case 'list':
|
|
196
|
+
case undefined:
|
|
197
|
+
return membersList({ ...global, json });
|
|
198
|
+
default:
|
|
199
|
+
throw new Error(`Unknown: ${PROGRAM} profile members ${action}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
147
202
|
case 'remove':
|
|
148
203
|
if (!rest[0]) {
|
|
149
204
|
throw new Error(
|
|
150
|
-
`Usage: ${PROGRAM} profile remove <name> [--
|
|
205
|
+
`Usage: ${PROGRAM} profile remove <name> [--workspace <name>] [--dry-run] [--yes] ` +
|
|
206
|
+
'[--delete-data | --migrate-to <profile>]',
|
|
151
207
|
);
|
|
152
208
|
}
|
|
153
209
|
return profileRemove(rest[0], {
|
|
@@ -155,23 +211,31 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
155
211
|
json,
|
|
156
212
|
dryRun: flags['dry-run'] === true,
|
|
157
213
|
yes: flags['yes'] === true,
|
|
214
|
+
deleteData: flags['delete-data'] === true,
|
|
215
|
+
...(typeof flags['migrate-to'] === 'string'
|
|
216
|
+
? { migrateTo: flags['migrate-to'] }
|
|
217
|
+
: {}),
|
|
158
218
|
});
|
|
159
219
|
default:
|
|
160
220
|
throw new Error(`Unknown: ${PROGRAM} profile ${second}`);
|
|
161
221
|
}
|
|
162
222
|
|
|
223
|
+
// `target` was the old word for the same thing (ADR-061). Kept as its own
|
|
224
|
+
// case rather than aliased, so `selection.test.ts` sees both spellings and
|
|
225
|
+
// neither can default quietly.
|
|
163
226
|
case 'target':
|
|
227
|
+
case 'workspace':
|
|
164
228
|
switch (second) {
|
|
165
229
|
case 'list':
|
|
166
230
|
case undefined:
|
|
167
231
|
return targetList({ ...global, json, urls: flags['urls'] === true });
|
|
168
232
|
case 'use':
|
|
169
|
-
if (!rest[0]) throw new Error(`Usage: ${PROGRAM}
|
|
233
|
+
if (!rest[0]) throw new Error(`Usage: ${PROGRAM} workspace use <name>`);
|
|
170
234
|
return targetUse(rest[0]);
|
|
171
235
|
case 'show':
|
|
172
236
|
return targetShow(rest[0], { ...global, json });
|
|
173
237
|
default:
|
|
174
|
-
throw new Error(`Unknown: ${PROGRAM}
|
|
238
|
+
throw new Error(`Unknown: ${PROGRAM} workspace ${second}`);
|
|
175
239
|
}
|
|
176
240
|
|
|
177
241
|
case 'identity': {
|
|
@@ -206,11 +270,15 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
206
270
|
const [capability] = rest;
|
|
207
271
|
if (!capability) {
|
|
208
272
|
throw new Error(
|
|
209
|
-
`Usage: ${PROGRAM} policy ${second} <capability>\n` +
|
|
210
|
-
` e.g. ${PROGRAM} policy ${second} gmail.send_message
|
|
273
|
+
`Usage: ${PROGRAM} policy ${second} <capability> --connection <provider>.<id>\n` +
|
|
274
|
+
` e.g. ${PROGRAM} policy ${second} gmail.send_message --connection gmail.personal,\n` +
|
|
275
|
+
` or gmail.* for everything that connection's provider offers`,
|
|
211
276
|
);
|
|
212
277
|
}
|
|
213
|
-
return policyRule(second, capability,
|
|
278
|
+
return policyRule(second, capability, {
|
|
279
|
+
...global,
|
|
280
|
+
...(typeof flags.connection === 'string' ? { connection: flags.connection } : {}),
|
|
281
|
+
});
|
|
214
282
|
}
|
|
215
283
|
default:
|
|
216
284
|
throw new Error(`Unknown: ${PROGRAM} policy ${second}`);
|
|
@@ -227,6 +295,14 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
227
295
|
throw new Error(`Unknown: ${PROGRAM} token ${second}`);
|
|
228
296
|
}
|
|
229
297
|
|
|
298
|
+
case 'pair':
|
|
299
|
+
return pair({
|
|
300
|
+
...global,
|
|
301
|
+
print: flags['print'] === true,
|
|
302
|
+
rotate: flags['rotate'] === true,
|
|
303
|
+
yes: flags['yes'] === true,
|
|
304
|
+
});
|
|
305
|
+
|
|
230
306
|
case 'audit':
|
|
231
307
|
if (second === 'verify') return auditVerify(global);
|
|
232
308
|
if (second !== 'tail' && second !== undefined) {
|
|
@@ -340,7 +416,10 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
340
416
|
dryRun: flags['dry-run'] === true,
|
|
341
417
|
force: flags['force'] === true,
|
|
342
418
|
noSkill: flags['no-skill'] === true,
|
|
419
|
+
headless: flags['headless'] === true,
|
|
343
420
|
});
|
|
421
|
+
case 'install-instructions':
|
|
422
|
+
return installInstructions({ ...(text(flags, 'client') ? { client: text(flags, 'client') } : {}) });
|
|
344
423
|
case 'stdio':
|
|
345
424
|
return mcpStdio({ ...global, ...(flags['only'] === true ? { only: true } : {}) });
|
|
346
425
|
case 'list':
|
|
@@ -398,6 +477,7 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
398
477
|
switch (second) {
|
|
399
478
|
case undefined:
|
|
400
479
|
case 'targets':
|
|
480
|
+
case 'workspaces':
|
|
401
481
|
return syncTargets({
|
|
402
482
|
...global,
|
|
403
483
|
dryRun: flags['dry-run'] === true,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { ConfigError } from '#profile';
|
|
2
|
+
import type { BlobStore } from '#stores/blobs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Moving objects between two layouts, without losing one.
|
|
6
|
+
*
|
|
7
|
+
* Shared by every contract migration that relocates bytes rather than YAML —
|
|
8
|
+
* contract 3 hoisting a profile's data to the workspace, contract 4 carrying it
|
|
9
|
+
* back into the profile. Each learned the same rules the same hard way, so they
|
|
10
|
+
* live once: nothing is deleted until what replaced it has been read back, every
|
|
11
|
+
* destination is checked before the first byte moves, and a rerun after an
|
|
12
|
+
* interruption finishes rather than refusing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Move {
|
|
16
|
+
readonly from: string;
|
|
17
|
+
readonly to: string;
|
|
18
|
+
/**
|
|
19
|
+
* Rewrite the object's bytes on the way across.
|
|
20
|
+
*
|
|
21
|
+
* Only a connection record needs this, and it needs it for a reason a plain
|
|
22
|
+
* copy cannot serve: `ConnectionRepository.list` reads `provider` and `id`
|
|
23
|
+
* out of the record *body*, not out of the key it was stored under. A renamed
|
|
24
|
+
* connection whose bytes were copied verbatim would sit at
|
|
25
|
+
* `connections.v1/vault.work` still calling itself `vault.main`, and the next
|
|
26
|
+
* `upsert` would write a second record beside it.
|
|
27
|
+
*/
|
|
28
|
+
readonly rewrite?: (data: Uint8Array) => Uint8Array;
|
|
29
|
+
/**
|
|
30
|
+
* Write only where the destination is empty; leave the source alone if not.
|
|
31
|
+
*
|
|
32
|
+
* For the objects two profiles can legitimately both hold — a connection they
|
|
33
|
+
* share, a provider-keyed cache, one custom manifest — where a second copy is
|
|
34
|
+
* a duplicate rather than a clash. `claim` drops the loser within one run, but
|
|
35
|
+
* deletes the winner's source and leaves the loser's: the rerun then saw a
|
|
36
|
+
* different first claimant, found foreign bytes at the destination, and threw,
|
|
37
|
+
* permanently. `rewriteProfiles` stamps the contract *after* the moves, so an
|
|
38
|
+
* interruption during them forces exactly that rerun.
|
|
39
|
+
*/
|
|
40
|
+
readonly whenAbsent?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Copy, and leave the source where it is.
|
|
43
|
+
*
|
|
44
|
+
* For the one thing contract 4 cannot decide: a store two profiles both grant
|
|
45
|
+
* has to land in both, and neither copy may delete what the other still needs
|
|
46
|
+
* to read. The original is reported rather than removed — merging two sets of
|
|
47
|
+
* notes is not reversible, and picking one profile's would take the other's
|
|
48
|
+
* away silently.
|
|
49
|
+
*/
|
|
50
|
+
readonly keep?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Two objects aimed at one key, caught while this is still a plan.
|
|
55
|
+
*
|
|
56
|
+
* `applyMoves` checks the destination per object as well, but that check cannot
|
|
57
|
+
* see a collision between two objects *in this run* once the moves are applied
|
|
58
|
+
* concurrently — both would look at an absent destination and both would write.
|
|
59
|
+
* Hoisting it here also puts it where this file says it belongs: everything that
|
|
60
|
+
* can fail happens before the first byte moves, so a refusal leaves the
|
|
61
|
+
* workspace exactly as it was.
|
|
62
|
+
*/
|
|
63
|
+
export function assertOneObjectPerDestination(moves: readonly Move[]): void {
|
|
64
|
+
const seen = new Map<string, string>();
|
|
65
|
+
|
|
66
|
+
for (const move of moves) {
|
|
67
|
+
const first = seen.get(move.to);
|
|
68
|
+
if (first !== undefined) {
|
|
69
|
+
throw new ConfigError(
|
|
70
|
+
`Two objects want to be at ${move.to}, and this migration cannot merge them.\n` +
|
|
71
|
+
` ${first} and ${move.from}. Nothing has been written.\n` +
|
|
72
|
+
' Which one to keep is yours to decide, because both are your data: move or\n' +
|
|
73
|
+
' delete one of the two, then run this again.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
seen.set(move.to, move.from);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** First claim on a destination wins; a later one is left where it is. */
|
|
81
|
+
export function claim(claimed: Set<string>, move: Move): Move | null {
|
|
82
|
+
if (claimed.has(move.to)) return null;
|
|
83
|
+
claimed.add(move.to);
|
|
84
|
+
return { ...move, whenAbsent: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* How many objects are in flight at once.
|
|
89
|
+
*
|
|
90
|
+
* Serial was fine while this only ever ran against a local disk. A deployed
|
|
91
|
+
* workspace's audit log is one object per event, so the first real bucket this
|
|
92
|
+
* migrated held 1,906 of them — three round trips each, in series, is minutes of
|
|
93
|
+
* a deploy spent with nothing on screen. The same 16 the read paths in
|
|
94
|
+
* `#providers/memory` and `#providers/tasks` settled on, and for the same
|
|
95
|
+
* reason: enough to hide the latency, not enough to look like an incident to the
|
|
96
|
+
* other end.
|
|
97
|
+
*
|
|
98
|
+
* Safe to widen only while each move stays independent, which is what
|
|
99
|
+
* `assertOneObjectPerDestination` guarantees.
|
|
100
|
+
*/
|
|
101
|
+
const MOVE_CONCURRENCY = 16;
|
|
102
|
+
|
|
103
|
+
export async function applyMoves(files: BlobStore, moves: readonly Move[]): Promise<void> {
|
|
104
|
+
const pending = moves.filter((move) => move.from !== move.to);
|
|
105
|
+
|
|
106
|
+
for (let start = 0; start < pending.length; start += MOVE_CONCURRENCY) {
|
|
107
|
+
await Promise.all(
|
|
108
|
+
pending.slice(start, start + MOVE_CONCURRENCY).map((move) => applyMove(files, move)),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One object, moved or finished. Never deletes before the copy reads back. */
|
|
114
|
+
async function applyMove(files: BlobStore, move: Move): Promise<void> {
|
|
115
|
+
const source = await files.get(move.from);
|
|
116
|
+
if (source === null) return;
|
|
117
|
+
|
|
118
|
+
// Before the comparison below as well as before the write. Comparing the
|
|
119
|
+
// *source* bytes against a destination that holds the rewritten ones would
|
|
120
|
+
// read an already-finished move as a collision, and refuse the one rerun this
|
|
121
|
+
// file promises.
|
|
122
|
+
const data = move.rewrite === undefined ? source : move.rewrite(source);
|
|
123
|
+
|
|
124
|
+
if (await files.has(move.to)) {
|
|
125
|
+
const held = await files.get(move.to);
|
|
126
|
+
|
|
127
|
+
// Raced away between the two calls, so there is nothing there after all and
|
|
128
|
+
// the ordinary path below is still the right one.
|
|
129
|
+
if (held !== null) {
|
|
130
|
+
// Another instance of the same connection, or another profile's copy of
|
|
131
|
+
// one shared cache, got there first. Left where it is rather than
|
|
132
|
+
// refused — and rerunnable, which is the whole point.
|
|
133
|
+
if (!sameBytes(held, data)) {
|
|
134
|
+
if (move.whenAbsent === true) return;
|
|
135
|
+
throw new ConfigError(
|
|
136
|
+
`Two objects want to be at ${move.to}, and this migration cannot merge them.\n` +
|
|
137
|
+
` ${move.from} is the second, and what is already there is not a copy of it.\n` +
|
|
138
|
+
' Nothing has been deleted. Please report it with the layout of your data ' +
|
|
139
|
+
'directory.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Already copied, by a run that did not get to the delete.
|
|
144
|
+
if (move.keep !== true) await files.delete(move.from);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await files.put(move.to, data);
|
|
150
|
+
if ((await files.get(move.to)) === null) {
|
|
151
|
+
throw new ConfigError(
|
|
152
|
+
`${move.to} did not read back after being written. Nothing has been deleted; ` +
|
|
153
|
+
`fix the store and run this again.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (move.keep !== true) await files.delete(move.from);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sameBytes(left: Uint8Array, right: Uint8Array): boolean {
|
|
161
|
+
if (left.length !== right.length) return false;
|
|
162
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
163
|
+
if (left[index] !== right[index]) return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
package/src/cli/migrate-plan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
LEGACY_DATA_DIR, ConfigError, layout, isRemoteWorkspace, type LegacyTarget, type WorkspaceTarget } from '#profile';
|
|
2
3
|
import { deployedWorkspace } from '#deployments/upload.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -88,7 +89,7 @@ export function toEntry(
|
|
|
88
89
|
// produces `gs://b` pointing at `gs://b`. That is a loop, and `openTarget`
|
|
89
90
|
// refuses it — which made `deploy` unable to run against the bucket it had
|
|
90
91
|
// just migrated, on the one command the refusal names as the fix.
|
|
91
|
-
if (remote && remote !== workspaceRoot) return {
|
|
92
|
+
if (remote && remote !== workspaceRoot) return { at: remote };
|
|
92
93
|
|
|
93
94
|
// **A filesystem target is dropped from a remote workspace.**
|
|
94
95
|
//
|
|
@@ -105,8 +106,13 @@ export function toEntry(
|
|
|
105
106
|
const storagePath = declared.storage.path;
|
|
106
107
|
const credentialsPath = declared.credentials.path;
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// The **contract-1** defaults, spelled out rather than taken from `layout`.
|
|
110
|
+
// This function reads a contract-1 profile, where every path was per profile,
|
|
111
|
+
// and `layout` describes where things live *now* — workspace-level since
|
|
112
|
+
// ADR-057. Asking it would compare a contract-1 path against a contract-3
|
|
113
|
+
// default and refuse every profile that had written the ordinary one.
|
|
114
|
+
const defaultStorage = `./${LEGACY_DATA_DIR}/${profile}`;
|
|
115
|
+
const defaultCredentials = `./${LEGACY_DATA_DIR}/${profile}/credentials.enc`;
|
|
110
116
|
|
|
111
117
|
refuseCustomPath(name, profile, workspaceRoot, 'storage.path', storagePath, defaultStorage);
|
|
112
118
|
refuseCustomPath(
|
|
@@ -149,12 +155,12 @@ function refuseCustomPath(
|
|
|
149
155
|
` A target is declared once for the whole workspace now (ADR-052), so it cannot hold a\n` +
|
|
150
156
|
` different path per profile. The default it would get is ${expected}.\n\n` +
|
|
151
157
|
` Move the data there and delete the line, or keep this profile in a workspace of its\n` +
|
|
152
|
-
` own: LANES_LINK_HOME=${workspaceRoot}/<somewhere> lanes link profile add ${profile} --
|
|
158
|
+
` own: LANES_LINK_HOME=${workspaceRoot}/<somewhere> lanes link profile add ${profile} --workspace ${target}`,
|
|
153
159
|
);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
export function summarise(entry: WorkspaceTarget): string {
|
|
157
|
-
if (entry.
|
|
163
|
+
if (entry.at !== undefined) return `points at ${entry.at}`;
|
|
158
164
|
const parts = [entry.credentials?.adapter, entry.storage?.adapter].filter(Boolean);
|
|
159
165
|
return `${parts.join(' + ')}${entry.deploy ? `, deploys ${entry.deploy.service}` : ''}`;
|
|
160
166
|
}
|
package/src/cli/output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wasDefaulted } from './selection-require.ts';
|
|
1
2
|
import type { Resolution } from '#profile';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -126,16 +127,48 @@ export function announceProfile(selection: {
|
|
|
126
127
|
print(style.dim(`profile ${style.bold(selection.profile)} ${selection.workspaceRoot}`));
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The line every command prints before its output.
|
|
132
|
+
*
|
|
133
|
+
* It names the workspace, and whether that workspace was *typed* or came from
|
|
134
|
+
* `default_workspace`. The provenance is not decoration: ADR-037 removed sticky
|
|
135
|
+
* selection because "the dotfile nothing prints" is how an operator runs a
|
|
136
|
+
* command against the wrong thing, and ADR-061 gives the default back only on
|
|
137
|
+
* the condition that it announces itself. Dropping the `(default)` marker for
|
|
138
|
+
* tidiness would reverse that decision.
|
|
139
|
+
*/
|
|
129
140
|
export function announce(resolution: Resolution): void {
|
|
141
|
+
const provenance = wasDefaulted(resolution.target) ? ' (default)' : '';
|
|
142
|
+
|
|
130
143
|
print(
|
|
131
144
|
style.dim(
|
|
132
145
|
`profile ${style.bold(resolution.profile)} ` +
|
|
133
|
-
`
|
|
146
|
+
`workspace ${style.bold(resolution.target)}${provenance} ` +
|
|
134
147
|
`${resolution.workspaceRoot}`,
|
|
135
148
|
),
|
|
136
149
|
);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The same line, for a command whose subject is the workspace.
|
|
154
|
+
*
|
|
155
|
+
* It names no profile, and that is the point rather than a saving. A
|
|
156
|
+
* workspace-level command still opens a runtime, and a runtime carries a
|
|
157
|
+
* profile — so `announce` would print whichever one happened to be picked, and
|
|
158
|
+
* a reader would take it for the thing being acted on. The audit log is one
|
|
159
|
+
* chain for the whole workspace; saying `profile personal` above it describes a
|
|
160
|
+
* log that does not exist.
|
|
161
|
+
*/
|
|
162
|
+
export function announceWorkspace(resolution: Resolution): void {
|
|
163
|
+
const provenance = wasDefaulted(resolution.target) ? ' (default)' : '';
|
|
164
|
+
|
|
165
|
+
print(
|
|
166
|
+
style.dim(
|
|
167
|
+
`workspace ${style.bold(resolution.target)}${provenance} ${resolution.workspaceRoot}`,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
/**
|
|
140
173
|
* Print a machine-readable result, or fall through to the human rendering.
|
|
141
174
|
*
|
package/src/cli/publish.ts
CHANGED
|
@@ -53,7 +53,8 @@ export async function publishAndNotify(input: {
|
|
|
53
53
|
readonly config: Config;
|
|
54
54
|
readonly workspaceRoot: string;
|
|
55
55
|
readonly target: string;
|
|
56
|
-
|
|
56
|
+
/** Every profile the edit touched. See `publishWorkspace`. */
|
|
57
|
+
readonly profile: string | readonly string[];
|
|
57
58
|
readonly credentials: SecretStore;
|
|
58
59
|
}): Promise<PublishOutcome> {
|
|
59
60
|
let published: string | null = null;
|
|
@@ -98,18 +99,16 @@ export async function publishProfileEdit(input: {
|
|
|
98
99
|
readonly resolution: { readonly workspaceRoot: string; readonly profile: string };
|
|
99
100
|
readonly config: Config;
|
|
100
101
|
readonly target: string;
|
|
102
|
+
/** Every profile the edit touched, where it reached more than the one named. */
|
|
103
|
+
readonly touched?: readonly string[] | undefined;
|
|
101
104
|
}): Promise<PublishOutcome> {
|
|
102
|
-
const credentials = await openSecretStoreFor(
|
|
103
|
-
input.config,
|
|
104
|
-
input.resolution.workspaceRoot,
|
|
105
|
-
input.target,
|
|
106
|
-
);
|
|
105
|
+
const credentials = await openSecretStoreFor(input.resolution.workspaceRoot, input.target);
|
|
107
106
|
|
|
108
107
|
return publishAndNotify({
|
|
109
108
|
config: input.config,
|
|
110
109
|
workspaceRoot: input.resolution.workspaceRoot,
|
|
111
110
|
target: input.target,
|
|
112
|
-
profile: input.resolution.profile,
|
|
111
|
+
profile: input.touched ?? input.resolution.profile,
|
|
113
112
|
credentials,
|
|
114
113
|
});
|
|
115
114
|
}
|