@lanes-sh/link 0.5.3 → 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/custom/index.ts +2 -0
- package/src/cli/commands/connect/custom/spec.ts +1 -0
- package/src/cli/commands/connect/declare.ts +76 -0
- package/src/cli/commands/connect/family.ts +19 -3
- package/src/cli/commands/connect/index.ts +23 -24
- package/src/cli/commands/connect/outcome.ts +8 -0
- package/src/cli/commands/connect/settle.ts +94 -22
- package/src/cli/commands/connect/target-note.ts +2 -2
- package/src/cli/commands/connection.ts +18 -7
- 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 +95 -70
- 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/dashboard-page.ts +10 -1
- package/src/cli/endpoint-url.ts +3 -3
- package/src/cli/main.ts +11 -7
- package/src/cli/publish.ts +4 -2
- package/src/cli/runtime/open.ts +20 -10
- package/src/cli/runtime/select.ts +69 -22
- package/src/cli/selection-require.ts +79 -0
- package/src/cli/selection.ts +45 -92
- package/src/cli/usage.ts +3 -0
- 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 +171 -110
- package/src/profile/targets.ts +62 -91
- package/src/profile/testing.ts +78 -0
- package/src/profile/workspace.ts +11 -25
- package/src/providers/setup/provider.ts +10 -2
- package/src/server/dashboard.ts +5 -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
|
@@ -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';
|
|
@@ -46,6 +55,9 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
46
55
|
provider: connection.provider,
|
|
47
56
|
id: connection.id,
|
|
48
57
|
account: connection.account,
|
|
58
|
+
// Null rather than absent: a caller reading this to prefill a rename box
|
|
59
|
+
// needs to tell "called nothing in particular" from a field it forgot.
|
|
60
|
+
label: connection.label ?? null,
|
|
49
61
|
state: byKey.get(key)?.status ?? 'not reconciled',
|
|
50
62
|
};
|
|
51
63
|
});
|
|
@@ -74,7 +86,7 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
74
86
|
// replaces it. Silently turning a string into an object would be a cost
|
|
75
87
|
// paid by every reader for the benefit of none.
|
|
76
88
|
const local = `http://${runtime.config.instance.host}:${runtime.config.instance.port}/mcp`;
|
|
77
|
-
const deployment = deploymentIdentity(runtime.
|
|
89
|
+
const deployment = deploymentIdentity(runtime.declared.deploy);
|
|
78
90
|
const endpoint = deployment ? null : local;
|
|
79
91
|
|
|
80
92
|
return emit(
|
|
@@ -102,7 +114,14 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
102
114
|
connection.state === 'active'
|
|
103
115
|
? style.green(connection.state)
|
|
104
116
|
: style.yellow(connection.state),
|
|
105
|
-
|
|
117
|
+
// Both, where they differ. The label is what the operator called
|
|
118
|
+
// it and the account is which mailbox it is; a row answering only
|
|
119
|
+
// one of those is the row this column already was.
|
|
120
|
+
style.dim(
|
|
121
|
+
connection.label && connection.label !== connection.account
|
|
122
|
+
? `${connection.label} — ${connection.account}`
|
|
123
|
+
: connection.account,
|
|
124
|
+
),
|
|
106
125
|
]),
|
|
107
126
|
);
|
|
108
127
|
}
|
|
@@ -142,97 +161,103 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
142
161
|
* profile narrows it to the detailed view above; naming none is not a missing
|
|
143
162
|
* answer, it is the wider one.
|
|
144
163
|
*
|
|
145
|
-
* **Opens no store
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* 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.
|
|
151
169
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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.
|
|
155
179
|
*/
|
|
156
180
|
async function workspaceStatus(flags: StatusFlags): Promise<void> {
|
|
157
181
|
const target = flags.target!;
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const profiles = workspace.loaded.map((entry) => ({
|
|
162
|
-
name: entry.profile,
|
|
163
|
-
declares: target in entry.config.targets,
|
|
164
|
-
connections: entry.config.connections.length,
|
|
165
|
-
deployment: deploymentIdentity(entry.config.targets[target]?.deploy),
|
|
166
|
-
}));
|
|
182
|
+
const localRoot = resolveWorkspaceRoot();
|
|
183
|
+
const registry = await readRegistry(localRoot);
|
|
184
|
+
const entry = registry[target];
|
|
167
185
|
|
|
168
|
-
|
|
186
|
+
if (!entry) throw notInRegistry(target, registry, localRoot);
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
209
|
|
|
182
210
|
return emit(
|
|
183
211
|
flags.json,
|
|
184
|
-
{
|
|
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
|
+
},
|
|
185
222
|
() => {
|
|
186
223
|
print(style.dim(`workspace ${style.bold(root)} target ${style.bold(target)}`));
|
|
187
224
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
]),
|
|
195
|
-
);
|
|
196
|
-
for (const bad of workspace.unreadable) {
|
|
197
|
-
print(` ${style.bold(bad.profile)} ${style.yellow(bad.reason)}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
heading('Endpoint');
|
|
201
|
-
if (declaring.length === 0) {
|
|
202
|
-
print(style.yellow(` No profile declares "${target}".`));
|
|
203
|
-
print(
|
|
204
|
-
style.dim(
|
|
205
|
-
' If it was deployed once, the deployment still exists and only the\n' +
|
|
206
|
-
` declaration is gone — recover it with: lanes link sync targets --target ${target}`,
|
|
207
|
-
),
|
|
208
|
-
);
|
|
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)}`);
|
|
209
231
|
return;
|
|
210
232
|
}
|
|
211
233
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
);
|
|
215
245
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const { platform, service, region } = deployment;
|
|
219
|
-
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)}`);
|
|
220
248
|
}
|
|
221
249
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
` ${deployments.length} profiles declare "${target}" against different services.`,
|
|
226
|
-
),
|
|
227
|
-
);
|
|
228
|
-
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.'));
|
|
229
253
|
return;
|
|
230
254
|
}
|
|
231
255
|
|
|
256
|
+
print(` ${deployment.platform} service ${style.bold(deployment.service)} in ${deployment.region}`);
|
|
232
257
|
print(
|
|
233
258
|
style.dim(
|
|
234
259
|
" the address is the platform's to assign — run: " +
|
|
235
|
-
`lanes link outputs --target ${target} --profile ${
|
|
260
|
+
`lanes link outputs --target ${target} --profile ${profiles[0]?.name ?? '<name>'}`,
|
|
236
261
|
),
|
|
237
262
|
);
|
|
238
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.
|
|
@@ -82,8 +82,17 @@ export interface RemovalPlan {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export interface PlanOptions {
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The target whose stores this plans against.
|
|
87
|
+
*
|
|
88
|
+
* Required now, and not a restriction: a profile lives in exactly one target
|
|
89
|
+
* (ADR-052), so there is no "all of them" left for this to mean. It used to be
|
|
90
|
+
* optional because a profile could declare several and removing it meant
|
|
91
|
+
* emptying each.
|
|
92
|
+
*/
|
|
93
|
+
readonly target: string;
|
|
94
|
+
/** That target's adapter set, from the workspace declaring it (ADR-052). */
|
|
95
|
+
readonly declared: TargetConfig;
|
|
87
96
|
readonly openSecrets: (target: string) => Promise<SecretStore>;
|
|
88
97
|
readonly openBlobs: (target: string, area?: string) => Promise<BlobStore>;
|
|
89
98
|
readonly readDefaultProfile?: (() => Promise<string | undefined>) | undefined;
|
|
@@ -110,14 +119,13 @@ export async function removalPlan(
|
|
|
110
119
|
const untouched: { target: string; refs: string[] }[] = [];
|
|
111
120
|
const warnings: string[] = [];
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
// One target, always. A profile lived in as many as it declared and this
|
|
123
|
+
// planned across all of them; it lives in exactly one now (ADR-052), and that
|
|
124
|
+
// one is whichever workspace the caller resolved to reach this file.
|
|
125
|
+
const names: string[] = [options.target];
|
|
126
|
+
const declared = options.declared;
|
|
114
127
|
|
|
115
128
|
for (const name of names) {
|
|
116
|
-
const declared = config.targets[name];
|
|
117
|
-
if (!declared) {
|
|
118
|
-
warnings.push(`Target "${name}" is not declared by this profile, so nothing was planned.`);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
129
|
|
|
122
130
|
// A repository this profile keeps memory and skills in is not this
|
|
123
131
|
// command's to empty, and it is not reachable from here either: the routing
|
|
@@ -128,9 +136,9 @@ export async function removalPlan(
|
|
|
128
136
|
// `rm -r data/<profile>` used to be the whole of "what could this profile
|
|
129
137
|
// reach". So it is said before the operator confirms rather than discovered
|
|
130
138
|
// afterwards.
|
|
131
|
-
if (
|
|
139
|
+
if (config.knowledge) {
|
|
132
140
|
warnings.push(
|
|
133
|
-
`
|
|
141
|
+
`This profile keeps its memory and skills in ${config.knowledge.repo}. ` +
|
|
134
142
|
'Nothing here touches a repository, so they survive this removal — delete them there ' +
|
|
135
143
|
'if you want them gone.',
|
|
136
144
|
);
|
|
@@ -226,23 +234,26 @@ export async function removalPlan(
|
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
236
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
items.push({ target: null, kind: 'config', id: profilePath(root, profile) });
|
|
237
|
+
// Always, now. `--target` used to mean "decommission this one target and leave
|
|
238
|
+
// the profile behind", which was a coherent thing to want while a profile
|
|
239
|
+
// could be declared against several. It lives in exactly one (ADR-052), and
|
|
240
|
+
// the file itself is *in* that target's workspace — so emptying the target and
|
|
241
|
+
// keeping the profile would leave a config nothing can open, in a workspace it
|
|
242
|
+
// no longer belongs to.
|
|
243
|
+
const defaultProfile = await options.readDefaultProfile?.();
|
|
244
|
+
if (defaultProfile === profile) {
|
|
245
|
+
items.push({
|
|
246
|
+
target: null,
|
|
247
|
+
kind: 'workspace-key',
|
|
248
|
+
id: 'default_profile',
|
|
249
|
+
note: 'cleared, not repointed at whatever remains',
|
|
250
|
+
});
|
|
244
251
|
}
|
|
245
252
|
|
|
253
|
+
// Last. It is the only record of where everything else lives, so a failure
|
|
254
|
+
// before this point leaves data a later run can still find.
|
|
255
|
+
items.push({ target: null, kind: 'config', id: profilePath(root, profile) });
|
|
256
|
+
|
|
246
257
|
return { profile, items, untouched, warnings };
|
|
247
258
|
}
|
|
248
259
|
|
|
@@ -3,6 +3,7 @@ import type { BlobStore } from '#stores/blobs';
|
|
|
3
3
|
import {
|
|
4
4
|
ConfigError,
|
|
5
5
|
WORKSPACE_FILE,
|
|
6
|
+
openTarget,
|
|
6
7
|
readWorkspace,
|
|
7
8
|
readWorkspaceFile,
|
|
8
9
|
workspaceFiles,
|
|
@@ -250,15 +251,17 @@ export interface RemoveFlags extends GlobalFlags {
|
|
|
250
251
|
* in the tool.
|
|
251
252
|
*/
|
|
252
253
|
export async function removeProfile(name: string, flags: RemoveFlags): Promise<void> {
|
|
253
|
-
const { selection, config } = await resolveProfileOnly({ ...flags, profile: name });
|
|
254
|
+
const { selection, config, target } = await resolveProfileOnly({ ...flags, profile: name });
|
|
254
255
|
announceProfile(selection);
|
|
255
256
|
|
|
256
257
|
const root = selection.workspaceRoot;
|
|
257
258
|
const registry = await buildRegistryWithWorkspace(root, name);
|
|
258
259
|
const files = workspaceFiles(root);
|
|
260
|
+
const { declared } = await openTarget(root, target);
|
|
259
261
|
|
|
260
262
|
const plan = await removalPlan(config, root, name, registry, {
|
|
261
|
-
target
|
|
263
|
+
target,
|
|
264
|
+
declared,
|
|
262
265
|
openSecrets: (target) => openSecretStoreFor(config, root, target),
|
|
263
266
|
openBlobs: (target, area) => openBlobStoreFor(config, root, target, area),
|
|
264
267
|
readDefaultProfile: async () => (await readWorkspace(root))?.default_profile,
|