@lanes-sh/link 0.5.4 → 0.6.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/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/migrate-plan.ts +160 -0
- 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 +263 -0
- package/src/deployments/bootstrap.ts +31 -11
- package/src/deployments/deploy.ts +103 -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
package/src/profile/schema.ts
CHANGED
|
@@ -27,8 +27,19 @@ import { knowledgeTargetSchema } from './knowledge.ts';
|
|
|
27
27
|
* outright — never a best-effort load. Guessing at an unknown schema on a file
|
|
28
28
|
* that governs authorization is how you end up granting something the operator
|
|
29
29
|
* did not write.
|
|
30
|
+
*
|
|
31
|
+
* **2 moved `targets:` out of the profile and into the workspace** (ADR-052). A
|
|
32
|
+
* profile used to declare the adapter sets it could be opened against, which
|
|
33
|
+
* meant a deploy left two copies of every profile — one in the workspace and one
|
|
34
|
+
* in the bucket the endpoint reads — with nothing keeping them honest. Now a
|
|
35
|
+
* workspace *is* a target: it declares its own adapters once, holds the profiles
|
|
36
|
+
* that live in it, and a profile is one copy in one place.
|
|
37
|
+
*
|
|
38
|
+
* A hard cut, and contract 1 is not read here. `./legacy.ts` understands it, and
|
|
39
|
+
* only the migration uses that — a runtime that loaded either shape would be the
|
|
40
|
+
* two-sources-of-truth problem again, one level up.
|
|
30
41
|
*/
|
|
31
|
-
export const SUPPORTED_CONTRACT =
|
|
42
|
+
export const SUPPORTED_CONTRACT = 2;
|
|
32
43
|
|
|
33
44
|
/**
|
|
34
45
|
* There is no `database:` block any more.
|
|
@@ -149,8 +160,13 @@ export const vaultTargetSchema = z.object({
|
|
|
149
160
|
*/
|
|
150
161
|
export const deployTargetSchema = z.object({
|
|
151
162
|
platform: z.enum(['cloudrun']),
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
// Non-empty here rather than in a referential check further down. Under
|
|
164
|
+
// contract 1 `assertReferentialIntegrity` walked the profile's `targets:`
|
|
165
|
+
// block for these; the block is the workspace's now (ADR-052), and a
|
|
166
|
+
// constraint the schema can express belongs in the schema — an empty service
|
|
167
|
+
// name should fail at load, not minutes into a build.
|
|
168
|
+
region: z.string().min(1),
|
|
169
|
+
service: z.string().min(1),
|
|
154
170
|
/**
|
|
155
171
|
* `iam` puts the platform's own identity check in front of the service;
|
|
156
172
|
* `public` leaves the door open and relies on this application's gate.
|
|
@@ -197,69 +213,21 @@ export const deployTargetSchema = z.object({
|
|
|
197
213
|
});
|
|
198
214
|
|
|
199
215
|
/**
|
|
200
|
-
*
|
|
216
|
+
* One adapter set — where credentials are kept, where bytes go, where it rolls.
|
|
201
217
|
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
218
|
+
* Declared by the **workspace** that is this target, not by a profile in it
|
|
219
|
+
* (ADR-052). The `cloudrun:` spelling `deploy:` replaced is gone with contract
|
|
220
|
+
* 1; `./legacy.ts` still normalises it, on the one path that reads a contract-1
|
|
221
|
+
* file at all.
|
|
206
222
|
*/
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
export const targetSchema = z.object({
|
|
224
|
+
credentials: credentialsTargetSchema,
|
|
225
|
+
audit: auditTargetSchema.optional(),
|
|
226
|
+
storage: storageTargetSchema,
|
|
227
|
+
vault: vaultTargetSchema.optional(),
|
|
228
|
+
deploy: deployTargetSchema.optional(),
|
|
211
229
|
});
|
|
212
230
|
|
|
213
|
-
export const targetSchema = z
|
|
214
|
-
.object({
|
|
215
|
-
credentials: credentialsTargetSchema,
|
|
216
|
-
audit: auditTargetSchema.optional(),
|
|
217
|
-
storage: storageTargetSchema,
|
|
218
|
-
vault: vaultTargetSchema.optional(),
|
|
219
|
-
/**
|
|
220
|
-
* Memory and skills, somewhere other than `storage` above.
|
|
221
|
-
*
|
|
222
|
-
* Optional and absent by default, so every profile written before it keeps
|
|
223
|
-
* storing both exactly where it did. See `knowledge.ts` for why these two
|
|
224
|
-
* are separable from the rest and why the credential store and the vault
|
|
225
|
-
* are not.
|
|
226
|
-
*/
|
|
227
|
-
knowledge: knowledgeTargetSchema.optional(),
|
|
228
|
-
deploy: deployTargetSchema.optional(),
|
|
229
|
-
/** @deprecated Write `deploy` with `platform: cloudrun`. */
|
|
230
|
-
cloudrun: legacyCloudRunSchema.optional(),
|
|
231
|
-
})
|
|
232
|
-
.superRefine((target, ctx) => {
|
|
233
|
-
// Both present is refused rather than resolved by precedence: a second place
|
|
234
|
-
// to say where this deploys could only ever disagree with the first, and
|
|
235
|
-
// silently preferring one would roll a revision to the project the operator
|
|
236
|
-
// was not reading.
|
|
237
|
-
if (target.deploy && target.cloudrun) {
|
|
238
|
-
ctx.addIssue({
|
|
239
|
-
code: 'custom',
|
|
240
|
-
path: ['cloudrun'],
|
|
241
|
-
message:
|
|
242
|
-
'both "deploy" and "cloudrun" are declared — remove "cloudrun", which "deploy" replaces',
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
.transform(({ cloudrun, ...target }) =>
|
|
247
|
-
target.deploy || !cloudrun
|
|
248
|
-
? target
|
|
249
|
-
: {
|
|
250
|
-
...target,
|
|
251
|
-
// The pre-`deploy` spelling predates both of these, so it gets the
|
|
252
|
-
// same defaults the current one would: the closed door, and no
|
|
253
|
-
// instance kept warm.
|
|
254
|
-
deploy: {
|
|
255
|
-
...cloudrun,
|
|
256
|
-
platform: 'cloudrun' as const,
|
|
257
|
-
access: 'iam' as const,
|
|
258
|
-
min_instances: 0,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
);
|
|
262
|
-
|
|
263
231
|
/**
|
|
264
232
|
* A rule is a pattern, and usually just a string.
|
|
265
233
|
*
|
|
@@ -328,27 +296,20 @@ export const oauthAppSchema = z.object({
|
|
|
328
296
|
});
|
|
329
297
|
|
|
330
298
|
|
|
299
|
+
/**
|
|
300
|
+
* One profile: who it is, what it reaches, and what it may do.
|
|
301
|
+
*
|
|
302
|
+
* **It does not say where it runs.** A profile lives in exactly one workspace
|
|
303
|
+
* and that workspace is a target (ADR-052), so the adapter set is a property of
|
|
304
|
+
* where the file is, not of what is in it. `instance.default_target` went with
|
|
305
|
+
* `targets:` — it was already inert under ADR-037, and contract 2 is the release
|
|
306
|
+
* that stops carrying it.
|
|
307
|
+
*/
|
|
331
308
|
export const configSchema = z.object({
|
|
332
309
|
contract: z.number().int().positive(),
|
|
333
310
|
|
|
334
311
|
instance: z.object({
|
|
335
312
|
profile: identifier,
|
|
336
|
-
/**
|
|
337
|
-
* @deprecated Parsed, never read. See ADR-037.
|
|
338
|
-
*
|
|
339
|
-
* Every command names its target on the command line now, so nothing
|
|
340
|
-
* consults this. It stays *declared* rather than being dropped, and that is
|
|
341
|
-
* the whole point: an undeclared key is stripped silently by the schema,
|
|
342
|
-
* which would leave `check` and `doctor` with nothing to report and an
|
|
343
|
-
* operator staring at a line they reasonably believe still selects
|
|
344
|
-
* something. Declaring it is what lets them be told it is inert.
|
|
345
|
-
*
|
|
346
|
-
* Optional, so a profile written today needs no such line, and unvalidated,
|
|
347
|
-
* so a stale value naming a target that no longer exists is harmless rather
|
|
348
|
-
* than a failure on a key nothing reads. The `database:` note above records
|
|
349
|
-
* the same decision for the same reason.
|
|
350
|
-
*/
|
|
351
|
-
default_target: z.string().min(1).optional(),
|
|
352
313
|
port: z.number().int().min(1).max(65535).default(7337),
|
|
353
314
|
/**
|
|
354
315
|
* Loopback by default. Binding elsewhere is possible but the server
|
|
@@ -357,8 +318,6 @@ export const configSchema = z.object({
|
|
|
357
318
|
host: z.string().default('127.0.0.1'),
|
|
358
319
|
}),
|
|
359
320
|
|
|
360
|
-
targets: z.record(z.string(), targetSchema),
|
|
361
|
-
|
|
362
321
|
auth: z
|
|
363
322
|
.object({
|
|
364
323
|
mode: z.literal('bearer').default('bearer'),
|
|
@@ -398,6 +357,22 @@ export const configSchema = z.object({
|
|
|
398
357
|
connections: z.array(connectionSchema).default([]),
|
|
399
358
|
policy: policySchema.default({ allow: [], deny: [] }),
|
|
400
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Memory and skills, somewhere other than the target's own storage (ADR-041).
|
|
362
|
+
*
|
|
363
|
+
* On the profile rather than the target, which is where contract 1 kept it.
|
|
364
|
+
* That was never really a property of the adapter set — it says where *this
|
|
365
|
+
* profile's* notes live — and it had to be per-target only because a profile
|
|
366
|
+
* could be declared against several. It lives in exactly one now (ADR-052), so
|
|
367
|
+
* per-profile and per-profile-per-target are the same thing, and this is the
|
|
368
|
+
* one of the two that matches what ADR-030 says a profile owns.
|
|
369
|
+
*
|
|
370
|
+
* Optional and absent by default, so a profile that says nothing keeps both
|
|
371
|
+
* where it keeps everything else. See `knowledge.ts` for why these two are
|
|
372
|
+
* separable from the rest and why the credential store and the vault are not.
|
|
373
|
+
*/
|
|
374
|
+
knowledge: knowledgeTargetSchema.optional(),
|
|
375
|
+
|
|
401
376
|
/**
|
|
402
377
|
* Who the owner is, for anything written as them. Optional and additive, so
|
|
403
378
|
* every profile written before it keeps loading unchanged — the same reasoning
|
|
@@ -416,44 +391,121 @@ export { authorizationSchema, identitySchema };
|
|
|
416
391
|
export type { IdentityEntry } from './identity.ts';
|
|
417
392
|
|
|
418
393
|
/**
|
|
419
|
-
*
|
|
394
|
+
* One entry in the workspace's target registry.
|
|
420
395
|
*
|
|
421
|
-
*
|
|
422
|
-
* and a command still acts on what the profile says. This exists because the
|
|
423
|
-
* profile file was the *only* record that a deployment existed, so rewriting
|
|
424
|
-
* one erased the sole pointer to a live service, a bucket holding every byte
|
|
425
|
-
* the endpoint remembered, and a credential store — none of which had gone
|
|
426
|
-
* anywhere. There was no command that could find them again.
|
|
396
|
+
* Two shapes, and which one a workspace writes says who owns the target:
|
|
427
397
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
398
|
+
* - **A declaration** — `credentials` and `storage`, and whatever else the
|
|
399
|
+
* adapter set needs. This workspace *is* that target. `~/.lanes-link` writes
|
|
400
|
+
* one of these for `local`.
|
|
401
|
+
* - **A pointer** — `workspace: gs://bucket[/prefix]`, and nothing else. The
|
|
402
|
+
* target lives elsewhere and the workspace at that URI declares it. This is
|
|
403
|
+
* what a machine holds for `cloud`.
|
|
432
404
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
405
|
+
* The pointer is why there is nothing to sync. ADR-044 added an index beside the
|
|
406
|
+
* profile's own `targets:` block and had to insist it was "an index, not
|
|
407
|
+
* configuration", because resolving from it would have made it a second source
|
|
408
|
+
* of truth. Here it is the *only* source: the profile declares no target at all,
|
|
409
|
+
* so a pointer and a declaration cannot disagree — there is one of them per
|
|
410
|
+
* target, in one file (ADR-052).
|
|
411
|
+
*
|
|
412
|
+
* A declaration for a target that is not deployed yet is the bootstrap case and
|
|
413
|
+
* is allowed: `deploy` reads it, creates the remote workspace, writes the
|
|
414
|
+
* declaration *there*, and replaces this one with a pointer. That is the only
|
|
415
|
+
* moment a target is described in two places, and it does not outlive the
|
|
416
|
+
* command.
|
|
435
417
|
*/
|
|
436
|
-
export const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
418
|
+
export const workspaceTargetSchema = z
|
|
419
|
+
.object({
|
|
420
|
+
/** A pointer: where the workspace declaring this target lives. */
|
|
421
|
+
workspace: z.string().min(1).optional(),
|
|
422
|
+
credentials: credentialsTargetSchema.optional(),
|
|
423
|
+
audit: auditTargetSchema.optional(),
|
|
424
|
+
storage: storageTargetSchema.optional(),
|
|
425
|
+
vault: vaultTargetSchema.optional(),
|
|
426
|
+
deploy: deployTargetSchema.optional(),
|
|
427
|
+
/**
|
|
428
|
+
* Whose bearer token opens this endpoint (ADR-009).
|
|
429
|
+
*
|
|
430
|
+
* Recorded rather than inferred. One endpoint serves every profile in the
|
|
431
|
+
* workspace under one token, and which profile's token that is decides who
|
|
432
|
+
* gets in — the one question about a deployment that must not be guessed at.
|
|
433
|
+
*/
|
|
434
|
+
primary: identifier.optional(),
|
|
435
|
+
last_deploy: z.string().optional(),
|
|
436
|
+
})
|
|
437
|
+
.superRefine((entry, ctx) => {
|
|
438
|
+
const declares = entry.credentials !== undefined || entry.storage !== undefined;
|
|
439
|
+
|
|
440
|
+
// Both is the state ADR-052 exists to prevent, and it is worth refusing
|
|
441
|
+
// rather than preferring one: a pointer beside a declaration is two answers
|
|
442
|
+
// to "where are this target's bytes", and picking either silently is how the
|
|
443
|
+
// fifteen-connection bucket got reported as seven.
|
|
444
|
+
if (declares && entry.workspace !== undefined) {
|
|
445
|
+
ctx.addIssue({
|
|
446
|
+
code: 'custom',
|
|
447
|
+
message:
|
|
448
|
+
'names a "workspace" and also declares adapters — a target is declared by exactly ' +
|
|
449
|
+
'one workspace. Keep the adapters here, or keep the pointer and declare them there.',
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!declares && entry.workspace === undefined) {
|
|
455
|
+
ctx.addIssue({
|
|
456
|
+
code: 'custom',
|
|
457
|
+
message:
|
|
458
|
+
'declares neither "workspace" nor "credentials" and "storage" — a target either ' +
|
|
459
|
+
'lives here or points at where it does.',
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (declares && (entry.credentials === undefined || entry.storage === undefined)) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: 'custom',
|
|
467
|
+
path: [entry.credentials === undefined ? 'credentials' : 'storage'],
|
|
468
|
+
message: 'a target declared here needs both "credentials" and "storage"',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
});
|
|
450
472
|
|
|
451
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* The workspace file: `lanes-link.yaml`, alongside a `profiles/` directory.
|
|
475
|
+
*
|
|
476
|
+
* `default_profile` is parsed and ignored, as it has been since ADR-037 — it
|
|
477
|
+
* survives so `check` can say it is inert rather than the schema stripping it
|
|
478
|
+
* silently. `deployments:` is gone; `targets` above is what it became.
|
|
479
|
+
*/
|
|
452
480
|
export const workspaceSchema = z.object({
|
|
453
481
|
contract: z.number().int().positive(),
|
|
454
482
|
default_profile: identifier.optional(),
|
|
455
|
-
|
|
483
|
+
targets: z.record(z.string(), workspaceTargetSchema).default({}),
|
|
456
484
|
});
|
|
457
485
|
|
|
458
486
|
export type WorkspaceConfig = z.infer<typeof workspaceSchema>;
|
|
459
|
-
export type
|
|
487
|
+
export type WorkspaceTarget = z.infer<typeof workspaceTargetSchema>;
|
|
488
|
+
|
|
489
|
+
/** Whether a registry entry points elsewhere rather than declaring the target. */
|
|
490
|
+
export function isPointer(
|
|
491
|
+
entry: WorkspaceTarget,
|
|
492
|
+
): entry is WorkspaceTarget & { workspace: string } {
|
|
493
|
+
return entry.workspace !== undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The adapter set a registry entry declares, or undefined for a pointer.
|
|
498
|
+
*
|
|
499
|
+
* A pointer has to be followed before there is anything to open, which is
|
|
500
|
+
* `resolveTargetWorkspace`'s job — this only narrows the shape once it has been.
|
|
501
|
+
*/
|
|
502
|
+
export function declaredTarget(entry: WorkspaceTarget): TargetConfig | undefined {
|
|
503
|
+
if (entry.credentials === undefined || entry.storage === undefined) return undefined;
|
|
504
|
+
return {
|
|
505
|
+
credentials: entry.credentials,
|
|
506
|
+
storage: entry.storage,
|
|
507
|
+
...(entry.audit ? { audit: entry.audit } : {}),
|
|
508
|
+
...(entry.vault ? { vault: entry.vault } : {}),
|
|
509
|
+
...(entry.deploy ? { deploy: entry.deploy } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
package/src/profile/targets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigError } from './load.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { isPointer, type WorkspaceTarget } from './schema.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which target a command acts on.
|
|
@@ -9,8 +9,9 @@ import type { Config } from './schema.ts';
|
|
|
9
9
|
* the profile, which is why it is a different file.
|
|
10
10
|
*
|
|
11
11
|
* **`--target`, or the command does not run** (ADR-037). There is no fallback:
|
|
12
|
-
* not `LANES_LINK_TARGET`, not `instance.default_target`.
|
|
13
|
-
*
|
|
12
|
+
* not `LANES_LINK_TARGET`, not `instance.default_target`. The variable is still
|
|
13
|
+
* named in a refusal so a shell configured for the old world can be told it is
|
|
14
|
+
* ignored; the key went with contract 1.
|
|
14
15
|
*
|
|
15
16
|
* The chain this replaces resolved `--target`, then the variable, then the key,
|
|
16
17
|
* and printed which of the three it landed on. What that bought was one flag
|
|
@@ -19,10 +20,18 @@ import type { Config } from './schema.ts';
|
|
|
19
20
|
* and the next command carried on from a different source, so the mistake
|
|
20
21
|
* surfaced one command later with nothing connecting it to its cause. A
|
|
21
22
|
* resolver with nowhere to fall back to cannot fail that way.
|
|
23
|
+
*
|
|
24
|
+
* **What changed under ADR-052** is where the list of targets comes from. It was
|
|
25
|
+
* one profile's `targets:` block, which meant "is this target declared" had a
|
|
26
|
+
* different answer per profile and a deployment could look vanished from inside
|
|
27
|
+
* one of them. It is now the workspace registry — one list, before any profile
|
|
28
|
+
* is read, which is what lets `--target` be chosen first.
|
|
22
29
|
*/
|
|
23
30
|
|
|
31
|
+
export type Registry = Record<string, WorkspaceTarget>;
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
|
-
* The target this command named, checked against
|
|
34
|
+
* The target this command named, checked against the workspace registry.
|
|
26
35
|
*
|
|
27
36
|
* `allowUndeclared` is for the one command whose job is to create the target it
|
|
28
37
|
* was given — `deploy`, on a first run. Every other command naming a target that
|
|
@@ -30,14 +39,14 @@ import type { Config } from './schema.ts';
|
|
|
30
39
|
* answer.
|
|
31
40
|
*/
|
|
32
41
|
export function requireTarget(
|
|
33
|
-
|
|
42
|
+
registry: Registry,
|
|
34
43
|
targetFlag: string | undefined,
|
|
35
|
-
options: { allowUndeclared?: boolean;
|
|
44
|
+
options: { allowUndeclared?: boolean; root?: string } = {},
|
|
36
45
|
): string {
|
|
37
|
-
if (!targetFlag) throw noTargetNamed(
|
|
46
|
+
if (!targetFlag) throw noTargetNamed(registry, options.root);
|
|
38
47
|
|
|
39
|
-
if (options.allowUndeclared !== true && !(targetFlag in
|
|
40
|
-
throw
|
|
48
|
+
if (options.allowUndeclared !== true && !(targetFlag in registry)) {
|
|
49
|
+
throw notInRegistry(targetFlag, registry, options.root);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
return targetFlag;
|
|
@@ -46,43 +55,41 @@ export function requireTarget(
|
|
|
46
55
|
/**
|
|
47
56
|
* The refusal for a command that named no target.
|
|
48
57
|
*
|
|
49
|
-
* It lists the targets
|
|
50
|
-
* being asked and
|
|
51
|
-
* the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
58
|
+
* It lists the targets and where each lives, because "which one" is the question
|
|
59
|
+
* being asked and here-versus-elsewhere is what distinguishes them. It also
|
|
60
|
+
* reports the variable that used to answer this and no longer does, since an
|
|
61
|
+
* operator with it exported has every reason to believe it still works.
|
|
62
|
+
*
|
|
63
|
+
* One refusal rather than the two this replaced. `noTargetNamed` used to list
|
|
64
|
+
* one profile's adapters and `noTargetInWorkspace` listed which profiles
|
|
65
|
+
* declared each name — a distinction that existed only because a target was
|
|
66
|
+
* declared per profile. There is one list now, so there is one sentence.
|
|
54
67
|
*/
|
|
55
68
|
export function noTargetNamed(
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
registry: Registry,
|
|
70
|
+
root?: string,
|
|
58
71
|
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
|
59
72
|
): ConfigError {
|
|
60
|
-
const names = Object.keys(
|
|
61
|
-
const
|
|
73
|
+
const names = Object.keys(registry).sort();
|
|
74
|
+
const where = root ?? 'this workspace';
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.join('\n');
|
|
76
|
+
if (names.length === 0) {
|
|
77
|
+
return new ConfigError(
|
|
78
|
+
`--target is required, and ${where} declares none.\n` +
|
|
79
|
+
' Create one with: lanes link profile add <name> --target local',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
70
82
|
|
|
71
83
|
const stale = env[LEGACY_TARGET_ENV];
|
|
72
|
-
const inert = config.instance.default_target;
|
|
73
84
|
|
|
74
85
|
return new ConfigError(
|
|
75
86
|
'--target is required. This command opens a target\'s stores, and nothing\n' +
|
|
76
87
|
'else selects one.\n\n' +
|
|
77
|
-
` Targets
|
|
78
|
-
`\n e.g. lanes link status --
|
|
88
|
+
` Targets in ${where}\n${rows(registry, names)}\n` +
|
|
89
|
+
`\n e.g. lanes link status --target ${names[0]}` +
|
|
79
90
|
(stale
|
|
80
91
|
? `\n\n ${LEGACY_TARGET_ENV}=${stale} is set in this shell and is no longer read.\n` +
|
|
81
92
|
' Unset it, or pass --target.'
|
|
82
|
-
: '') +
|
|
83
|
-
(inert
|
|
84
|
-
? `\n\n instance.default_target: ${inert} is still in this profile. It is no longer\n` +
|
|
85
|
-
' read either, and is safe to delete.'
|
|
86
93
|
: ''),
|
|
87
94
|
);
|
|
88
95
|
}
|
|
@@ -94,72 +101,36 @@ export function noTargetNamed(
|
|
|
94
101
|
* an operator is looking at and reasonably believes is still working — which is
|
|
95
102
|
* the whole difference between "this stopped working" and "this stopped working
|
|
96
103
|
* and here is why".
|
|
104
|
+
*
|
|
105
|
+
* The deployed image still *sets* it, and that is not a contradiction: a
|
|
106
|
+
* revision serves exactly one target and passes it to `serve` on the command
|
|
107
|
+
* line. Nothing resolves from the variable on either side.
|
|
97
108
|
*/
|
|
98
109
|
export const LEGACY_TARGET_ENV = 'LANES_LINK_TARGET';
|
|
99
110
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
* than a sentence naming the available targets survives: the copies drift the
|
|
105
|
-
* moment either learns something the other does not.
|
|
106
|
-
*/
|
|
107
|
-
export function undeclaredTarget(target: string, config: Config, profile?: string): ConfigError {
|
|
108
|
-
const have = Object.keys(config.targets).join(', ') || 'none';
|
|
109
|
-
const whose = profile ? `profile "${profile}"` : 'this profile';
|
|
111
|
+
/** The refusal for a target that is not in the registry, in one spelling. */
|
|
112
|
+
export function notInRegistry(target: string, registry: Registry, root?: string): ConfigError {
|
|
113
|
+
const names = Object.keys(registry).sort();
|
|
114
|
+
const where = root ?? 'this workspace';
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The refusal for a target-scoped command that named no target.
|
|
116
|
-
*
|
|
117
|
-
* The twin of `noTargetNamed`, for the commands whose subject is the target
|
|
118
|
-
* rather than one profile's view of it (ADR-043). It lists the target names the
|
|
119
|
-
* *workspace* declares and who declares each, because the question those
|
|
120
|
-
* commands are asking is "which endpoint", and a target only one profile knows
|
|
121
|
-
* about is the answer to a different question than one they all share.
|
|
122
|
-
*
|
|
123
|
-
* Naming the profiles is not decoration. A target declared by one profile and
|
|
124
|
-
* not its sibling is precisely the state that reads as a deployment having
|
|
125
|
-
* disappeared, and it is invisible from inside either profile alone.
|
|
126
|
-
*/
|
|
127
|
-
export function noTargetInWorkspace(
|
|
128
|
-
declared: ReadonlyMap<string, readonly string[]>,
|
|
129
|
-
workspaceRoot: string,
|
|
130
|
-
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
|
131
|
-
): ConfigError {
|
|
132
|
-
if (declared.size === 0) {
|
|
133
|
-
return new ConfigError(
|
|
134
|
-
`--target is required, and no profile in ${workspaceRoot} declares one.\n` +
|
|
135
|
-
' Create a profile with: lanes link profile add <name> --target local',
|
|
136
|
-
);
|
|
116
|
+
if (names.length === 0) {
|
|
117
|
+
return new ConfigError(`Target "${target}" is not declared, and ${where} declares none.`);
|
|
137
118
|
}
|
|
138
119
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
120
|
+
return new ConfigError(
|
|
121
|
+
`Target "${target}" is not declared in ${where}.\n\n Targets\n${rows(registry, names)}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
142
124
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
function rows(registry: Registry, names: readonly string[]): string {
|
|
126
|
+
return names
|
|
127
|
+
.map((name) => {
|
|
128
|
+
const entry = registry[name]!;
|
|
129
|
+
if (isPointer(entry)) return ` ${name} ${entry.workspace}`;
|
|
130
|
+
const adapters = [entry.credentials?.adapter, entry.storage?.adapter]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(' ');
|
|
133
|
+
return ` ${name} here ${adapters}${entry.deploy ? ' deployable' : ''}`;
|
|
149
134
|
})
|
|
150
135
|
.join('\n');
|
|
151
|
-
|
|
152
|
-
const first = [...declared.keys()][0]!;
|
|
153
|
-
const stale = env[LEGACY_TARGET_ENV];
|
|
154
|
-
|
|
155
|
-
return new ConfigError(
|
|
156
|
-
'--target is required. This command acts on a target, and every profile\n' +
|
|
157
|
-
'that declares it.\n\n' +
|
|
158
|
-
` Targets in ${workspaceRoot}\n${rows}\n` +
|
|
159
|
-
`\n e.g. lanes link status --target ${first}` +
|
|
160
|
-
(stale
|
|
161
|
-
? `\n\n ${LEGACY_TARGET_ENV}=${stale} is set in this shell and is no longer read.\n` +
|
|
162
|
-
' Unset it, or pass --target.'
|
|
163
|
-
: ''),
|
|
164
|
-
);
|
|
165
136
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SUPPORTED_CONTRACT } from './schema.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace files for tests, so a fixture says what it is testing.
|
|
5
|
+
*
|
|
6
|
+
* Under contract 1 a test workspace needed only `contract` and a profile, because
|
|
7
|
+
* the profile carried its own `targets:` block. The target is declared by the
|
|
8
|
+
* workspace now (ADR-052), so every fixture that opens a store needs one here —
|
|
9
|
+
* and thirty of them writing the same eight lines by hand is how a fixture ends
|
|
10
|
+
* up subtly different from the thing it is standing in for.
|
|
11
|
+
*
|
|
12
|
+
* Kept beside the schema rather than under a test directory, following
|
|
13
|
+
* `stores/blobs/testing.ts` and `stores/state/testing.ts`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The adapters each well-known test target gets. */
|
|
17
|
+
const ADAPTERS: Record<string, string> = {
|
|
18
|
+
local: ` credentials: { adapter: file }
|
|
19
|
+
storage: { adapter: filesystem }`,
|
|
20
|
+
// Deployable, because that is what `cloud` means everywhere it is used as a
|
|
21
|
+
// fixture: the tests that name it are about a target with a service in front
|
|
22
|
+
// of it, and one without a `deploy:` block reads as an ordinary bucket.
|
|
23
|
+
cloud: ` credentials: { adapter: gcp-secret-manager, project: my-project }
|
|
24
|
+
storage: { adapter: gcs, bucket: your-bucket }
|
|
25
|
+
vault: { adapter: secret }
|
|
26
|
+
deploy:
|
|
27
|
+
platform: cloudrun
|
|
28
|
+
project: my-project
|
|
29
|
+
region: europe-west1
|
|
30
|
+
service: my-service`,
|
|
31
|
+
staging: ` credentials: { adapter: gcp-secret-manager, project: staging-project }
|
|
32
|
+
storage: { adapter: gcs, bucket: staging-bucket }`,
|
|
33
|
+
s3: ` credentials: { adapter: file }
|
|
34
|
+
storage:
|
|
35
|
+
adapter: s3
|
|
36
|
+
bucket: your-bucket
|
|
37
|
+
endpoint: https://example.storage.example.com/storage/v1/s3
|
|
38
|
+
region: eu-central-1
|
|
39
|
+
access_key_id_ref: s3/access_key_id
|
|
40
|
+
secret_access_key_ref: s3/secret_access_key`,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A `lanes-link.yaml` declaring the named targets.
|
|
45
|
+
*
|
|
46
|
+
* Anything not in the table above is given the local adapters, which is what a
|
|
47
|
+
* test naming an arbitrary target is nearly always after — it wants the name to
|
|
48
|
+
* resolve, not the bytes to go anywhere in particular.
|
|
49
|
+
*/
|
|
50
|
+
export function workspaceYaml(
|
|
51
|
+
targets: readonly string[] = ['local'],
|
|
52
|
+
options: { defaultProfile?: string } = {},
|
|
53
|
+
): string {
|
|
54
|
+
const blocks = targets
|
|
55
|
+
.map((name) => ` ${name}:\n${ADAPTERS[name] ?? ADAPTERS['local']}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
`contract: ${SUPPORTED_CONTRACT}\n` +
|
|
60
|
+
(options.defaultProfile ? `default_profile: ${options.defaultProfile}\n` : '') +
|
|
61
|
+
`targets:\n${blocks}\n`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A registry entry pointing at another workspace, for the pointer paths. */
|
|
66
|
+
export function pointerYaml(
|
|
67
|
+
target: string,
|
|
68
|
+
workspace: string,
|
|
69
|
+
options: { defaultProfile?: string; alsoLocal?: boolean } = {},
|
|
70
|
+
): string {
|
|
71
|
+
const local = options.alsoLocal === false ? '' : ` local:\n${ADAPTERS['local']}\n`;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
`contract: ${SUPPORTED_CONTRACT}\n` +
|
|
75
|
+
(options.defaultProfile ? `default_profile: ${options.defaultProfile}\n` : '') +
|
|
76
|
+
`targets:\n${local} ${target}:\n workspace: ${workspace}\n`
|
|
77
|
+
);
|
|
78
|
+
}
|