@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CONNECTIONS_FILE, readConnections, type Resolution } from '#profile';
|
|
2
|
+
import { ConfigDocument } from '../config-edit.ts';
|
|
3
|
+
import { announce, announceWorkspace, emit, ok, print, style } from '../output.ts';
|
|
4
|
+
import { recordConfigChange } from '../audit-change.ts';
|
|
5
|
+
import { openWorkspaceRuntime, type GlobalFlags } from '../runtime.ts';
|
|
6
|
+
import { nextAfterEdit, publishProfileEdit } from '../publish.ts';
|
|
7
|
+
import { locate } from './connection.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `lanes link relabel` — the operator's own word for an account.
|
|
11
|
+
*
|
|
12
|
+
* Split from `disconnect` so `connection.ts` stays inside the size budget, and
|
|
13
|
+
* on a real seam rather than a line count: `disconnect` reaches two files, every
|
|
14
|
+
* profile that granted the row, and the credential store, while this writes one
|
|
15
|
+
* field in one document.
|
|
16
|
+
*
|
|
17
|
+
* The label is a separate field from `account` for the reason the bug below
|
|
18
|
+
* gives, and the two must not be merged back.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface Relabelled {
|
|
22
|
+
readonly profile: string;
|
|
23
|
+
readonly target: string;
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly from: string;
|
|
26
|
+
readonly to: string;
|
|
27
|
+
readonly published: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RelabelFlags extends GlobalFlags {
|
|
31
|
+
readonly json?: boolean | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rename a connection, writing `label` and never `account`.
|
|
36
|
+
*
|
|
37
|
+
* It wrote `account` until it was noticed that `account` is not a display name:
|
|
38
|
+
* `settleIdentity` matches on it to tell a repair from a new account,
|
|
39
|
+
* `idFromAccount` derives the id from it, and `gmail.send_message` writes it
|
|
40
|
+
* into a `From` header. Renaming through it therefore un-recognised the account
|
|
41
|
+
* it renamed — the next `connect` added a second row beside it.
|
|
42
|
+
*/
|
|
43
|
+
export async function renameConnection(
|
|
44
|
+
key: string,
|
|
45
|
+
label: string,
|
|
46
|
+
flags: RelabelFlags,
|
|
47
|
+
): Promise<{ resolution: Resolution; relabelled: Relabelled }> {
|
|
48
|
+
const runtime = await openWorkspaceRuntime(flags);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const { resolution, config, target } = runtime;
|
|
52
|
+
const root = resolution.workspaceRoot;
|
|
53
|
+
|
|
54
|
+
// The workspace's file, not the profile's. A label is the operator's own
|
|
55
|
+
// word for an account (ADR-057 moved the row that carries it), so renaming
|
|
56
|
+
// it once renames it everywhere — which is what someone renaming "the work
|
|
57
|
+
// mailbox" means, and what two copies could never keep true.
|
|
58
|
+
const connectionsFile = await readConnections(root);
|
|
59
|
+
const located = locate(connectionsFile.connections, key, target);
|
|
60
|
+
|
|
61
|
+
const document = await ConfigDocument.openKey(root, CONNECTIONS_FILE);
|
|
62
|
+
document.setIn(['connections', located.index, 'label'], label);
|
|
63
|
+
await document.save();
|
|
64
|
+
|
|
65
|
+
await recordConfigChange(config, root, target, {
|
|
66
|
+
capability: 'config.connection.relabel',
|
|
67
|
+
scope: target,
|
|
68
|
+
connection: key,
|
|
69
|
+
arguments: { from: located.connection.label ?? located.connection.account, to: label },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
resolution,
|
|
74
|
+
relabelled: {
|
|
75
|
+
profile: resolution.profile,
|
|
76
|
+
target,
|
|
77
|
+
key,
|
|
78
|
+
// What it was called a moment ago, which is the account only until the
|
|
79
|
+
// first rename.
|
|
80
|
+
from: located.connection.label ?? located.connection.account,
|
|
81
|
+
to: label,
|
|
82
|
+
published: nextAfterEdit(await publishProfileEdit({ resolution, config, target })),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
} finally {
|
|
86
|
+
await runtime.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function relabel(
|
|
91
|
+
key: string | undefined,
|
|
92
|
+
label: string | undefined,
|
|
93
|
+
flags: RelabelFlags,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (!key) throw new Error('Which connection? Run: lanes link status');
|
|
96
|
+
if (!label) throw new Error(`What should ${key} be called? Run: lanes link relabel ${key} "New name"`);
|
|
97
|
+
|
|
98
|
+
const { resolution, relabelled: result } = await renameConnection(key, label, flags);
|
|
99
|
+
|
|
100
|
+
return emit(flags.json, result, () => {
|
|
101
|
+
announceWorkspace(resolution);
|
|
102
|
+
print(ok(`${style.bold(result.key)} is now ${style.bold(result.to)}`));
|
|
103
|
+
if (result.from) print(` was ${style.dim(result.from)}`);
|
|
104
|
+
// The label is a display name in two places, and only one of them changed.
|
|
105
|
+
print(
|
|
106
|
+
style.dim(
|
|
107
|
+
' The state store keeps the old name until the next reconcile, which updates it.',
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
if (result.published) print(style.dim(` ${result.published}`));
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { ConfigError } from '#profile';
|
|
2
2
|
import { assertValidSecretRef } from '#secrets';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { announceWorkspace, announceProfile, heading, ok, print, style } from '../output.ts';
|
|
4
|
+
import {
|
|
5
|
+
openSecretStoreFor,
|
|
6
|
+
primaryProfile,
|
|
7
|
+
resolveProfile,
|
|
8
|
+
resolveProfileOnly,
|
|
9
|
+
type GlobalFlags,
|
|
10
|
+
} from '../runtime.ts';
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* `lanes link secrets` — moving credential values between a profile's targets.
|
|
8
14
|
*
|
|
9
15
|
* Credentials follow the target, because each target has its own credential
|
|
10
|
-
* store: `lanes link connect gmail.side --
|
|
11
|
-
* Secret Manager, and the same command with `--
|
|
16
|
+
* store: `lanes link connect gmail.side --workspace cloud` writes the refresh token into
|
|
17
|
+
* Secret Manager, and the same command with `--workspace local` writes it into
|
|
12
18
|
* the encrypted file. That is the right default and it leaves one gap, which
|
|
13
19
|
* this command fills — a setup built locally first, and now wanted in the
|
|
14
20
|
* cloud, without re-running every OAuth flow.
|
|
@@ -38,12 +44,12 @@ export async function secretsPush(flags: SecretsFlags): Promise<void> {
|
|
|
38
44
|
const { selection, config } = await resolveProfileOnly(flags);
|
|
39
45
|
announceProfile(selection);
|
|
40
46
|
|
|
41
|
-
const source = await openSecretStoreFor(
|
|
42
|
-
const destination = await openSecretStoreFor(
|
|
47
|
+
const source = await openSecretStoreFor(selection.workspaceRoot, flags.from);
|
|
48
|
+
const destination = await openSecretStoreFor(selection.workspaceRoot, flags.to);
|
|
43
49
|
|
|
44
50
|
const refs = await source.list();
|
|
45
51
|
if (refs.length === 0) {
|
|
46
|
-
print(style.dim(`No credentials in
|
|
52
|
+
print(style.dim(`No credentials in workspace "${flags.from}".`));
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -104,7 +110,7 @@ export async function secretsSet(ref: string | undefined, flags: GlobalFlags): P
|
|
|
104
110
|
if (!ref) {
|
|
105
111
|
throw new ConfigError(
|
|
106
112
|
'Usage: lanes link secrets set <ref> (the value is read from stdin)\n' +
|
|
107
|
-
' e.g. printf %s "$DATABASE_URL" | lanes link secrets set cloud/database_url --
|
|
113
|
+
' e.g. printf %s "$DATABASE_URL" | lanes link secrets set cloud/database_url --workspace cloud',
|
|
108
114
|
);
|
|
109
115
|
}
|
|
110
116
|
assertValidSecretRef(ref);
|
|
@@ -119,8 +125,16 @@ export async function secretsSet(ref: string | undefined, flags: GlobalFlags): P
|
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
// The credential store belongs to the workspace since contract 3, so every
|
|
129
|
+
// profile opened the same one and naming one chose nothing. A profile is
|
|
130
|
+
// still resolved, because `openSecretStoreFor` reads the adapter set through
|
|
131
|
+
// one — so the banner names the workspace rather than reporting whichever
|
|
132
|
+
// profile happened to supply it.
|
|
133
|
+
const { resolution, config, target } = await resolveProfile({
|
|
134
|
+
...flags,
|
|
135
|
+
profile: await primaryProfile(flags),
|
|
136
|
+
});
|
|
137
|
+
announceWorkspace(resolution);
|
|
124
138
|
|
|
125
139
|
const value = (await Bun.stdin.text()).replace(/\n$/, '');
|
|
126
140
|
if (!value) {
|
|
@@ -129,24 +143,32 @@ export async function secretsSet(ref: string | undefined, flags: GlobalFlags): P
|
|
|
129
143
|
);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
const credentials = await openSecretStoreFor(
|
|
146
|
+
const credentials = await openSecretStoreFor(resolution.workspaceRoot, target);
|
|
133
147
|
const replacing = await credentials.has(ref);
|
|
134
148
|
await credentials.set(ref, value);
|
|
135
149
|
|
|
136
|
-
print(ok(`${replacing ? 'replaced' : 'stored'} ${style.bold(ref)} in
|
|
150
|
+
print(ok(`${replacing ? 'replaced' : 'stored'} ${style.bold(ref)} in workspace ${target}`));
|
|
137
151
|
if (replacing) {
|
|
138
152
|
print(style.dim(' Anything still using the old value will start failing.'));
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
export async function secretsList(flags: GlobalFlags): Promise<void> {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
// The credential store belongs to the workspace since contract 3, so every
|
|
158
|
+
// profile opened the same one and naming one chose nothing. A profile is
|
|
159
|
+
// still resolved, because `openSecretStoreFor` reads the adapter set through
|
|
160
|
+
// one — so the banner names the workspace rather than reporting whichever
|
|
161
|
+
// profile happened to supply it.
|
|
162
|
+
const { resolution, config, target } = await resolveProfile({
|
|
163
|
+
...flags,
|
|
164
|
+
profile: await primaryProfile(flags),
|
|
165
|
+
});
|
|
166
|
+
announceWorkspace(resolution);
|
|
167
|
+
|
|
168
|
+
const credentials = await openSecretStoreFor(resolution.workspaceRoot, target);
|
|
147
169
|
const refs = await credentials.list();
|
|
148
170
|
|
|
149
|
-
heading(`Credential references in
|
|
171
|
+
heading(`Credential references in workspace ${target} (${refs.length})`);
|
|
150
172
|
if (refs.length === 0) {
|
|
151
173
|
print(style.dim(' none'));
|
|
152
174
|
} else {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigError,
|
|
3
|
+
WORKSPACE_FILE,
|
|
4
|
+
readRegistry,
|
|
5
|
+
readWorkspace,
|
|
6
|
+
resolveWorkspaceRoot,
|
|
7
|
+
} from '#profile';
|
|
8
|
+
import { ConfigDocument } from '../config-edit.ts';
|
|
9
|
+
import { ok, print, style } from '../output.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `lanes set-workspace <name>` — which workspace a command means when it does
|
|
13
|
+
* not say.
|
|
14
|
+
*
|
|
15
|
+
* ADR-037 deleted the command this replaces, and its reasoning is the thing to
|
|
16
|
+
* read before changing anything here:
|
|
17
|
+
*
|
|
18
|
+
* > Persisted context state is the standard way operators run destructive
|
|
19
|
+
* > commands against the wrong target, and the version of it that bites is the
|
|
20
|
+
* > dotfile nothing prints.
|
|
21
|
+
*
|
|
22
|
+
* Both halves of that sentence are answered rather than argued with (ADR-061).
|
|
23
|
+
* The default is **printed on every command that uses it**, so it is not the
|
|
24
|
+
* dotfile nothing prints; and it is **refused by every command that publishes or
|
|
25
|
+
* destroys**, so it is not what runs a destructive command against the wrong
|
|
26
|
+
* thing. Take either half away and the objection stands again.
|
|
27
|
+
*
|
|
28
|
+
* Top level rather than under `lanes link`, because it selects the workspace
|
|
29
|
+
* *the CLI* acts in and a second area added to `lanes` will want the same
|
|
30
|
+
* answer.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export interface SetWorkspaceResult {
|
|
34
|
+
readonly workspace: string;
|
|
35
|
+
readonly previous: string | null;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function setWorkspace(
|
|
40
|
+
name: string | undefined,
|
|
41
|
+
options: { json?: boolean } = {},
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const root = resolveWorkspaceRoot();
|
|
44
|
+
|
|
45
|
+
if (name === undefined) {
|
|
46
|
+
const current = (await readWorkspace(root))?.default_workspace;
|
|
47
|
+
throw new ConfigError(
|
|
48
|
+
`Usage: lanes set-workspace <name>\n` +
|
|
49
|
+
(current ? ` The default is currently "${current}".\n` : ' No default is set.\n') +
|
|
50
|
+
` Run "lanes link workspace list" to see what there is.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checked against the registry, not accepted on trust. A default naming a
|
|
55
|
+
// workspace that does not exist would turn every later command's refusal into
|
|
56
|
+
// one about the wrong thing — and the operator would have been told "ok" by
|
|
57
|
+
// the command that caused it.
|
|
58
|
+
const registry = await readRegistry(root);
|
|
59
|
+
if (!(name in registry)) {
|
|
60
|
+
const names = Object.keys(registry).sort();
|
|
61
|
+
throw new ConfigError(
|
|
62
|
+
`${root} declares no workspace "${name}".\n` +
|
|
63
|
+
(names.length > 0
|
|
64
|
+
? ` It has: ${names.join(', ')}\n`
|
|
65
|
+
: ' It has none yet. Create one with: lanes link profile add <name> --workspace local\n') +
|
|
66
|
+
` Run "lanes link workspace list" to see them.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const document = await ConfigDocument.openKey(root, WORKSPACE_FILE);
|
|
71
|
+
const previous = document.getIn(['default_workspace']);
|
|
72
|
+
document.setIn(['default_workspace'], name);
|
|
73
|
+
await document.save();
|
|
74
|
+
|
|
75
|
+
const result: SetWorkspaceResult = {
|
|
76
|
+
workspace: name,
|
|
77
|
+
previous: typeof previous === 'string' ? previous : null,
|
|
78
|
+
path: `${root}/${WORKSPACE_FILE}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (options.json === true) {
|
|
82
|
+
print(JSON.stringify(result, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
print(ok(`default workspace: ${style.bold(name)}`));
|
|
87
|
+
print(style.dim(` ${result.path}`));
|
|
88
|
+
print('');
|
|
89
|
+
print(
|
|
90
|
+
style.dim(
|
|
91
|
+
' Every command that uses it prints it. Commands that publish or destroy —\n' +
|
|
92
|
+
' deploy, sync, secrets push, profile remove, disconnect, token rotate —\n' +
|
|
93
|
+
' still want --workspace typed out.',
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -25,11 +25,11 @@ export async function setupPlan(provider: string | undefined, flags: SetupFlags)
|
|
|
25
25
|
const context = {
|
|
26
26
|
profile: runtime.resolution.profile,
|
|
27
27
|
target: runtime.resolution.target,
|
|
28
|
-
connections: runtime.
|
|
28
|
+
connections: runtime.connections.map(({ ref }) => ref),
|
|
29
29
|
// Declaring an `oauth_apps` entry is how a profile says "use my own
|
|
30
30
|
// client". Without this the plan would tell someone who has already
|
|
31
31
|
// registered one that they need nothing.
|
|
32
|
-
ownClients:
|
|
32
|
+
ownClients: runtime.ownClients,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const manifests = runtime.registry.list().map((entry) => entry.manifest);
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -64,15 +64,15 @@ async function locateRemote(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const entry = (await readRegistry(root))[target];
|
|
67
|
-
if (entry?.
|
|
67
|
+
if (entry?.at) return { workspace: entry.at, how: 'already recorded' };
|
|
68
68
|
|
|
69
69
|
if (flags.discover !== true) {
|
|
70
70
|
throw new ConfigError(
|
|
71
71
|
`Nothing here says where "${target}" lives.\n` +
|
|
72
72
|
' No pointer to it in lanes-link.yaml, and nothing else records one.\n\n' +
|
|
73
|
-
' If you know the bucket: lanes link sync
|
|
73
|
+
' If you know the bucket: lanes link sync workspaces --workspace ' +
|
|
74
74
|
`${target} --from gs://<bucket>\n` +
|
|
75
|
-
` If you do not: lanes link sync targets --
|
|
75
|
+
` If you do not: lanes link sync targets --workspace ${target} --discover`,
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -99,7 +99,7 @@ async function locateRemote(
|
|
|
99
99
|
if (candidates.length > 1 || !isInteractive()) {
|
|
100
100
|
throw new ConfigError(
|
|
101
101
|
`Found ${candidates.length} deployment(s). Name the one you mean:\n` +
|
|
102
|
-
` lanes link sync targets --
|
|
102
|
+
` lanes link sync targets --workspace ${target} --from ${first.workspace}`,
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -132,7 +132,7 @@ export async function syncTargets(flags: SyncFlags): Promise<void> {
|
|
|
132
132
|
const remoteRegistry = await readRegistry(workspace);
|
|
133
133
|
const declaresIt = remoteRegistry[target] !== undefined;
|
|
134
134
|
const existing = (await readRegistry(root))[target];
|
|
135
|
-
const already = existing?.
|
|
135
|
+
const already = existing?.at === workspace;
|
|
136
136
|
|
|
137
137
|
const payload = { workspace: root, remote: workspace, target, how, declaresIt, applied: false };
|
|
138
138
|
|
|
@@ -163,7 +163,7 @@ export async function syncTargets(flags: SyncFlags): Promise<void> {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
heading('Would write');
|
|
166
|
-
print(`
|
|
166
|
+
print(` workspaces.${target}.at: ${workspace}`);
|
|
167
167
|
print(style.dim(' The bucket keeps everything else; this records where it is.'));
|
|
168
168
|
};
|
|
169
169
|
|
|
@@ -184,11 +184,11 @@ export async function syncTargets(flags: SyncFlags): Promise<void> {
|
|
|
184
184
|
// deploy block, whose token opens it — is declared where it lives, and reading
|
|
185
185
|
// it from there is what makes this safe to run on a workspace that has lost
|
|
186
186
|
// its own copy of anything.
|
|
187
|
-
await recordTarget(root, target, { workspace });
|
|
187
|
+
await recordTarget(root, target, { at: workspace });
|
|
188
188
|
|
|
189
189
|
print('');
|
|
190
190
|
print(ok(`"${target}" now points at ${workspace}`));
|
|
191
|
-
print(style.dim(` lanes link status --
|
|
191
|
+
print(style.dim(` lanes link status --workspace ${target} reads it from there`));
|
|
192
192
|
|
|
193
193
|
return emit(flags.json, { ...payload, applied: true }, () => {});
|
|
194
194
|
}
|
|
@@ -158,7 +158,7 @@ function summarise(name: string, entry: WorkspaceTarget, isSelected: boolean): T
|
|
|
158
158
|
return {
|
|
159
159
|
name,
|
|
160
160
|
isSelected,
|
|
161
|
-
pointsAt: entry.
|
|
161
|
+
pointsAt: entry.at,
|
|
162
162
|
credentials: null,
|
|
163
163
|
storage: null,
|
|
164
164
|
vault: null,
|
|
@@ -200,7 +200,7 @@ function summarise(name: string, entry: WorkspaceTarget, isSelected: boolean): T
|
|
|
200
200
|
* line above, from the same object.
|
|
201
201
|
*/
|
|
202
202
|
export function resolvedEntry(resolved: ResolvedTarget): WorkspaceTarget {
|
|
203
|
-
const {
|
|
203
|
+
const { at: _followed, ...record } = resolved.entry;
|
|
204
204
|
return { ...resolved.declared, ...record };
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -261,7 +261,7 @@ export async function targetList(flags: TargetFlags): Promise<void> {
|
|
|
261
261
|
// as input would be circular, and it has to keep working in the state every
|
|
262
262
|
// other command fails in — which is what `selectedDeclared` reports.
|
|
263
263
|
if (listing.selected === null) {
|
|
264
|
-
print(style.dim(' Every other command names one of these with --
|
|
264
|
+
print(style.dim(' Every other command names one of these with --workspace.'));
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -299,10 +299,12 @@ function deploymentCell(target: TargetSummary): string {
|
|
|
299
299
|
*/
|
|
300
300
|
export function targetUse(name: string | undefined): never {
|
|
301
301
|
throw new ConfigError(
|
|
302
|
-
'lanes link
|
|
303
|
-
'
|
|
304
|
-
'
|
|
305
|
-
` lanes
|
|
302
|
+
'lanes link workspace use was removed, and came back under a new name.\n' +
|
|
303
|
+
' `instance.default_target` is still inert — nothing reads it. What does\n' +
|
|
304
|
+
' read a default is `default_workspace` in lanes-link.yaml (ADR-061):\n' +
|
|
305
|
+
` lanes set-workspace ${name ?? '<name>'}\n` +
|
|
306
|
+
' Or name it per command:\n' +
|
|
307
|
+
` lanes link status --profile <name> --workspace ${name ?? '<name>'}\n` +
|
|
306
308
|
' If the key is still in your profile it is inert, and safe to delete.',
|
|
307
309
|
);
|
|
308
310
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Contract3Migration } from '../contract3.ts';
|
|
2
|
+
import type { Contract4Migration } from '../contract4.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What each contract migration did, as `update` reports it.
|
|
6
|
+
*
|
|
7
|
+
* Split from `update.ts` so that file stays inside the size budget, and on the
|
|
8
|
+
* seam it already had: that file decides *whether* to migrate and this says
|
|
9
|
+
* *what happened*. Kept beside it rather than beside the migrations, because
|
|
10
|
+
* `doctor --fix` renders the same facts its own way and the two are allowed to
|
|
11
|
+
* read differently — what they may not do is disagree about which of them is
|
|
12
|
+
* worth mentioning.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What contract 4 did, and the one part of it that is the operator's.
|
|
17
|
+
*
|
|
18
|
+
* `update` reported nothing, so the shared-store duplication happened in
|
|
19
|
+
* silence — a decision the migration calls the owner's, and which `doctor --fix`
|
|
20
|
+
* prints. Two paths disagreeing on that is how one becomes the wrong one to run.
|
|
21
|
+
*/
|
|
22
|
+
export function sayContract4(migration: Contract4Migration, say: (line: string) => void): void {
|
|
23
|
+
say(
|
|
24
|
+
`migrated ${migration.profiles.length} profile(s) to contract 4 — a profile owns its data ` +
|
|
25
|
+
'again, and Lanes\u2019 own surfaces are lanes_memory, lanes_tasks and the rest',
|
|
26
|
+
);
|
|
27
|
+
for (const change of migration.changes) say(` ${change}`);
|
|
28
|
+
|
|
29
|
+
if (migration.shared.length > 0) {
|
|
30
|
+
say(' A store more than one profile granted was copied into each, and the original kept.');
|
|
31
|
+
say(' Deleting what you do not want is the one step here that is yours.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (migration.orphaned.length > 0) {
|
|
35
|
+
say(' What no profile grants was left exactly where it is, and named above.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
say(' Every registered client caches its tool list, so re-add them: lanes link mcp add');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sayContract3(migration: Contract3Migration, say: (line: string) => void): void {
|
|
42
|
+
say(
|
|
43
|
+
`migrated ${migration.profiles.length} profile(s) to contract 3 — connections belong to the ` +
|
|
44
|
+
'workspace now, and a profile grants them one by one',
|
|
45
|
+
);
|
|
46
|
+
for (const change of migration.changes) say(` ${change}`);
|
|
47
|
+
|
|
48
|
+
if (migration.renames.length > 0) {
|
|
49
|
+
say(' Two profiles named different accounts with the same id, so one was renamed.');
|
|
50
|
+
say(' Check the grants in each profile before running an agent against them.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
say(` The old per-profile credential stores are left in place; remove them once this works.`);
|
|
54
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
3
|
import { installRoot, resolveWorkspaceRoot } from '#profile';
|
|
4
|
-
import { repairOwnerLayer } from '../config-repair.ts';
|
|
5
|
-
import {
|
|
4
|
+
import { repairOwnerLayer } from '../config-repair-sweep.ts';
|
|
5
|
+
import { migrateToCurrentContract, needsMigration, type ContractMigration } from '../workspace-migrate.ts';
|
|
6
|
+
import { needsContract3, type Contract3Migration } from '../contract3.ts';
|
|
7
|
+
import { needsContract4 } from '../contract4.ts';
|
|
8
|
+
import { sayContract3, sayContract4 } from './update-migration.ts';
|
|
6
9
|
import { emit, fail, ok, print, printErr, progress, style, warn } from '../output.ts';
|
|
7
10
|
import { PACKAGE, release, type ReleaseState } from '../release.ts';
|
|
8
11
|
import { version } from '../version.ts';
|
|
12
|
+
import { readSession } from '#auth/lanes/session.ts';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* `lanes link update` — install the newer release, or say why it will not.
|
|
@@ -182,11 +186,17 @@ export async function update(flags: UpdateFlags): Promise<void> {
|
|
|
182
186
|
// a contract it does not implement until someone redeploys. `deploy` is what
|
|
183
187
|
// migrates a bucket, because it is the command that ships the image in the
|
|
184
188
|
// same breath.
|
|
185
|
-
await migrateLocal(root, flags.json === true ? progress : print);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
const migrated = await migrateLocal(root, flags.json === true ? progress : print);
|
|
190
|
+
|
|
191
|
+
// Only on a workspace that is actually at the current contract — see
|
|
192
|
+
// `migrateLocal`. The repair writes contract-3 shapes, and writing them
|
|
193
|
+
// after a refusal that said nothing had been written is what left a stray
|
|
194
|
+
// `connections.yaml` in a contract-2 workspace.
|
|
195
|
+
if (migrated) {
|
|
196
|
+
await repairOwnerLayer(root, undefined, {
|
|
197
|
+
...(flags.json === true ? { report: progress } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
190
200
|
}
|
|
191
201
|
|
|
192
202
|
const report = {
|
|
@@ -306,27 +316,71 @@ async function runInstall(argv: readonly string[], json: boolean): Promise<boole
|
|
|
306
316
|
* about, in a sentence naming the fix, rather than a reason for the upgrade to
|
|
307
317
|
* fail. `check` and `doctor` both refuse loudly on the next run.
|
|
308
318
|
*/
|
|
309
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Contract 2 to contract 3, reported the same way its predecessor is.
|
|
321
|
+
*
|
|
322
|
+
* Loud about what moved, because this one moves credentials. An operator whose
|
|
323
|
+
* fifteen accounts were merged into one store should see that happen rather than
|
|
324
|
+
* discover it from a directory listing.
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
async function migrateLocal(root: string, say: (line: string) => void): Promise<boolean> {
|
|
310
328
|
try {
|
|
311
|
-
|
|
329
|
+
// One call, both steps, in order — a workspace that predates both walks
|
|
330
|
+
// through them rather than jumping, so each step's reasoning applies to the
|
|
331
|
+
// shape it was written for. `update` used to spell that walk itself, which
|
|
332
|
+
// is how `deploy` and `doctor --fix` came to spell only half of it.
|
|
333
|
+
//
|
|
334
|
+
// The subject is passed rather than left to be defaulted because this is
|
|
335
|
+
// the one caller that already had it in hand.
|
|
336
|
+
const session = await readSession();
|
|
337
|
+
const migration = await migrateToCurrentContract(root, {
|
|
338
|
+
apply: true,
|
|
339
|
+
...(session ? { subject: session.subject } : {}),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (migration.legacy) sayLegacyTargets(migration.legacy, say);
|
|
343
|
+
if (migration.contract3) sayContract3(migration.contract3, say);
|
|
344
|
+
if (migration.contract4) sayContract4(migration.contract4, say);
|
|
345
|
+
return true;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
say(`could not migrate this workspace: ${error instanceof Error ? error.message : String(error)}`);
|
|
348
|
+
|
|
349
|
+
// **Still behind, not "something threw".** Keying the repair on the throw
|
|
350
|
+
// suppressed it for the whole workspace when one profile failed to
|
|
351
|
+
// migrate — so the others, already current, never received a newly shipped
|
|
352
|
+
// surface, which is the case `update` runs the repair for at all. One
|
|
353
|
+
// unreadable profile is a per-profile warning, which the repair already
|
|
354
|
+
// gives it.
|
|
355
|
+
const behind = await stillBehind(root);
|
|
356
|
+
if (behind) say(' the owner-layer repair is skipped until it does');
|
|
357
|
+
return !behind;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
312
360
|
|
|
313
|
-
|
|
314
|
-
|
|
361
|
+
/** Anything still below the current contract. Unreadable counts as behind. */
|
|
362
|
+
function stillBehind(root: string): Promise<boolean> {
|
|
363
|
+
return needsMigration(root)
|
|
364
|
+
.then(async (one) => one || (await needsContract3(root)) || (await needsContract4(root)))
|
|
365
|
+
.catch(() => true);
|
|
366
|
+
}
|
|
315
367
|
|
|
368
|
+
/** Contract 1 to contract 2: targets move from the profile to the workspace. */
|
|
369
|
+
function sayLegacyTargets(
|
|
370
|
+
migration: NonNullable<ContractMigration['legacy']>,
|
|
371
|
+
say: (line: string) => void,
|
|
372
|
+
): void {
|
|
373
|
+
say(
|
|
374
|
+
`migrated ${migration.profiles.length} profile(s) to contract 2 — a target is declared by ` +
|
|
375
|
+
'the workspace now, not by each profile',
|
|
376
|
+
);
|
|
377
|
+
for (const change of migration.changes) say(` ${change}`);
|
|
378
|
+
|
|
379
|
+
for (const pointer of migration.targets.filter((one) => one.kind === 'pointer')) {
|
|
316
380
|
say(
|
|
317
|
-
`
|
|
318
|
-
|
|
381
|
+
` "${pointer.name}" points at ${pointer.where} — run ` +
|
|
382
|
+
`lanes link deploy --workspace ${pointer.name} to migrate what is there`,
|
|
319
383
|
);
|
|
320
|
-
for (const change of migration.changes) say(` ${change}`);
|
|
321
|
-
|
|
322
|
-
const pointers = migration.targets.filter((one) => one.kind === 'pointer');
|
|
323
|
-
for (const pointer of pointers) {
|
|
324
|
-
say(
|
|
325
|
-
` "${pointer.name}" points at ${pointer.where} — run ` +
|
|
326
|
-
`lanes link deploy --target ${pointer.name} to migrate what is there`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
} catch (error) {
|
|
330
|
-
say(`could not migrate this workspace: ${error instanceof Error ? error.message : String(error)}`);
|
|
331
384
|
}
|
|
332
385
|
}
|
|
386
|
+
|