@lanes-sh/link 0.2.2 → 0.3.1
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 +22 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +63 -164
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/policy.ts +7 -7
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/status.ts +108 -1
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile/remove.ts +5 -5
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/secrets.ts +6 -6
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/sync.ts +262 -0
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +53 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/dispatch-owner.ts +93 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +90 -61
- package/src/cli/nearest.ts +45 -0
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +74 -51
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +365 -0
- package/src/cli/usage.ts +42 -11
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +94 -114
- package/src/deployments/discover.ts +103 -0
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +12 -6
- package/src/deployments/report.ts +117 -0
- package/src/deployments/servable.ts +82 -0
- package/src/deployments/serving.ts +165 -0
- package/src/deployments/sync-apply.ts +276 -0
- package/src/deployments/sync.ts +136 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +19 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/deployments.ts +80 -0
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +23 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +81 -3
- package/src/profile/targets.ts +122 -109
- package/src/profile/workspace.ts +139 -79
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +21 -3
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { BROKERED } from '#connectivity/auth/index.ts';
|
|
4
|
-
import { allowedConnections } from '#policy';
|
|
5
|
-
import {
|
|
6
|
-
credentialRefFor,
|
|
7
|
-
formatPlan,
|
|
8
|
-
planIsNoop,
|
|
9
|
-
planReconcile,
|
|
10
|
-
toPolicyDocument,
|
|
11
|
-
} from '#registry';
|
|
12
|
-
import { announce, emit, fail, ok, print, warn } from '../../output.ts';
|
|
1
|
+
import { credentialRefFor, formatPlan, planIsNoop, planReconcile } from '#registry';
|
|
2
|
+
import { announce, announceProfile, emit, fail, ok, print, warn } from '../../output.ts';
|
|
13
3
|
import { staleNudge } from '../../release.ts';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
4
|
+
import { openRuntime, resolveProfileOnly, type GlobalFlags } from '../../runtime.ts';
|
|
5
|
+
import type { FetchLike } from '#deployments/knowledge.ts';
|
|
6
|
+
import { credentialAge, reportCapabilityDrift } from './findings.ts';
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
9
|
* The gate order — check, doctor, plan, start — exists so failures surface in
|
|
@@ -22,12 +13,13 @@ import { openRuntime, resolveProfile, type GlobalFlags } from '../../runtime.ts'
|
|
|
22
13
|
|
|
23
14
|
/** Static validation only. No external calls, no database, no credentials. */
|
|
24
15
|
export async function check(flags: GlobalFlags): Promise<void> {
|
|
25
|
-
const {
|
|
26
|
-
|
|
16
|
+
const { selection, config } = await resolveProfileOnly(flags);
|
|
17
|
+
announceProfile(selection);
|
|
27
18
|
|
|
28
19
|
// Reaching here means the loader accepted it: contract major, no credential
|
|
29
|
-
// values, referential integrity
|
|
30
|
-
|
|
20
|
+
// values, and referential integrity all passed. Not target resolution — this
|
|
21
|
+
// validates a file, and the file is the same whichever target reads it.
|
|
22
|
+
print(ok(`${selection.profilePath} is valid`));
|
|
31
23
|
print(
|
|
32
24
|
` ${config.connections.length} connection(s), ` +
|
|
33
25
|
`${config.policy.allow.length} allow rule(s), ${config.policy.deny.length} deny rule(s)`,
|
|
@@ -48,16 +40,18 @@ export async function plan(flags: GlobalFlags): Promise<void> {
|
|
|
48
40
|
|
|
49
41
|
export interface DoctorFlags extends GlobalFlags {
|
|
50
42
|
readonly json?: boolean | undefined;
|
|
43
|
+
/** Injected for tests. A knowledge repository is the only thing doctor fetches. */
|
|
44
|
+
readonly fetch?: FetchLike | undefined;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
48
|
* One thing doctor found: what is wrong, and the command that fixes it.
|
|
55
49
|
*
|
|
56
|
-
* `fix` carries
|
|
57
|
-
* the terminal just resolved a profile
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
50
|
+
* `fix` carries both flags. That used to be an argument about JSON callers —
|
|
51
|
+
* a person reading the terminal had just resolved a profile and their next
|
|
52
|
+
* command would resolve the same one, while a parser had no shell context. The
|
|
53
|
+
* argument is moot now: nothing resolves on its own (ADR-037), so a command
|
|
54
|
+
* without them refuses whoever runs it. What survives is the shape.
|
|
61
55
|
*/
|
|
62
56
|
export interface DoctorFinding {
|
|
63
57
|
readonly kind: string;
|
|
@@ -68,15 +62,15 @@ export interface DoctorFinding {
|
|
|
68
62
|
|
|
69
63
|
/** Read-only external checks: credentials resolve, stores reachable. */
|
|
70
64
|
export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
71
|
-
const runtime = await openRuntime(flags);
|
|
65
|
+
const runtime = await openRuntime(flags, { fetch: flags.fetch });
|
|
72
66
|
|
|
73
67
|
const checks: string[] = [];
|
|
74
68
|
const warnings: DoctorFinding[] = [];
|
|
75
69
|
const problems: DoctorFinding[] = [];
|
|
76
70
|
|
|
77
71
|
try {
|
|
78
|
-
const
|
|
79
|
-
`${command} --profile ${runtime.resolution.profile}`;
|
|
72
|
+
const forSelection = (command: string) =>
|
|
73
|
+
`${command} --profile ${runtime.resolution.profile} --target ${runtime.resolution.target}`;
|
|
80
74
|
|
|
81
75
|
checks.push('config is valid');
|
|
82
76
|
checks.push('state store is reachable');
|
|
@@ -90,7 +84,7 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
90
84
|
warnings.push({
|
|
91
85
|
kind: 'no_profile_token',
|
|
92
86
|
message: 'no profile token yet — lanes link start will mint one, or run: lanes link token rotate',
|
|
93
|
-
fix:
|
|
87
|
+
fix: forSelection('lanes link token rotate'),
|
|
94
88
|
});
|
|
95
89
|
}
|
|
96
90
|
|
|
@@ -120,7 +114,7 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
120
114
|
message:
|
|
121
115
|
`${key} credential is ${staleness.days} days old — a Google app in "Testing" expires at 7. ` +
|
|
122
116
|
`Run: lanes link connect ${key}`,
|
|
123
|
-
fix:
|
|
117
|
+
fix: forSelection(`lanes link connect ${key}`),
|
|
124
118
|
});
|
|
125
119
|
} else {
|
|
126
120
|
const age = staleness
|
|
@@ -133,7 +127,45 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
133
127
|
kind: 'missing_credential',
|
|
134
128
|
key,
|
|
135
129
|
message: `${key} has no stored credential — run: lanes link connect ${key}`,
|
|
136
|
-
fix:
|
|
130
|
+
fix: forSelection(`lanes link connect ${key}`),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A repository this profile stores memory and skills in is an external
|
|
136
|
+
// dependency the other checks here do not cover, and it fails in the way
|
|
137
|
+
// doctor exists for: a token expires, and memory stops working with nothing
|
|
138
|
+
// said until the next read. One request answers all of it.
|
|
139
|
+
if (runtime.knowledge) {
|
|
140
|
+
try {
|
|
141
|
+
const facts = await runtime.knowledge.repository.facts();
|
|
142
|
+
|
|
143
|
+
if (!facts.canPush) {
|
|
144
|
+
problems.push({
|
|
145
|
+
kind: 'knowledge_read_only',
|
|
146
|
+
key: facts.fullName,
|
|
147
|
+
message:
|
|
148
|
+
`memory and skills are kept in ${facts.fullName}, and the token can no longer write ` +
|
|
149
|
+
'to it — writes will fail. Regenerate it with Contents: read and write, then run: ' +
|
|
150
|
+
'lanes link knowledge use github --repo ' + facts.fullName + ' --replace',
|
|
151
|
+
fix: forSelection(`lanes link knowledge use github --repo ${facts.fullName} --replace`),
|
|
152
|
+
});
|
|
153
|
+
} else if (!facts.private) {
|
|
154
|
+
warnings.push({
|
|
155
|
+
kind: 'knowledge_public',
|
|
156
|
+
key: facts.fullName,
|
|
157
|
+
message: `${facts.fullName} is public — every memory entry and skill in it is world-readable`,
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
checks.push(`memory and skills reachable in ${facts.fullName}`);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
problems.push({
|
|
164
|
+
kind: 'knowledge_unreachable',
|
|
165
|
+
key: runtime.knowledge.describe,
|
|
166
|
+
message:
|
|
167
|
+
`memory and skills are kept in ${runtime.knowledge.describe}, which could not be ` +
|
|
168
|
+
`reached: ${(error as Error).message}`,
|
|
137
169
|
});
|
|
138
170
|
}
|
|
139
171
|
}
|
|
@@ -174,7 +206,7 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
174
206
|
message:
|
|
175
207
|
`this profile ${hasSetupRow ? 'does not grant "setup.*"' : 'has no "setup" connection'}` +
|
|
176
208
|
', so an agent cannot see what is configured — run: lanes link connect setup',
|
|
177
|
-
fix:
|
|
209
|
+
fix: forSelection('lanes link connect setup'),
|
|
178
210
|
});
|
|
179
211
|
}
|
|
180
212
|
|
|
@@ -205,7 +237,7 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
205
237
|
warnings.push({
|
|
206
238
|
kind: 'reconcile_drift',
|
|
207
239
|
message: 'runtime state differs from config — run: lanes link plan',
|
|
208
|
-
fix:
|
|
240
|
+
fix: forSelection('lanes link plan'),
|
|
209
241
|
});
|
|
210
242
|
}
|
|
211
243
|
|
|
@@ -227,136 +259,3 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
227
259
|
await runtime.close();
|
|
228
260
|
}
|
|
229
261
|
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* How old a stored OAuth credential is.
|
|
233
|
-
*
|
|
234
|
-
* Derived from the `expires_at` the OAuth provider stamps when saving tokens.
|
|
235
|
-
* Returns null for anything that is not a token blob — an app password has no
|
|
236
|
-
* meaningful age, and guessing one would produce a confusing warning.
|
|
237
|
-
*/
|
|
238
|
-
async function credentialAge(
|
|
239
|
-
credentials: { get(ref: string): Promise<string | null> },
|
|
240
|
-
ref: string,
|
|
241
|
-
): Promise<{ days: number; brokered: boolean } | null> {
|
|
242
|
-
const raw = await credentials.get(ref);
|
|
243
|
-
if (!raw) return null;
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const parsed = JSON.parse(raw) as {
|
|
247
|
-
expires_at?: number;
|
|
248
|
-
expires_in?: number;
|
|
249
|
-
authorized_via?: string;
|
|
250
|
-
};
|
|
251
|
-
if (typeof parsed.expires_at !== 'number') return null;
|
|
252
|
-
|
|
253
|
-
const issued = parsed.expires_at - (parsed.expires_in ?? 3600) * 1000;
|
|
254
|
-
return {
|
|
255
|
-
days: Math.floor((Date.now() - issued) / 86_400_000),
|
|
256
|
-
brokered: parsed.authorized_via === BROKERED,
|
|
257
|
-
};
|
|
258
|
-
} catch {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Capabilities the upstream has grown since you connected.
|
|
265
|
-
*
|
|
266
|
-
* This is what replaced pinning an allow line per tool. `connect` writes
|
|
267
|
-
* `provider.*`, which is readable and which a vendor can quietly widen by
|
|
268
|
-
* shipping a new tool — so the widening has to be *visible* somewhere, and
|
|
269
|
-
* doctor is where you look when you want to know what changed.
|
|
270
|
-
*
|
|
271
|
-
* Compared against the discovery cache rather than against config, because the
|
|
272
|
-
* cache is what the running server actually serves. Failures are reported and
|
|
273
|
-
* skipped: an upstream being down is not a reason for doctor to fail, and it is
|
|
274
|
-
* already obvious from every other check.
|
|
275
|
-
*
|
|
276
|
-
* This used to open `if (kind !== 'mcp') continue`, which meant it watched the
|
|
277
|
-
* one connector kind whose capabilities this repository does not ship and
|
|
278
|
-
* ignored the two it does. Drive gained three operations in a commit and the
|
|
279
|
-
* endpoint served six for as long as the operator did not re-authorise; the
|
|
280
|
-
* check written to make exactly that visible never ran. `http` is no longer
|
|
281
|
-
* listed here because it no longer can drift — `runtime/discovery.ts` re-derives
|
|
282
|
-
* it at startup — so what is left is the kinds that genuinely still cache.
|
|
283
|
-
*/
|
|
284
|
-
async function reportCapabilityDrift(
|
|
285
|
-
runtime: Awaited<ReturnType<typeof openRuntime>>,
|
|
286
|
-
say: (message: string) => void,
|
|
287
|
-
): Promise<void> {
|
|
288
|
-
const policy = toPolicyDocument(runtime.config);
|
|
289
|
-
const principal = ownerPrincipal(runtime.config.instance.profile).id;
|
|
290
|
-
|
|
291
|
-
for (const entry of runtime.registry.list()) {
|
|
292
|
-
const connection = runtime.config.connections.find((c) => c.provider === entry.manifest.id);
|
|
293
|
-
if (!connection) continue;
|
|
294
|
-
|
|
295
|
-
const connector = runtime.connectorFor(entry.manifest.id, connection.id);
|
|
296
|
-
const probe = discoveryProbe(entry.manifest, connector);
|
|
297
|
-
// Re-derived every startup, so what is served is what is shipped. Probing
|
|
298
|
-
// it here would only ever compare a value against itself.
|
|
299
|
-
if (!probe || probe.cost === 'offline') continue;
|
|
300
|
-
|
|
301
|
-
const cached = runtime.registry.discovered(entry.manifest.id) ?? [];
|
|
302
|
-
|
|
303
|
-
let live: readonly DiscoveredCapability[];
|
|
304
|
-
try {
|
|
305
|
-
live = await probe.run();
|
|
306
|
-
} catch {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// A provider that was never discovered serves nothing at all, which reads
|
|
311
|
-
// from the outside like the provider being broken. Skipping it silently is
|
|
312
|
-
// how that stays a mystery.
|
|
313
|
-
if (cached.length === 0) {
|
|
314
|
-
say(
|
|
315
|
-
`${entry.manifest.name} has never been discovered, so none of its ${live.length} ` +
|
|
316
|
-
`capability(ies) are served — run: lanes link connect ${entry.manifest.id}`,
|
|
317
|
-
);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const diff = capabilityDiff(cached, live);
|
|
322
|
-
|
|
323
|
-
if (diff.added.length > 0) {
|
|
324
|
-
// Through the same `allowedConnections` the dispatcher enforces with,
|
|
325
|
-
// rather than reading `policy.allow` directly. The predicate this replaced
|
|
326
|
-
// ignored its own `name` argument, so the answer was all-or-nothing, and
|
|
327
|
-
// it never consulted `deny` — telling an operator their policy covered a
|
|
328
|
-
// capability they had explicitly withheld.
|
|
329
|
-
const reachable = diff.added.filter(
|
|
330
|
-
(name) =>
|
|
331
|
-
allowedConnections(
|
|
332
|
-
`${entry.manifest.id}.${name}`,
|
|
333
|
-
[`${entry.manifest.id}.${connection.id}`],
|
|
334
|
-
principal,
|
|
335
|
-
policy,
|
|
336
|
-
).length > 0,
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
say(
|
|
340
|
-
`${entry.manifest.name} has ${diff.added.length} new capability(ies) since you connected: ` +
|
|
341
|
-
`${diff.added.slice(0, 5).join(', ')}${diff.added.length > 5 ? ', …' : ''}` +
|
|
342
|
-
(reachable.length > 0
|
|
343
|
-
? `\n Your policy covers ${reachable.length} of them — lanes link policy deny ${entry.manifest.id}.<name> to withhold one.`
|
|
344
|
-
: ''),
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (diff.removed.length > 0) {
|
|
349
|
-
say(`${entry.manifest.name} no longer offers: ${diff.removed.slice(0, 5).join(', ')}`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// The case ADR-017 said `plan` structurally could not report: a schema or a
|
|
353
|
-
// description moved, the name did not, and the endpoint is serving the old
|
|
354
|
-
// shape until it restarts.
|
|
355
|
-
if (diff.changed.length > 0) {
|
|
356
|
-
say(
|
|
357
|
-
`${entry.manifest.name} changed the shape of: ${diff.changed.slice(0, 5).join(', ')}` +
|
|
358
|
-
`\n Run: lanes link connect ${entry.manifest.id}, then restart the endpoint.`,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listProfiles } from '#profile';
|
|
2
|
+
import { unservableProfiles } from '#deployments/servable.ts';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { deployedUrl, endpointHealth, localUrl } from '../../endpoint-url.ts';
|
|
4
5
|
import { announce, heading, print, style, warn } from '../../output.ts';
|
|
@@ -36,11 +37,14 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
36
37
|
const live = await endpointHealth(url, token);
|
|
37
38
|
const mine = live?.profile === runtime.resolution.profile;
|
|
38
39
|
|
|
39
|
-
// Live if it is up, otherwise what `start` would serve
|
|
40
|
-
//
|
|
40
|
+
// Live if it is up, otherwise what `start` would serve — which is every
|
|
41
|
+
// profile in the workspace *that declares this target*, not every profile
|
|
42
|
+
// full stop. `start` opens them all against one target and skips the ones
|
|
43
|
+
// that cannot run on it, so listing those here would promise a reach the
|
|
44
|
+
// endpoint will not have.
|
|
41
45
|
const profiles = mine
|
|
42
46
|
? live.profiles
|
|
43
|
-
: await
|
|
47
|
+
: await servable(runtime.resolution.workspaceRoot, runtime.target);
|
|
44
48
|
|
|
45
49
|
if (flags.json) {
|
|
46
50
|
print(
|
|
@@ -99,7 +103,11 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
99
103
|
);
|
|
100
104
|
print('');
|
|
101
105
|
|
|
102
|
-
const invocation = await tokenInvocation(
|
|
106
|
+
const invocation = await tokenInvocation(
|
|
107
|
+
token,
|
|
108
|
+
runtime.resolution.profile,
|
|
109
|
+
runtime.resolution.target,
|
|
110
|
+
);
|
|
103
111
|
|
|
104
112
|
print(
|
|
105
113
|
` claude mcp add --transport http lanes-link ${url} \\\n` +
|
|
@@ -130,6 +138,18 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
|
|
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
|
+
|
|
133
153
|
/**
|
|
134
154
|
* The command that prints this endpoint's token, verified to actually work.
|
|
135
155
|
*
|
|
@@ -140,18 +160,25 @@ export async function outputs(flags: OutputsFlags): Promise<void> {
|
|
|
140
160
|
* checking it returns *this* token; otherwise the printed command names this
|
|
141
161
|
* checkout, which always works.
|
|
142
162
|
*/
|
|
143
|
-
async function tokenInvocation(
|
|
144
|
-
tokenRef: string,
|
|
163
|
+
export async function tokenInvocation(
|
|
145
164
|
expected: string,
|
|
146
|
-
profile: string
|
|
165
|
+
profile: string,
|
|
166
|
+
target: string,
|
|
147
167
|
): Promise<{ command: string; onPath: boolean }> {
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
// Both, always, and from the *resolved* selection rather than the flags. A
|
|
169
|
+
// token is per-target, so `outputs --target cloud` printing a bare
|
|
170
|
+
// `token show --raw` hands over the local one beside a deployed URL — a
|
|
171
|
+
// credential that looks like an answer and fails as a wrong password. Naming
|
|
172
|
+
// the profile as well makes the line pasteable into any shell rather than
|
|
173
|
+
// only into one where the same default happens to resolve.
|
|
174
|
+
const selection = ` --profile ${profile} --target ${target}`;
|
|
175
|
+
const short = `lanes link token show --raw${selection}`;
|
|
176
|
+
const argv = ['link', 'token', 'show', '--raw', '--profile', profile, '--target', target];
|
|
150
177
|
|
|
151
178
|
const resolved = Bun.which('lanes');
|
|
152
179
|
if (resolved) {
|
|
153
180
|
try {
|
|
154
|
-
const result = Bun.spawnSync([resolved,
|
|
181
|
+
const result = Bun.spawnSync([resolved, ...argv]);
|
|
155
182
|
// Compared against the token rather than merely checking it exited zero:
|
|
156
183
|
// a `lanes` on PATH could belong to a different workspace entirely,
|
|
157
184
|
// and would hand the harness a token this endpoint rejects.
|
|
@@ -168,6 +195,6 @@ async function tokenInvocation(
|
|
|
168
195
|
// which is the empty-token failure this whole function exists to avoid,
|
|
169
196
|
// reintroduced by the fallback meant to prevent it.
|
|
170
197
|
const entry = fileURLToPath(new URL('../../lanes.ts', import.meta.url));
|
|
171
|
-
return { command: `bun run ${entry} link token show --raw${
|
|
198
|
+
return { command: `bun run ${entry} link token show --raw${selection}`, onPath: false };
|
|
172
199
|
}
|
|
173
200
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadConfigFile } from '#profile';
|
|
2
2
|
import { ConfigDocument } from '../../config-edit.ts';
|
|
3
|
-
import { announce, heading, ok, print, style, table } from '../../output.ts';
|
|
4
|
-
import { resolveProfile, type GlobalFlags } from '../../runtime.ts';
|
|
3
|
+
import { announce, announceProfile, heading, ok, print, style, table } from '../../output.ts';
|
|
4
|
+
import { resolveProfile, resolveProfileOnly, type GlobalFlags } from '../../runtime.ts';
|
|
5
5
|
import { nextAfterEdit, publishProfileEdit } from '../../publish.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -12,8 +12,8 @@ import { nextAfterEdit, publishProfileEdit } from '../../publish.ts';
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export async function policyList(flags: GlobalFlags): Promise<void> {
|
|
15
|
-
const {
|
|
16
|
-
|
|
15
|
+
const { selection, config } = await resolveProfileOnly(flags);
|
|
16
|
+
announceProfile(selection);
|
|
17
17
|
|
|
18
18
|
if (config.policy.allow.length === 0 && config.policy.deny.length === 0) {
|
|
19
19
|
print(style.dim('No rules. Default deny is in effect: nothing is reachable.'));
|
|
@@ -73,8 +73,8 @@ export async function policyRule(
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export async function configShow(flags: GlobalFlags): Promise<void> {
|
|
76
|
-
const {
|
|
77
|
-
|
|
78
|
-
const { config } = await loadConfigFile(
|
|
76
|
+
const { selection } = await resolveProfileOnly(flags);
|
|
77
|
+
announceProfile(selection);
|
|
78
|
+
const { config } = await loadConfigFile(selection.profilePath);
|
|
79
79
|
print(JSON.stringify(config, null, 2));
|
|
80
80
|
}
|
|
@@ -24,6 +24,9 @@ export async function start(
|
|
|
24
24
|
port: flags.port,
|
|
25
25
|
only: flags.only,
|
|
26
26
|
mintToken: true,
|
|
27
|
+
// Local, so there is a browser and a person at it. `container.ts` does not
|
|
28
|
+
// pass this — see `#server/dashboard.ts`.
|
|
29
|
+
dashboard: true,
|
|
27
30
|
// Stderr, not stdout: `--json` and `--raw` callers parse the other stream.
|
|
28
31
|
// A refused credential is the event worth seeing while this runs in the
|
|
29
32
|
// foreground, and until now nothing printed it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listProfiles } from '#profile';
|
|
1
|
+
import { listProfiles, loadWorkspaceProfiles, resolveWorkspaceRoot } from '#profile';
|
|
2
2
|
import { oneProfile, visibleCapabilities } from '#server/mcp';
|
|
3
3
|
import { toPolicyDocument } from '#registry';
|
|
4
4
|
import { announce, emit, heading, print, style, table } from '../../output.ts';
|
|
@@ -32,6 +32,8 @@ export interface StatusFlags extends GlobalFlags {
|
|
|
32
32
|
* config-only command can say that much honestly.
|
|
33
33
|
*/
|
|
34
34
|
export async function status(flags: StatusFlags): Promise<void> {
|
|
35
|
+
if (flags.profile === undefined) return workspaceStatus(flags);
|
|
36
|
+
|
|
35
37
|
const runtime = await openRuntime(flags);
|
|
36
38
|
try {
|
|
37
39
|
const records = await runtime.state.connections.list();
|
|
@@ -131,3 +133,108 @@ export async function status(flags: StatusFlags): Promise<void> {
|
|
|
131
133
|
await runtime.close();
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `lanes link status --target <t>` — the whole workspace at one target.
|
|
139
|
+
*
|
|
140
|
+
* The default, because the subject of this command is the endpoint and one
|
|
141
|
+
* endpoint serves every profile in the workspace (ADR-009, ADR-043). Naming a
|
|
142
|
+
* profile narrows it to the detailed view above; naming none is not a missing
|
|
143
|
+
* answer, it is the wider one.
|
|
144
|
+
*
|
|
145
|
+
* **Opens no store and makes no network call.** The view above already declines
|
|
146
|
+
* to probe, for the reasons in this file's header; this one additionally
|
|
147
|
+
* declines to open a target's adapters, so it stays instant and — more to the
|
|
148
|
+
* point — still answers for a target whose stores are unreachable, misdeclared,
|
|
149
|
+
* or gone. A summary that needs the deployment to be healthy cannot report that
|
|
150
|
+
* it is not.
|
|
151
|
+
*
|
|
152
|
+
* That is what makes the row it exists for legible: a target declared by one
|
|
153
|
+
* profile and not its sibling. From inside either profile that state is
|
|
154
|
+
* invisible, and it is what a vanished deployment looks like from the outside.
|
|
155
|
+
*/
|
|
156
|
+
async function workspaceStatus(flags: StatusFlags): Promise<void> {
|
|
157
|
+
const target = flags.target!;
|
|
158
|
+
const root = resolveWorkspaceRoot();
|
|
159
|
+
const workspace = await loadWorkspaceProfiles(root);
|
|
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
|
+
}));
|
|
167
|
+
|
|
168
|
+
const declaring = profiles.filter((entry) => entry.declares);
|
|
169
|
+
|
|
170
|
+
// Distinct deployments, not the first one found. Two profiles declaring the
|
|
171
|
+
// same target name against different services is drift worth printing rather
|
|
172
|
+
// than a tie to break silently — and picking one would make the wrong half of
|
|
173
|
+
// the workspace look correctly configured.
|
|
174
|
+
const deployments = [
|
|
175
|
+
...new Map(
|
|
176
|
+
declaring
|
|
177
|
+
.filter((entry) => entry.deployment !== null)
|
|
178
|
+
.map((entry) => [JSON.stringify(entry.deployment), entry.deployment!] as const),
|
|
179
|
+
).values(),
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
return emit(
|
|
183
|
+
flags.json,
|
|
184
|
+
{ workspace: root, target, profiles, deployments, unreadable: workspace.unreadable },
|
|
185
|
+
() => {
|
|
186
|
+
print(style.dim(`workspace ${style.bold(root)} target ${style.bold(target)}`));
|
|
187
|
+
|
|
188
|
+
heading('Profiles');
|
|
189
|
+
table(
|
|
190
|
+
profiles.map((entry) => [
|
|
191
|
+
` ${style.bold(entry.name)}`,
|
|
192
|
+
entry.declares ? style.green(`${target} declared`) : style.yellow('not declared'),
|
|
193
|
+
style.dim(entry.declares ? `${entry.connections} connection(s)` : '—'),
|
|
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
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (deployments.length === 0) {
|
|
213
|
+
print(style.dim(' No deployment — this target runs wherever the CLI does.'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const deployment of deployments) {
|
|
218
|
+
const { platform, service, region } = deployment;
|
|
219
|
+
print(` ${platform} service ${style.bold(service)} in ${region}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (deployments.length > 1) {
|
|
223
|
+
print(
|
|
224
|
+
style.yellow(
|
|
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.'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
print(
|
|
233
|
+
style.dim(
|
|
234
|
+
" the address is the platform's to assign — run: " +
|
|
235
|
+
`lanes link outputs --target ${target} --profile ${declaring[0]!.name}`,
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -63,7 +63,7 @@ export async function tokenRotate(
|
|
|
63
63
|
// than the command, so nothing re-reads this on its own.
|
|
64
64
|
print(warn('every agent configured with the old token must be re-registered'));
|
|
65
65
|
print(style.dim(' A harness stores the token it was given, not the command that produced it.'));
|
|
66
|
-
print(style.dim(' Run: lanes link outputs
|
|
66
|
+
print(style.dim(' Run: lanes link outputs with this profile and target for the command to re-run.'));
|
|
67
67
|
} finally {
|
|
68
68
|
await runtime.close();
|
|
69
69
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* inspect.ts check, plan, doctor — the gate order, cheapest failure first
|
|
9
9
|
* status.ts connections, reachable capabilities, endpoint
|
|
10
10
|
* outputs.ts what an agent harness needs, and proving the short form works
|
|
11
|
+
* dashboard.ts opening the page a local endpoint serves, with the key it needs
|
|
11
12
|
* serve.ts start
|
|
12
13
|
* audit.ts audit tail and verify, and the Markdown rendering of tail
|
|
13
14
|
* token.ts token show, token rotate
|
|
@@ -22,6 +23,7 @@ export { status } from './operate/status.ts';
|
|
|
22
23
|
export { outputs, type OutputsFlags } from './operate/outputs.ts';
|
|
23
24
|
export { tools, type ToolsFlags } from './operate/tools.ts';
|
|
24
25
|
export { start } from './operate/serve.ts';
|
|
26
|
+
export { dashboard, type DashboardFlags } from './operate/dashboard.ts';
|
|
25
27
|
export { auditTail, auditVerify, markdownCell } from './operate/audit.ts';
|
|
26
28
|
export { attachFile } from './operate/attach.ts';
|
|
27
29
|
export { tokenRotate, tokenShow } from './operate/token.ts';
|