@lanes-sh/link 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/instructions/agents/lanes-link-scout.md +1 -1
- package/instructions/skills/lanes-link/SKILL.md +33 -29
- package/package.json +2 -1
- package/src/cli/commands/connect/target-note.ts +2 -2
- package/src/cli/commands/knowledge/index.ts +14 -34
- package/src/cli/commands/mcp/register.ts +1 -1
- package/src/cli/commands/operate/dashboard.ts +2 -2
- package/src/cli/commands/operate/inspect.ts +5 -1
- package/src/cli/commands/operate/migrate.ts +85 -1
- package/src/cli/commands/operate/outputs.ts +7 -22
- package/src/cli/commands/operate/status.ts +84 -69
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/profile/removal.ts +36 -25
- package/src/cli/commands/profile/remove.ts +5 -2
- package/src/cli/commands/profile.ts +56 -40
- package/src/cli/commands/sync.ts +94 -162
- package/src/cli/commands/target.ts +115 -74
- package/src/cli/commands/update.ts +56 -1
- package/src/cli/config-edit.ts +47 -16
- package/src/cli/endpoint-url.ts +3 -3
- package/src/cli/main.ts +9 -7
- package/src/cli/publish.ts +4 -2
- package/src/cli/runtime/open.ts +19 -10
- package/src/cli/runtime/select.ts +69 -22
- package/src/cli/selection-require.ts +79 -0
- package/src/cli/selection.ts +44 -92
- package/src/cli/workspace-migrate.ts +385 -0
- package/src/deployments/bootstrap.ts +31 -11
- package/src/deployments/deploy.ts +54 -27
- package/src/deployments/knowledge.ts +5 -2
- package/src/deployments/prepare.ts +3 -1
- package/src/deployments/serving.ts +19 -15
- package/src/deployments/upload.ts +10 -1
- package/src/profile/deployments.ts +64 -53
- package/src/profile/index.ts +22 -6
- package/src/profile/legacy.ts +92 -0
- package/src/profile/load.ts +12 -28
- package/src/profile/registry.ts +182 -0
- package/src/profile/schema.ts +162 -110
- package/src/profile/targets.ts +62 -91
- package/src/profile/testing.ts +78 -0
- package/src/profile/workspace.ts +11 -25
- package/src/server/dashboard.ts +4 -1
- package/src/server/harness.ts +1 -6
- package/src/cli/commands/profile/declare.ts +0 -154
- package/src/deployments/servable.ts +0 -82
- package/src/deployments/sync-apply.ts +0 -330
- package/src/deployments/sync.ts +0 -164
|
@@ -68,7 +68,7 @@ stops a write is policy on the endpoint:
|
|
|
68
68
|
|
|
69
69
|
```console
|
|
70
70
|
$ lanes link policy deny memory.write --profile <name> --target <name>
|
|
71
|
-
$ lanes link policy list --profile <name>
|
|
71
|
+
$ lanes link policy list --profile <name> --target <name>
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
If you are running against a profile that grants writes, that is the owner's
|
|
@@ -22,7 +22,9 @@ and personal apart; they share no database and no credential store.
|
|
|
22
22
|
**Ask which profile is meant when it is ambiguous. Do not default to whichever
|
|
23
23
|
is listed first.** Quietly picking one crosses the line the profile exists to
|
|
24
24
|
draw. There is no "current profile" to switch — the choice is made per call, and
|
|
25
|
-
`lanes link profile list
|
|
25
|
+
`lanes link profile list --target <name>` shows what exists *in that target*. A
|
|
26
|
+
profile lives in exactly one, so `personal` on `local` and `personal` on `cloud`
|
|
27
|
+
are two profiles that share a name rather than one profile in two places.
|
|
26
28
|
|
|
27
29
|
**What a command must be told is never inferred — but it is not always both.**
|
|
28
30
|
Nothing resolves from an environment variable or a config default, so a command
|
|
@@ -30,12 +32,11 @@ missing what it needs refuses rather than acting somewhere else. Passing a flag
|
|
|
30
32
|
command does not read is refused too, which makes "add both to be safe" its own
|
|
31
33
|
failure. Four levels:
|
|
32
34
|
|
|
33
|
-
- **Neither.** `lanes link
|
|
35
|
+
- **Neither.** `lanes link target list`, `lanes link mcp list`,
|
|
34
36
|
`lanes link version`.
|
|
35
|
-
- **`--
|
|
36
|
-
`lanes link
|
|
37
|
-
|
|
38
|
-
YAML that applies wherever the profile runs.
|
|
37
|
+
- **`--target` alone.** `lanes link profile list`, `lanes link profile add`,
|
|
38
|
+
`lanes link profile remove`, `lanes link target show`. A profile lives inside
|
|
39
|
+
one target's workspace, so listing or creating one names which workspace.
|
|
39
40
|
- **`--target`, with the profiles derived from it.** `lanes link status`,
|
|
40
41
|
`lanes link deploy` and `lanes link sync targets` act on one endpoint serving
|
|
41
42
|
every profile that declares that target. `--profile` is accepted and *narrows*
|
|
@@ -222,7 +223,7 @@ granted, and retrying will not reveal it. A call that *is* refused was refused b
|
|
|
222
223
|
policy on purpose.
|
|
223
224
|
|
|
224
225
|
Report it plainly and let the owner decide whether to widen the grant —
|
|
225
|
-
`lanes link policy list --profile <name>` shows the rules, `lanes link policy allow <capability> --profile <name> --target <name>`
|
|
226
|
+
`lanes link policy list --profile <name> --target <name>` shows the rules, `lanes link policy allow <capability> --profile <name> --target <name>`
|
|
226
227
|
changes them, and that is their call, not yours. **Do not look for another route
|
|
227
228
|
to the same data.** Every call is audited either way; `lanes link audit tail --profile <name> --target <name>`
|
|
228
229
|
shows what was attempted, refusals included.
|
|
@@ -299,10 +300,12 @@ key on — do not treat the absence of JSON as a failure. `status`, `doctor`,
|
|
|
299
300
|
`config show`, `audit tail` and every `mcp` subcommand do not.
|
|
300
301
|
|
|
301
302
|
**A profile is created and removed, never switched.** `lanes link profile add
|
|
302
|
-
<name>` writes a new one
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
the profile
|
|
303
|
+
<name> --target <name>` writes a new one *into that target's workspace* — one
|
|
304
|
+
target, not a list, because a profile lives in exactly one.
|
|
305
|
+
`lanes link profile remove <name> --target <name>` takes `--dry-run` and then
|
|
306
|
+
`--yes`, and removes the profile itself along with its stores: the file is in
|
|
307
|
+
that workspace, so there is nowhere left for it to survive. Neither reads
|
|
308
|
+
`--profile`; both name the profile positionally.
|
|
306
309
|
|
|
307
310
|
There is no current profile and no default target. `lanes link profile default`
|
|
308
311
|
and `lanes link target use` are gone and now refuse with an explanation — if you
|
|
@@ -312,9 +315,9 @@ replacement to find. The choice is made per command, on purpose.
|
|
|
312
315
|
## Deploying, and what it decides
|
|
313
316
|
|
|
314
317
|
`lanes link deploy` builds an image and rolls a revision. Its subject is a
|
|
315
|
-
**target**, and the profiles behind it are every profile
|
|
316
|
-
so one deploy serves all of them — there is no per-profile deploy to
|
|
317
|
-
reason to loop over them.
|
|
318
|
+
**target**, and the profiles behind it are every profile *in* that target's
|
|
319
|
+
workspace, so one deploy serves all of them — there is no per-profile deploy to
|
|
320
|
+
run and no reason to loop over them.
|
|
318
321
|
|
|
319
322
|
**Always `--dry-run` first, and show what it printed.** It creates cloud
|
|
320
323
|
resources that cost money, it implements no `--json` to inspect instead, and that
|
|
@@ -326,9 +329,9 @@ Two things it refuses to guess, and both are the owner's to answer:
|
|
|
326
329
|
- **Whose bearer token opens the endpoint.** One token reaches every profile
|
|
327
330
|
behind that target, so this decides who gets in. With several candidates and
|
|
328
331
|
nothing recorded, it refuses and prints the command that names one.
|
|
329
|
-
- **A first deploy.** A target
|
|
330
|
-
so `--profile` is required there. It may be repeated, and the first
|
|
331
|
-
is the primary.
|
|
332
|
+
- **A first deploy.** A target that does not exist yet has no workspace to derive
|
|
333
|
+
a set from, so `--profile` is required there. It may be repeated, and the first
|
|
334
|
+
one named is the primary.
|
|
332
335
|
|
|
333
336
|
**Never pass `--yes`, `--non-interactive`, `--access public` or
|
|
334
337
|
`--service-account` yourself.** Each settles a question about who can reach their
|
|
@@ -338,22 +341,23 @@ this file already applies to `--accept-broad-scopes`.
|
|
|
338
341
|
Deploying is how new code reaches the endpoint. It is not how an account gets
|
|
339
342
|
connected and not how a config change lands; both of those publish themselves.
|
|
340
343
|
|
|
341
|
-
## When
|
|
344
|
+
## When a machine has lost track of a deployment
|
|
342
345
|
|
|
343
|
-
A
|
|
344
|
-
machine, a reinstall, a
|
|
345
|
-
is still serving; what went missing is the
|
|
346
|
-
is a `lanes link status` that reports nothing
|
|
347
|
-
up.
|
|
346
|
+
A workspace holds a *pointer* to each target it does not itself hold, and a
|
|
347
|
+
machine can lose one — a new laptop, a reinstall, a workspace file restored from
|
|
348
|
+
something older. The endpoint is still serving; what went missing is the line
|
|
349
|
+
saying where it lives. The symptom is a `lanes link status` that reports nothing
|
|
350
|
+
for a target you know is up.
|
|
348
351
|
|
|
349
|
-
`lanes link sync targets`
|
|
350
|
-
|
|
351
|
-
`--dry-run` reports what it would
|
|
352
|
+
`lanes link sync targets` adopts it. `--discover` looks for a deployment the
|
|
353
|
+
workspace has no pointer to, `--from <location>` names one directly, and
|
|
354
|
+
`--dry-run` reports what it would write without writing it. Run the dry run and
|
|
352
355
|
show it.
|
|
353
356
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
+
**It cannot lose anything.** Adopting writes one line — the pointer — and the
|
|
358
|
+
workspace at the other end stays authoritative for everything else. There is no
|
|
359
|
+
`--prefer` any more and passing one is refused: it chose a winner when a profile
|
|
360
|
+
existed in two copies that could disagree, and there is one copy now.
|
|
357
361
|
|
|
358
362
|
## Registering it, and re-registering it
|
|
359
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanes-sh/link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://lanes.sh/link",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"#dispatch": "./src/dispatch/index.ts",
|
|
67
67
|
"#policy": "./src/policy/index.ts",
|
|
68
68
|
"#profile": "./src/profile/index.ts",
|
|
69
|
+
"#profile/*": "./src/profile/*",
|
|
69
70
|
"#providers/*": "./src/providers/*",
|
|
70
71
|
"#registry": "./src/registry/index.ts",
|
|
71
72
|
"#secrets": "./src/secrets/index.ts",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Resolution } from '#profile';
|
|
2
2
|
import { announce } from '../../output.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -21,7 +21,7 @@ import { announce } from '../../output.ts';
|
|
|
21
21
|
* is the same mistake caught where it is still cheap.
|
|
22
22
|
*/
|
|
23
23
|
export function announceConnectTarget(
|
|
24
|
-
runtime: { readonly resolution: Resolution
|
|
24
|
+
runtime: { readonly resolution: Resolution },
|
|
25
25
|
json?: boolean | undefined,
|
|
26
26
|
): void {
|
|
27
27
|
// `emit`'s early return only protects lines printed *at* the emit, and this
|
|
@@ -230,7 +230,6 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
|
|
|
230
230
|
|
|
231
231
|
print('');
|
|
232
232
|
print(ok(`memory and skills are now kept in ${facts.fullName}`));
|
|
233
|
-
reportOtherTargets(runtime.config, runtime.target, knowledge.token_ref);
|
|
234
233
|
} finally {
|
|
235
234
|
await runtime.close();
|
|
236
235
|
}
|
|
@@ -242,7 +241,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
|
|
|
242
241
|
announce(runtime.resolution);
|
|
243
242
|
const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
|
|
244
243
|
|
|
245
|
-
const knowledge = runtime.config.
|
|
244
|
+
const knowledge = runtime.config.knowledge;
|
|
246
245
|
if (!knowledge) {
|
|
247
246
|
print(style.dim(' This profile already keeps memory and skills on its own storage.'));
|
|
248
247
|
return;
|
|
@@ -299,8 +298,7 @@ async function openLocalStores(
|
|
|
299
298
|
runtime: Runtime,
|
|
300
299
|
): Promise<{ storage: BlobStore; skills: BlobStore }> {
|
|
301
300
|
const { openStorage } = await import('#deployments/target.ts');
|
|
302
|
-
const declared = runtime.
|
|
303
|
-
if (!declared) throw new ConfigError(`Target "${runtime.target}" is not declared`);
|
|
301
|
+
const declared = runtime.declared;
|
|
304
302
|
|
|
305
303
|
const factory = await openStorage(
|
|
306
304
|
{
|
|
@@ -314,7 +312,14 @@ async function openLocalStores(
|
|
|
314
312
|
return { storage: factory(), skills: factory(layout.skills(runtime.config.instance.profile)) };
|
|
315
313
|
}
|
|
316
314
|
|
|
317
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Write, or remove, this profile's knowledge block.
|
|
317
|
+
*
|
|
318
|
+
* One place, not one per target. It used to loop over every target the profile
|
|
319
|
+
* declared and write the same block into each — which is what a per-target key
|
|
320
|
+
* holding a per-profile fact costs. The block is on the profile now (ADR-052),
|
|
321
|
+
* so there is one of it.
|
|
322
|
+
*/
|
|
318
323
|
async function writeBlock(
|
|
319
324
|
config: Config,
|
|
320
325
|
root: string,
|
|
@@ -322,12 +327,10 @@ async function writeBlock(
|
|
|
322
327
|
): Promise<void> {
|
|
323
328
|
const document = await ConfigDocument.open(root, config.instance.profile);
|
|
324
329
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
document.setIn(['targets', target, 'knowledge'], {
|
|
330
|
+
if (knowledge === undefined) {
|
|
331
|
+
document.removeIn(['knowledge']);
|
|
332
|
+
} else {
|
|
333
|
+
document.setIn(['knowledge'], {
|
|
331
334
|
adapter: knowledge.adapter,
|
|
332
335
|
repo: knowledge.repo,
|
|
333
336
|
...(knowledge.branch ? { branch: knowledge.branch } : {}),
|
|
@@ -339,29 +342,6 @@ async function writeBlock(
|
|
|
339
342
|
await document.save();
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
/**
|
|
343
|
-
* The other targets need the same token in their own credential stores.
|
|
344
|
-
*
|
|
345
|
-
* Not written for them: a second target's store is Secret Manager, which needs
|
|
346
|
-
* cloud credentials this command has no business assuming. `secrets push` is
|
|
347
|
-
* the command that already does exactly this, so this points at it.
|
|
348
|
-
*/
|
|
349
|
-
function reportOtherTargets(config: Config, current: string, ref: string): void {
|
|
350
|
-
const others = Object.keys(config.targets).filter((target) => target !== current);
|
|
351
|
-
if (others.length === 0) return;
|
|
352
|
-
|
|
353
|
-
const profile = config.instance.profile;
|
|
354
|
-
|
|
355
|
-
print(
|
|
356
|
-
style.dim(
|
|
357
|
-
` Written into every target (${others.join(', ')} as well). Each reads "${ref}" from its own credential store:`,
|
|
358
|
-
),
|
|
359
|
-
);
|
|
360
|
-
for (const target of others) {
|
|
361
|
-
print(style.dim(` lanes link secrets push --from ${current} --to ${target} --profile ${profile}`));
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
345
|
async function decideMigration(hasContent: boolean, flags: KnowledgeFlags): Promise<boolean> {
|
|
366
346
|
if (flags.migrate !== undefined) return flags.migrate;
|
|
367
347
|
if (!hasContent) return false;
|
|
@@ -77,7 +77,7 @@ export async function mcpAdd(target: string | undefined, options: McpAddOptions)
|
|
|
77
77
|
// `http://<host>:<port>/mcp` unconditionally, so `mcp add --target cloud`
|
|
78
78
|
// registered loopback with the agent: a registration that reports success,
|
|
79
79
|
// names the right server, and points at a port with nothing behind it.
|
|
80
|
-
const url = await endpointUrl(runtime.config, runtime.
|
|
80
|
+
const url = await endpointUrl(runtime.config, runtime.declared);
|
|
81
81
|
const input: AddInput = {
|
|
82
82
|
name,
|
|
83
83
|
url,
|
|
@@ -36,10 +36,10 @@ export async function dashboard(flags: DashboardFlags): Promise<void> {
|
|
|
36
36
|
// still be correct and nobody would ever read it, because a storage 403
|
|
37
37
|
// arrives first. This is the seam `runtime.ts` documents `deploy` and
|
|
38
38
|
// `secrets push` stopping at, for the same reason.
|
|
39
|
-
const { resolution, config, target } = await resolveProfile(flags);
|
|
39
|
+
const { resolution, config, target, resolved } = await resolveProfile(flags);
|
|
40
40
|
announce(resolution);
|
|
41
41
|
|
|
42
|
-
const deployed = deploymentIdentity(
|
|
42
|
+
const deployed = deploymentIdentity(resolved?.declared.deploy);
|
|
43
43
|
if (deployed) {
|
|
44
44
|
throw new Error(
|
|
45
45
|
`Target "${target}" is deployed to ${deployed.platform}, and the dashboard is served only ` +
|
|
@@ -5,7 +5,7 @@ import { staleNudge } from '../../release.ts';
|
|
|
5
5
|
import { openRuntime, resolveProfileOnly, type GlobalFlags, type Runtime } from '../../runtime.ts';
|
|
6
6
|
import type { FetchLike } from '#deployments/knowledge.ts';
|
|
7
7
|
import { credentialAge, reportCapabilityDrift } from './findings.ts';
|
|
8
|
-
import { migratedRenamedProviders } from './migrate.ts';
|
|
8
|
+
import { migratedContract, migratedRenamedProviders } from './migrate.ts';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The gate order — check, doctor, plan, start — exists so failures surface in
|
|
@@ -75,6 +75,10 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
75
75
|
try {
|
|
76
76
|
runtime = await openRuntime(flags, { fetch: flags.fetch });
|
|
77
77
|
} catch (refusal) {
|
|
78
|
+
// Contract first: it is checked before the schema, so a profile carrying
|
|
79
|
+
// both problems reports the contract one and the rename cannot be seen until
|
|
80
|
+
// that is settled.
|
|
81
|
+
if (await migratedContract(flags, refusal)) return;
|
|
78
82
|
if (await migratedRenamedProviders(flags, refusal)) return;
|
|
79
83
|
throw refusal;
|
|
80
84
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ConfigError, resolveSelection } from '#profile';
|
|
1
|
+
import { ConfigError, resolveSelection, resolveTargetWorkspace, resolveWorkspaceRoot } from '#profile';
|
|
2
2
|
import { ConfigDocument } from '../../config-edit.ts';
|
|
3
3
|
import { migrateRenamedProviders, pendingRenames, shapeOf } from '../../config-migrate.ts';
|
|
4
|
+
import { migrateWorkspace, needsMigration } from '../../workspace-migrate.ts';
|
|
4
5
|
import { emit, fail, ok, print, style, warn } from '../../output.ts';
|
|
5
6
|
import { openSecretStoreFor, type GlobalFlags } from '../../runtime.ts';
|
|
6
7
|
|
|
@@ -98,3 +99,86 @@ export async function migratedRenamedProviders(
|
|
|
98
99
|
|
|
99
100
|
return true;
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether a refusal to load was a stale contract, and — with `--fix` — raise it.
|
|
106
|
+
*
|
|
107
|
+
* The sibling of `migratedRenamedProviders` above, and asked *before* it: a
|
|
108
|
+
* contract-1 profile is refused by `assertSupportedContract`, which runs before
|
|
109
|
+
* the schema, so a file with both problems reports this one and the rename is
|
|
110
|
+
* invisible until it is fixed.
|
|
111
|
+
*
|
|
112
|
+
* Here for the reason this file's header already gives — `doctor` is what
|
|
113
|
+
* someone runs when something is broken, so it has to be the command that works
|
|
114
|
+
* when everything else refuses. `update` migrates the local workspace on its own
|
|
115
|
+
* and most people will never reach this; the ones who do are the ones whose
|
|
116
|
+
* `--target` is a bucket, which `update` deliberately leaves alone.
|
|
117
|
+
*
|
|
118
|
+
* **It will migrate a remote workspace, and says what that costs.** The endpoint
|
|
119
|
+
* in front of a bucket runs a pinned image, and one built before contract 2
|
|
120
|
+
* cannot read what this writes. That is a real consequence and it is the
|
|
121
|
+
* operator's to accept, so the line naming the redeploy is printed whether or
|
|
122
|
+
* not `--fix` was passed.
|
|
123
|
+
*/
|
|
124
|
+
export async function migratedContract(flags: RenameFlags, refusal: unknown): Promise<boolean> {
|
|
125
|
+
if (!(refusal instanceof ConfigError)) return false;
|
|
126
|
+
|
|
127
|
+
const root = resolveWorkspaceRoot();
|
|
128
|
+
const target = flags.target ?? '';
|
|
129
|
+
// Where the profiles actually are. For a pointer that is the bucket, which is
|
|
130
|
+
// the case this exists for; a target the registry cannot resolve is not a
|
|
131
|
+
// migration problem and falls through to the original refusal.
|
|
132
|
+
const where = await resolveTargetWorkspace(root, target).catch(() => null);
|
|
133
|
+
if (where === null) return false;
|
|
134
|
+
|
|
135
|
+
if (!(await needsMigration(where))) return false;
|
|
136
|
+
|
|
137
|
+
const migration = await migrateWorkspace(where, { apply: flags.fix === true });
|
|
138
|
+
const applied = flags.fix === true;
|
|
139
|
+
const remote = where !== root;
|
|
140
|
+
|
|
141
|
+
if (!applied) process.exitCode = 1;
|
|
142
|
+
|
|
143
|
+
await emit(
|
|
144
|
+
flags.json,
|
|
145
|
+
{
|
|
146
|
+
ok: applied,
|
|
147
|
+
workspace: where,
|
|
148
|
+
target,
|
|
149
|
+
applied,
|
|
150
|
+
remote,
|
|
151
|
+
profiles: migration.profiles,
|
|
152
|
+
targets: migration.targets,
|
|
153
|
+
changes: migration.changes,
|
|
154
|
+
},
|
|
155
|
+
() => {
|
|
156
|
+
print(`workspace ${style.bold(where)} target ${style.bold(target)}`);
|
|
157
|
+
print();
|
|
158
|
+
print(
|
|
159
|
+
applied
|
|
160
|
+
? ok('migrated to contract 2 — a target is declared by the workspace, not by each profile')
|
|
161
|
+
: warn('this workspace is contract 1, and nothing here reads that any more'),
|
|
162
|
+
);
|
|
163
|
+
for (const change of migration.changes) print(` ${change}`);
|
|
164
|
+
|
|
165
|
+
if (remote) {
|
|
166
|
+
print();
|
|
167
|
+
print(
|
|
168
|
+
style.dim(
|
|
169
|
+
applied
|
|
170
|
+
? ' The endpoint serving this bucket is running an older image and cannot read\n' +
|
|
171
|
+
` what was just written. Roll a new one: lanes link deploy --target ${target}`
|
|
172
|
+
: ` lanes link deploy --target ${target} migrates it and ships the image that\n` +
|
|
173
|
+
' understands it, in one step. Prefer that to --fix here.',
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!applied) print(style.dim(` Fix it with: lanes link doctor --fix --target ${target}`));
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { listProfiles } from '#profile';
|
|
2
|
-
import { unservableProfiles } from '#deployments/servable.ts';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import { deployedUrl, endpointHealth, localUrl } from '../../endpoint-url.ts';
|
|
5
4
|
import { announce, heading, print, style, warn } from '../../output.ts';
|
|
@@ -28,7 +27,7 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
28
27
|
|
|
29
28
|
try {
|
|
30
29
|
const { token } = await ensureProfileToken(runtime.credentials, runtime.config.auth.token_ref);
|
|
31
|
-
const declared = runtime.
|
|
30
|
+
const declared = runtime.declared.deploy;
|
|
32
31
|
const deployed = await deployedUrl(declared);
|
|
33
32
|
// Not `endpointUrl`, which would ask the platform a second time for an
|
|
34
33
|
// answer this line already has.
|
|
@@ -37,14 +36,12 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
37
36
|
const live = await endpointHealth(url, token);
|
|
38
37
|
const mine = live?.profile === runtime.resolution.profile;
|
|
39
38
|
|
|
40
|
-
// Live if it is up, otherwise
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// that
|
|
44
|
-
//
|
|
45
|
-
const profiles = mine
|
|
46
|
-
? live.profiles
|
|
47
|
-
: await servable(runtime.resolution.workspaceRoot, runtime.target);
|
|
39
|
+
// Live if it is up, otherwise every profile in this target's workspace.
|
|
40
|
+
// Those are the same set now: a profile lives in exactly one target
|
|
41
|
+
// (ADR-052), so every profile here is one this target can open. The filter
|
|
42
|
+
// that used to sit between them answered "which of these declare it", a
|
|
43
|
+
// question no profile has an opinion on any more.
|
|
44
|
+
const profiles = mine ? live.profiles : await listProfiles(runtime.resolution.workspaceRoot);
|
|
48
45
|
|
|
49
46
|
if (flags.json) {
|
|
50
47
|
print(
|
|
@@ -138,18 +135,6 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
/** The profiles a `start` on this target would actually open. */
|
|
142
|
-
async function servable(workspaceRoot: string, target: string): Promise<string[]> {
|
|
143
|
-
const all = await listProfiles(workspaceRoot);
|
|
144
|
-
const cannot = new Set(
|
|
145
|
-
(await unservableProfiles({ workspaceRoot, profiles: undefined, target })).map(
|
|
146
|
-
(one) => one.profile,
|
|
147
|
-
),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
return all.filter((name) => !cannot.has(name));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
138
|
/**
|
|
154
139
|
* The command that prints this endpoint's token, verified to actually work.
|
|
155
140
|
*
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isPointer,
|
|
3
|
+
listProfiles,
|
|
4
|
+
loadWorkspaceProfiles,
|
|
5
|
+
notInRegistry,
|
|
6
|
+
openTarget,
|
|
7
|
+
readRegistry,
|
|
8
|
+
resolveWorkspaceRoot,
|
|
9
|
+
type ResolvedTarget,
|
|
10
|
+
} from '#profile';
|
|
2
11
|
import { oneProfile, visibleCapabilities } from '#server/mcp';
|
|
3
12
|
import { toPolicyDocument } from '#registry';
|
|
4
13
|
import { announce, emit, heading, print, style, table } from '../../output.ts';
|
|
@@ -77,7 +86,7 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
77
86
|
// replaces it. Silently turning a string into an object would be a cost
|
|
78
87
|
// paid by every reader for the benefit of none.
|
|
79
88
|
const local = `http://${runtime.config.instance.host}:${runtime.config.instance.port}/mcp`;
|
|
80
|
-
const deployment = deploymentIdentity(runtime.
|
|
89
|
+
const deployment = deploymentIdentity(runtime.declared.deploy);
|
|
81
90
|
const endpoint = deployment ? null : local;
|
|
82
91
|
|
|
83
92
|
return emit(
|
|
@@ -152,97 +161,103 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
152
161
|
* profile narrows it to the detailed view above; naming none is not a missing
|
|
153
162
|
* answer, it is the wider one.
|
|
154
163
|
*
|
|
155
|
-
* **Opens no store
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* it is not.
|
|
164
|
+
* **Opens no profile store.** The view above already declines to probe, for the
|
|
165
|
+
* reasons in this file's header; this one additionally declines to open a
|
|
166
|
+
* target's adapters, so it stays fast and — more to the point — still answers
|
|
167
|
+
* for a target whose stores are unreachable or gone. A summary that needs the
|
|
168
|
+
* deployment to be healthy cannot report that it is not.
|
|
161
169
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
170
|
+
* It does read the target's workspace, which for a pointer is a bucket. That is
|
|
171
|
+
* the one call it cannot avoid: the profiles it is reporting on live there
|
|
172
|
+
* (ADR-052). When that read fails it says so under the target's own heading
|
|
173
|
+
* rather than printing an empty profile list, because "no profiles" and "could
|
|
174
|
+
* not look" are the two answers this command must never blur.
|
|
175
|
+
*
|
|
176
|
+
* The row it used to exist for — a target declared by one profile and not its
|
|
177
|
+
* sibling — cannot happen now. A target is declared once, by its workspace, so
|
|
178
|
+
* there is no per-profile disagreement left to surface.
|
|
165
179
|
*/
|
|
166
180
|
async function workspaceStatus(flags: StatusFlags): Promise<void> {
|
|
167
181
|
const target = flags.target!;
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const profiles = workspace.loaded.map((entry) => ({
|
|
172
|
-
name: entry.profile,
|
|
173
|
-
declares: target in entry.config.targets,
|
|
174
|
-
connections: entry.config.connections.length,
|
|
175
|
-
deployment: deploymentIdentity(entry.config.targets[target]?.deploy),
|
|
176
|
-
}));
|
|
182
|
+
const localRoot = resolveWorkspaceRoot();
|
|
183
|
+
const registry = await readRegistry(localRoot);
|
|
184
|
+
const entry = registry[target];
|
|
177
185
|
|
|
178
|
-
|
|
186
|
+
if (!entry) throw notInRegistry(target, registry, localRoot);
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
let resolved: ResolvedTarget | undefined;
|
|
189
|
+
let unreachable: string | undefined;
|
|
190
|
+
try {
|
|
191
|
+
resolved = await openTarget(localRoot, target);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// The whole message, not its first line. Truncating to `split('\n')[0]` is
|
|
194
|
+
// exactly what the sync command used to do, and it is why a bucket refusing
|
|
195
|
+
// for a nameable reason reported an empty one instead — a `ConfigError`
|
|
196
|
+
// carries its fix on the lines after the first.
|
|
197
|
+
unreachable = error instanceof Error ? error.message : String(error);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const root = resolved?.workspaceRoot ?? (isPointer(entry) ? entry.workspace : localRoot);
|
|
201
|
+
const workspace = resolved ? await loadWorkspaceProfiles(root) : undefined;
|
|
202
|
+
|
|
203
|
+
const profiles = (workspace?.loaded ?? []).map((loaded) => ({
|
|
204
|
+
name: loaded.profile,
|
|
205
|
+
connections: loaded.config.connections.length,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
const deployment = deploymentIdentity(resolved?.declared.deploy);
|
|
191
209
|
|
|
192
210
|
return emit(
|
|
193
211
|
flags.json,
|
|
194
|
-
{
|
|
212
|
+
{
|
|
213
|
+
workspace: root,
|
|
214
|
+
target,
|
|
215
|
+
remote: resolved?.remote ?? isPointer(entry),
|
|
216
|
+
reachable: unreachable === undefined,
|
|
217
|
+
...(unreachable ? { error: unreachable } : {}),
|
|
218
|
+
profiles,
|
|
219
|
+
deployment,
|
|
220
|
+
unreadable: workspace?.unreadable ?? [],
|
|
221
|
+
},
|
|
195
222
|
() => {
|
|
196
223
|
print(style.dim(`workspace ${style.bold(root)} target ${style.bold(target)}`));
|
|
197
224
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
]),
|
|
205
|
-
);
|
|
206
|
-
for (const bad of workspace.unreadable) {
|
|
207
|
-
print(` ${style.bold(bad.profile)} ${style.yellow(bad.reason)}`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
heading('Endpoint');
|
|
211
|
-
if (declaring.length === 0) {
|
|
212
|
-
print(style.yellow(` No profile declares "${target}".`));
|
|
213
|
-
print(
|
|
214
|
-
style.dim(
|
|
215
|
-
' If it was deployed once, the deployment still exists and only the\n' +
|
|
216
|
-
` declaration is gone — recover it with: lanes link sync targets --target ${target}`,
|
|
217
|
-
),
|
|
218
|
-
);
|
|
225
|
+
if (unreachable !== undefined) {
|
|
226
|
+
heading('Unreachable');
|
|
227
|
+
// The refusal's own wording, and nothing after it. A generic trailer
|
|
228
|
+
// here read as a second, vaguer diagnosis of a problem the message above
|
|
229
|
+
// had already named precisely.
|
|
230
|
+
for (const line of unreachable.split('\n')) print(` ${style.yellow(line)}`);
|
|
219
231
|
return;
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
heading('Profiles');
|
|
235
|
+
if (profiles.length === 0) {
|
|
236
|
+
print(style.dim(' None yet.'));
|
|
237
|
+
print(style.dim(` Create one with: lanes link profile add <name> --target ${target}`));
|
|
238
|
+
} else {
|
|
239
|
+
table(
|
|
240
|
+
profiles.map((profile) => [
|
|
241
|
+
` ${style.bold(profile.name)}`,
|
|
242
|
+
style.dim(`${profile.connections} connection(s)`),
|
|
243
|
+
]),
|
|
244
|
+
);
|
|
225
245
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const { platform, service, region } = deployment;
|
|
229
|
-
print(` ${platform} service ${style.bold(service)} in ${region}`);
|
|
246
|
+
for (const bad of workspace?.unreadable ?? []) {
|
|
247
|
+
print(` ${style.bold(bad.profile)} ${style.yellow(bad.reason)}`);
|
|
230
248
|
}
|
|
231
249
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
` ${deployments.length} profiles declare "${target}" against different services.`,
|
|
236
|
-
),
|
|
237
|
-
);
|
|
238
|
-
print(style.dim(' They are separate endpoints sharing a name. Check each profile.'));
|
|
250
|
+
heading('Endpoint');
|
|
251
|
+
if (!deployment) {
|
|
252
|
+
print(style.dim(' No deployment — this target runs wherever the CLI does.'));
|
|
239
253
|
return;
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
print(` ${deployment.platform} service ${style.bold(deployment.service)} in ${deployment.region}`);
|
|
242
257
|
print(
|
|
243
258
|
style.dim(
|
|
244
259
|
" the address is the platform's to assign — run: " +
|
|
245
|
-
`lanes link outputs --target ${target} --profile ${
|
|
260
|
+
`lanes link outputs --target ${target} --profile ${profiles[0]?.name ?? '<name>'}`,
|
|
246
261
|
),
|
|
247
262
|
);
|
|
248
263
|
},
|
|
@@ -28,7 +28,7 @@ export async function tools(flags: ToolsFlags): Promise<void> {
|
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
30
|
const { token } = await ensureProfileToken(runtime.credentials, runtime.config.auth.token_ref);
|
|
31
|
-
const declared = runtime.
|
|
31
|
+
const declared = runtime.declared.deploy;
|
|
32
32
|
const deployed = await deployedUrl(declared);
|
|
33
33
|
// Not `endpointUrl`, which asks the platform a second time for an answer
|
|
34
34
|
// this line already has.
|