@clossys/launcher 0.1.1 → 0.1.2
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/CHANGELOG.md +17 -0
- package/README.md +22 -5
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +5 -1
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +15 -2
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +210 -24
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/check-cli.ts +5 -1
- package/src/cli.ts +5 -1
- package/src/core.ts +218 -21
- package/src/index.ts +7 -0
- package/src/types.ts +34 -0
package/src/core.ts
CHANGED
|
@@ -3,9 +3,13 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import type {
|
|
4
4
|
CommandResult,
|
|
5
5
|
CwdObservation,
|
|
6
|
+
DependencyBucket,
|
|
6
7
|
HubDocument,
|
|
7
8
|
HubHealthReport,
|
|
8
9
|
InventoryObservation,
|
|
10
|
+
InventoryValidationEntry,
|
|
11
|
+
InventoryValidationReport,
|
|
12
|
+
PinFinding,
|
|
9
13
|
WorkspaceApplyResult,
|
|
10
14
|
WorkspaceDecision,
|
|
11
15
|
WorkspaceHost,
|
|
@@ -20,6 +24,13 @@ export const WORKSPACE_MARKER_REL = ".clossys/workspace.json";
|
|
|
20
24
|
export const WORKSPACE_INVENTORY_REL = ".clossys/inventory.json";
|
|
21
25
|
export const ADVISOR_PACKAGE = "@clossys/advisor";
|
|
22
26
|
|
|
27
|
+
const DEPENDENCY_BUCKETS: readonly DependencyBucket[] = [
|
|
28
|
+
"dependencies",
|
|
29
|
+
"devDependencies",
|
|
30
|
+
"optionalDependencies",
|
|
31
|
+
"peerDependencies",
|
|
32
|
+
];
|
|
33
|
+
|
|
23
34
|
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
24
35
|
const REPO = /^[A-Za-z0-9._-]+$/;
|
|
25
36
|
const SKELETON_FILES = [
|
|
@@ -89,6 +100,34 @@ export function parseGitHubRemote(url: string): { owner: string; repository: str
|
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
function readJson(host: WorkspaceHost, path: string): unknown {
|
|
104
|
+
const raw = host.readText(path);
|
|
105
|
+
if (raw === null) return undefined;
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(raw) as unknown;
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Tiny semver-ish compare of `a` versus `b`: -1 older, 0 equal, 1 newer. Unparseable versions are indeterminate (null). */
|
|
114
|
+
function compareVersions(a: string, b: string): -1 | 0 | 1 | null {
|
|
115
|
+
const parse = (version: string): readonly number[] | null => {
|
|
116
|
+
const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/);
|
|
117
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
118
|
+
};
|
|
119
|
+
const left = parse(a);
|
|
120
|
+
const right = parse(b);
|
|
121
|
+
if (!left || !right) return null;
|
|
122
|
+
for (let index = 0; index < 3; index += 1) {
|
|
123
|
+
const l = left[index] ?? 0;
|
|
124
|
+
const r = right[index] ?? 0;
|
|
125
|
+
if (l < r) return -1;
|
|
126
|
+
if (l > r) return 1;
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
92
131
|
/** True when the marker is a v1 account-hub document. */
|
|
93
132
|
export function isHubDocument(value: unknown): value is HubDocument {
|
|
94
133
|
if (!isRecord(value) || value.schemaVersion !== 1 || value.kind !== "account-hub") return false;
|
|
@@ -159,6 +198,12 @@ function commandAvailable(host: WorkspaceHost, command: string): boolean {
|
|
|
159
198
|
return result.status === 0;
|
|
160
199
|
}
|
|
161
200
|
|
|
201
|
+
/** True when the tree already pins Advisor in some bucket, so adopt would not need a new version. */
|
|
202
|
+
export function hasAdvisorPin(manifest: unknown): boolean {
|
|
203
|
+
if (!isRecord(manifest)) return false;
|
|
204
|
+
return DEPENDENCY_BUCKETS.some((bucket) => clossysNames(manifest[bucket], new Set()) !== undefined);
|
|
205
|
+
}
|
|
206
|
+
|
|
162
207
|
/** Collects GitHub owner, cwd shape, default-hub presence, and the public Advisor pin. */
|
|
163
208
|
export function observeWorkspace(host: WorkspaceHost): WorkspaceObservation {
|
|
164
209
|
const cwd = host.cwd;
|
|
@@ -196,9 +241,14 @@ export function observeWorkspace(host: WorkspaceHost): WorkspaceObservation {
|
|
|
196
241
|
const viewed = host.run("gh", ["repo", "view", `${ownerGuess}/${DEFAULT_REPOSITORY_NAME}`, "--json", "name"]);
|
|
197
242
|
if (viewed.status === 0) remoteDefaultHub = { owner: ownerGuess, repository: DEFAULT_REPOSITORY_NAME };
|
|
198
243
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
244
|
+
// Skip the registry read when the tree already pins Advisor in some bucket:
|
|
245
|
+
// adopt leaves an existing pin alone, so no live version is needed to plan it.
|
|
246
|
+
const manifestPinsAdvisor = hasAdvisorPin(readJson(host, join(cwd, "package.json")));
|
|
247
|
+
if (!manifestPinsAdvisor) {
|
|
248
|
+
const viewedAdvisor = host.run("npm", ["view", ADVISOR_PACKAGE, "version"]);
|
|
249
|
+
const version = viewedAdvisor.stdout.trim();
|
|
250
|
+
if (viewedAdvisor.status === 0 && /^\d+\.\d+\.\d+$/.test(version)) advisorVersion = version;
|
|
251
|
+
}
|
|
202
252
|
|
|
203
253
|
const cwdObservation: CwdObservation = {
|
|
204
254
|
absolutePath: cwd,
|
|
@@ -245,19 +295,43 @@ function resolveOwner(observation: WorkspaceObservation, host: WorkspaceHost): {
|
|
|
245
295
|
* Decides create, resume, or adopt from a cwd observation.
|
|
246
296
|
* Appointing means: run this from the GitHub repository that should own the hub.
|
|
247
297
|
*/
|
|
298
|
+
function readInventoryRepositories(host: WorkspaceHost, source: string, label: string): readonly string[] {
|
|
299
|
+
const raw = host.readText(source);
|
|
300
|
+
if (raw === null) return [];
|
|
301
|
+
try {
|
|
302
|
+
const parsed: unknown = JSON.parse(raw);
|
|
303
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.repositories)) return [];
|
|
304
|
+
return parsed.repositories.map((entry) => {
|
|
305
|
+
if (isRecord(entry) && isText(entry.id)) return entry.id.trim();
|
|
306
|
+
return "";
|
|
307
|
+
});
|
|
308
|
+
} catch {
|
|
309
|
+
throw new Error(`${label} is not readable inventory JSON (schemaVersion 1, repositories array)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resolves which repositories the adopt plan inventories. When the on-disk hub
|
|
315
|
+
* inventory is populated AND --inventory is supplied, the two are merged by id:
|
|
316
|
+
* on-disk entries keep their order, ids not already on disk are appended in
|
|
317
|
+
* supplied-file order, and the first occurrence of an id wins; the plan carries
|
|
318
|
+
* the merged ids for applyWorkspacePlan to write. When both are empty or
|
|
319
|
+
* missing, adopt refuses rather than appointing an inventory-less hub.
|
|
320
|
+
*/
|
|
248
321
|
function resolveAdoptInventory(
|
|
249
322
|
host: WorkspaceHost,
|
|
250
323
|
cwd: CwdObservation,
|
|
251
324
|
inventoryPath: string | undefined,
|
|
252
|
-
): { inventorySource?: string } | WorkspaceRefusal {
|
|
253
|
-
if (cwd.inventory?.status === "populated") return {};
|
|
325
|
+
): { inventorySource?: string; mergedInventoryIds?: readonly string[] } | WorkspaceRefusal {
|
|
254
326
|
const trimmed = inventoryPath?.trim();
|
|
255
|
-
|
|
327
|
+
const onDiskPopulated = cwd.inventory?.status === "populated";
|
|
328
|
+
if (!onDiskPopulated && !trimmed) {
|
|
256
329
|
return refuse(
|
|
257
330
|
"violated",
|
|
258
331
|
"appointing requires a populated generated hub inventory (packed template skeleton/.clossys/inventory.json; the generated path does not ship), or --inventory <path> to a populated inventory document",
|
|
259
332
|
);
|
|
260
333
|
}
|
|
334
|
+
if (!trimmed) return {};
|
|
261
335
|
const resolved = resolve(cwd.absolutePath, trimmed);
|
|
262
336
|
const imported = inspectInventory(host.readText(resolved));
|
|
263
337
|
if (imported.status !== "populated") {
|
|
@@ -266,7 +340,22 @@ function resolveAdoptInventory(
|
|
|
266
340
|
"--inventory must point at a populated inventory document (schemaVersion 1, nonempty repositories)",
|
|
267
341
|
);
|
|
268
342
|
}
|
|
269
|
-
return { inventorySource: resolved };
|
|
343
|
+
if (!onDiskPopulated) return { inventorySource: resolved };
|
|
344
|
+
const merged: string[] = [];
|
|
345
|
+
const seen = new Set<string>();
|
|
346
|
+
for (const id of readInventoryRepositories(host, join(cwd.absolutePath, WORKSPACE_INVENTORY_REL), "the on-disk hub inventory")) {
|
|
347
|
+
if (id !== "" && !seen.has(id)) {
|
|
348
|
+
seen.add(id);
|
|
349
|
+
merged.push(id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const id of readInventoryRepositories(host, resolved, "--inventory")) {
|
|
353
|
+
if (id !== "" && !seen.has(id)) {
|
|
354
|
+
seen.add(id);
|
|
355
|
+
merged.push(id);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { mergedInventoryIds: merged };
|
|
270
359
|
}
|
|
271
360
|
|
|
272
361
|
export function planWorkspace(
|
|
@@ -275,6 +364,20 @@ export function planWorkspace(
|
|
|
275
364
|
options: { inventoryPath?: string } = {},
|
|
276
365
|
): WorkspaceDecision {
|
|
277
366
|
const { cwd } = observation;
|
|
367
|
+
const envOwner = observation.envOwner ?? (host.env.CLOSSYS_OWNER?.trim() || undefined);
|
|
368
|
+
if (
|
|
369
|
+
envOwner !== undefined &&
|
|
370
|
+
!cwd.looksLikeFoundry &&
|
|
371
|
+
cwd.hub === undefined &&
|
|
372
|
+
cwd.git &&
|
|
373
|
+
cwd.githubOwner !== undefined &&
|
|
374
|
+
cwd.githubOwner !== envOwner
|
|
375
|
+
) {
|
|
376
|
+
return refuse(
|
|
377
|
+
"violated",
|
|
378
|
+
`CLOSSYS_OWNER is "${envOwner}" but the github.com origin here is owned by "${cwd.githubOwner}"; refusing to appoint a marker for the wrong account`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
278
381
|
if (cwd.looksLikeFoundry) {
|
|
279
382
|
return refuse(
|
|
280
383
|
"violated",
|
|
@@ -303,6 +406,7 @@ export function planWorkspace(
|
|
|
303
406
|
directory: cwd.absolutePath,
|
|
304
407
|
advisorVersion: observation.advisorVersion,
|
|
305
408
|
...(imported.inventorySource === undefined ? {} : { inventorySource: imported.inventorySource }),
|
|
409
|
+
...(imported.mergedInventoryIds === undefined ? {} : { mergedInventoryIds: imported.mergedInventoryIds }),
|
|
306
410
|
};
|
|
307
411
|
}
|
|
308
412
|
if (cwd.git) {
|
|
@@ -404,9 +508,8 @@ function mergeAdvisorPin(
|
|
|
404
508
|
throw new Error("existing package.json is unreadable JSON");
|
|
405
509
|
}
|
|
406
510
|
const extra = new Set<string>();
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
if (inDependencies !== undefined || inDevDependencies !== undefined) return;
|
|
511
|
+
const pinnedSomewhere = DEPENDENCY_BUCKETS.some((bucket) => clossysNames(manifest[bucket], extra) !== undefined);
|
|
512
|
+
if (pinnedSomewhere) return;
|
|
410
513
|
const devDependencies = isRecord(manifest.devDependencies) ? { ...manifest.devDependencies } : {};
|
|
411
514
|
devDependencies[ADVISOR_PACKAGE] = advisorVersion;
|
|
412
515
|
manifest.devDependencies = devDependencies;
|
|
@@ -423,7 +526,30 @@ function copySkeleton(host: WorkspaceHost, skeletonRoot: string, plan: Workspace
|
|
|
423
526
|
writeSkeletonFile(host, plan.directory, "CLAUDE.md", CONSUMER_CLAUDE_MD);
|
|
424
527
|
}
|
|
425
528
|
|
|
529
|
+
/** Refuses adopt when the working tree has uncommitted changes; names a non-github.com remote host. */
|
|
530
|
+
function assertCleanTree(host: WorkspaceHost, directory: string): void {
|
|
531
|
+
const status = host.run("git", ["status", "--porcelain"], { cwd: directory });
|
|
532
|
+
if (status.status === 0 && status.stdout.trim() !== "") {
|
|
533
|
+
const origin = host.run("git", ["remote", "get-url", "origin"], { cwd: directory }).stdout.trim();
|
|
534
|
+
let where = "this checkout";
|
|
535
|
+
if (origin !== "") {
|
|
536
|
+
try {
|
|
537
|
+
const parsed = new URL(origin);
|
|
538
|
+
if (parsed.protocol === "ssh:" || parsed.protocol === "https:" || parsed.protocol === "http:") {
|
|
539
|
+
where = `the repository on ${parsed.hostname}`;
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
/* ssh form or unparsable; keep the generic host */
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
throw new Error(
|
|
546
|
+
`${where} has uncommitted changes (git status --porcelain is nonempty); commit or stash them before appointing it as the account hub`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
426
551
|
function adoptHubFiles(host: WorkspaceHost, skeletonRoot: string, plan: WorkspacePlan & { advisorVersion: string }): void {
|
|
552
|
+
assertCleanTree(host, plan.directory);
|
|
427
553
|
const marker = {
|
|
428
554
|
schemaVersion: 1,
|
|
429
555
|
kind: "account-hub",
|
|
@@ -431,7 +557,15 @@ function adoptHubFiles(host: WorkspaceHost, skeletonRoot: string, plan: Workspac
|
|
|
431
557
|
repository: `${plan.owner}/${plan.repository}`,
|
|
432
558
|
};
|
|
433
559
|
writeSkeletonFile(host, plan.directory, WORKSPACE_MARKER_REL, `${JSON.stringify(marker, null, 2)}\n`);
|
|
434
|
-
if ("
|
|
560
|
+
if ("mergedInventoryIds" in plan && Array.isArray(plan.mergedInventoryIds)) {
|
|
561
|
+
const document = { schemaVersion: 1, repositories: plan.mergedInventoryIds.map((id) => ({ id })) };
|
|
562
|
+
writeSkeletonFile(
|
|
563
|
+
host,
|
|
564
|
+
plan.directory,
|
|
565
|
+
WORKSPACE_INVENTORY_REL,
|
|
566
|
+
`${JSON.stringify(document, null, 2)}\n`,
|
|
567
|
+
);
|
|
568
|
+
} else if ("inventorySource" in plan && typeof plan.inventorySource === "string") {
|
|
435
569
|
const raw = host.readText(plan.inventorySource);
|
|
436
570
|
if (raw === null || inspectInventory(raw).status !== "populated") {
|
|
437
571
|
throw new Error("inventory source is not a populated inventory document");
|
|
@@ -461,55 +595,118 @@ function requireZero(result: CommandResult, label: string): void {
|
|
|
461
595
|
}
|
|
462
596
|
}
|
|
463
597
|
|
|
464
|
-
/**
|
|
598
|
+
/**
|
|
599
|
+
* Read-only pin and inventory report. Does not install or uninstall. Scans all
|
|
600
|
+
* four dependency buckets; grades each pinned advisor version against the live
|
|
601
|
+
* registry version, marking a pin older than live as a stale-pin finding and a
|
|
602
|
+
* degraded report.
|
|
603
|
+
*/
|
|
465
604
|
export function reportHubHealth(host: WorkspaceHost, directory: string, liveAdvisorVersion?: string): HubHealthReport {
|
|
466
605
|
const extra = new Set<string>();
|
|
467
|
-
|
|
468
|
-
let devDependencies: string | undefined;
|
|
606
|
+
const pins: Partial<Record<DependencyBucket, string>> = {};
|
|
469
607
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
470
608
|
if (manifestRaw !== null) {
|
|
471
609
|
try {
|
|
472
610
|
const parsed: unknown = JSON.parse(manifestRaw);
|
|
473
611
|
if (isRecord(parsed)) {
|
|
474
|
-
|
|
475
|
-
|
|
612
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
613
|
+
const pin = clossysNames(parsed[bucket], extra);
|
|
614
|
+
if (pin !== undefined) pins[bucket] = pin;
|
|
615
|
+
}
|
|
476
616
|
}
|
|
477
617
|
} catch {
|
|
478
618
|
/* unreadable manifest is reported as missing pins */
|
|
479
619
|
}
|
|
480
620
|
}
|
|
621
|
+
const pinFindings: PinFinding[] = Object.entries(pins).flatMap(([bucket, pinned]): PinFinding[] => {
|
|
622
|
+
if (liveAdvisorVersion === undefined) return [];
|
|
623
|
+
const comparison = compareVersions(pinned, liveAdvisorVersion);
|
|
624
|
+
if (comparison === null) {
|
|
625
|
+
return [{ bucket: bucket as DependencyBucket, pinned, grade: "indeterminate", note: `cannot compare ${pinned} with live ${liveAdvisorVersion}` }];
|
|
626
|
+
}
|
|
627
|
+
return comparison < 0
|
|
628
|
+
? [{ bucket: bucket as DependencyBucket, pinned, grade: "stale", note: `pinned ${pinned} is older than live ${liveAdvisorVersion}` }]
|
|
629
|
+
: [];
|
|
630
|
+
});
|
|
481
631
|
return {
|
|
482
632
|
marker: readHub(host, directory) === undefined ? "missing" : "present",
|
|
483
633
|
inventory: inspectInventory(host.readText(join(directory, WORKSPACE_INVENTORY_REL))),
|
|
484
634
|
advisorPin: {
|
|
485
|
-
...(dependencies === undefined ? {} : { dependencies }),
|
|
486
|
-
...(devDependencies === undefined ? {} : { devDependencies }),
|
|
635
|
+
...(pins.dependencies === undefined ? {} : { dependencies: pins.dependencies }),
|
|
636
|
+
...(pins.devDependencies === undefined ? {} : { devDependencies: pins.devDependencies }),
|
|
637
|
+
...(pins.optionalDependencies === undefined ? {} : { optionalDependencies: pins.optionalDependencies }),
|
|
638
|
+
...(pins.peerDependencies === undefined ? {} : { peerDependencies: pins.peerDependencies }),
|
|
487
639
|
...(liveAdvisorVersion === undefined ? {} : { live: liveAdvisorVersion }),
|
|
488
640
|
},
|
|
489
|
-
dualPin:
|
|
641
|
+
dualPin: Object.values(pins).filter((value) => value !== undefined).length > 1,
|
|
490
642
|
extraClossys: [...extra].sort(),
|
|
643
|
+
pinFindings,
|
|
644
|
+
degraded: pinFindings.some((finding) => finding.grade === "stale"),
|
|
491
645
|
};
|
|
492
646
|
}
|
|
493
647
|
|
|
494
648
|
export function formatHubHealth(report: HubHealthReport): string {
|
|
495
649
|
const pinParts: string[] = [];
|
|
496
|
-
|
|
497
|
-
|
|
650
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
651
|
+
const pinned = report.advisorPin[bucket];
|
|
652
|
+
if (pinned !== undefined) pinParts.push(`${bucket} ${pinned}`);
|
|
653
|
+
}
|
|
498
654
|
const pin = pinParts.length === 0 ? "missing" : pinParts.join(" and ");
|
|
499
655
|
const live = report.advisorPin.live === undefined ? "" : `; live ${report.advisorPin.live}`;
|
|
500
656
|
const extra = report.extraClossys.length === 0 ? "none" : report.extraClossys.join(", ");
|
|
501
657
|
const inventory =
|
|
502
658
|
report.inventory.status === "populated" ? `populated (${report.inventory.count})` : report.inventory.status;
|
|
659
|
+
const findings = report.pinFindings.map((finding) =>
|
|
660
|
+
finding.note !== undefined ? `${finding.bucket} ${finding.note}` : `${finding.bucket} ${finding.grade}`,
|
|
661
|
+
);
|
|
662
|
+
const findingLine = findings.length === 0 ? "none" : findings.join("; ");
|
|
503
663
|
return [
|
|
504
664
|
`hub marker: ${report.marker}`,
|
|
505
665
|
`inventory: ${inventory}`,
|
|
506
666
|
`advisor pin: ${pin}${live}`,
|
|
507
667
|
`dual pin: ${report.dualPin ? "yes" : "no"}`,
|
|
508
668
|
`extra @clossys/*: ${extra}`,
|
|
669
|
+
`pin findings: ${findingLine}`,
|
|
670
|
+
`degraded: ${report.degraded ? "yes" : "no"}`,
|
|
509
671
|
`health: ${JSON.stringify(report)}`,
|
|
510
672
|
].join("\n");
|
|
511
673
|
}
|
|
512
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Read-only inventory validation: runs `gh repo view --json name` for each
|
|
677
|
+
* repository id, batched. Tolerates a missing or failing `gh` by skipping with
|
|
678
|
+
* a note; marks ids whose check fails as unknown. Never mutates the inventory.
|
|
679
|
+
*/
|
|
680
|
+
export function checkInventoryEntries(host: WorkspaceHost, directory: string): InventoryValidationReport {
|
|
681
|
+
const raw = host.readText(join(directory, WORKSPACE_INVENTORY_REL));
|
|
682
|
+
const observation = inspectInventory(raw);
|
|
683
|
+
if (observation.status !== "populated") {
|
|
684
|
+
return { entries: [], skipped: true, note: `inventory is ${observation.status}; nothing to validate` };
|
|
685
|
+
}
|
|
686
|
+
const available = commandAvailable(host, "gh");
|
|
687
|
+
if (!available) {
|
|
688
|
+
return { entries: [], skipped: true, note: "`gh` is unavailable; inventory ids were not validated" };
|
|
689
|
+
}
|
|
690
|
+
const ids = readInventoryRepositories(host, join(directory, WORKSPACE_INVENTORY_REL), "the hub inventory").filter(
|
|
691
|
+
(id) => id !== "",
|
|
692
|
+
);
|
|
693
|
+
const batch = 20;
|
|
694
|
+
const entries: InventoryValidationEntry[] = [];
|
|
695
|
+
for (let index = 0; index < ids.length; index += batch) {
|
|
696
|
+
for (const id of ids.slice(index, index + batch)) {
|
|
697
|
+
const viewed = host.run("gh", ["repo", "view", id, "--json", "name"]);
|
|
698
|
+
if (viewed.status === 0) {
|
|
699
|
+
entries.push({ id, known: true });
|
|
700
|
+
} else if (viewed.status === 127 || (viewed.status === null && viewed.stderr.includes("ENOENT"))) {
|
|
701
|
+
entries.push({ id, known: null, note: "`gh` unavailable; not checked" });
|
|
702
|
+
} else {
|
|
703
|
+
entries.push({ id, known: false, note: "`gh repo view` failed; the repository may not exist" });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return { entries, skipped: false };
|
|
708
|
+
}
|
|
709
|
+
|
|
513
710
|
function withHealth(
|
|
514
711
|
host: WorkspaceHost,
|
|
515
712
|
directory: string,
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Consumer-owned workspace hub scaffolder: create, resume, or appoint a GitHub repository. */
|
|
2
2
|
export {
|
|
3
3
|
applyWorkspacePlan,
|
|
4
|
+
checkInventoryEntries,
|
|
4
5
|
formatHubHealth,
|
|
6
|
+
hasAdvisorPin,
|
|
5
7
|
inspectInventory,
|
|
6
8
|
isHubDocument,
|
|
7
9
|
observeWorkspace,
|
|
@@ -15,9 +17,14 @@ export {
|
|
|
15
17
|
export type {
|
|
16
18
|
CommandResult,
|
|
17
19
|
CwdObservation,
|
|
20
|
+
DependencyBucket,
|
|
18
21
|
HubDocument,
|
|
19
22
|
HubHealthReport,
|
|
20
23
|
InventoryObservation,
|
|
24
|
+
InventoryValidationEntry,
|
|
25
|
+
InventoryValidationReport,
|
|
26
|
+
PinFinding,
|
|
27
|
+
PinGrade,
|
|
21
28
|
WorkspaceApplyResult,
|
|
22
29
|
WorkspaceDecision,
|
|
23
30
|
WorkspaceHost,
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,34 @@ export interface InventoryObservation {
|
|
|
37
37
|
readonly count: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** A package.json dependency bucket scanned for the advisor pin. */
|
|
41
|
+
export type DependencyBucket = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
|
|
42
|
+
|
|
43
|
+
/** Staleness verdict for one pinned advisor version against the live registry version. */
|
|
44
|
+
export type PinGrade = "stale" | "current" | "indeterminate";
|
|
45
|
+
|
|
46
|
+
/** One graded advisor pin in one dependency bucket. */
|
|
47
|
+
export interface PinFinding {
|
|
48
|
+
readonly bucket: DependencyBucket;
|
|
49
|
+
readonly pinned: string;
|
|
50
|
+
readonly grade: PinGrade;
|
|
51
|
+
readonly note?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Verdict for one hub inventory repository id after a read-only existence check. */
|
|
55
|
+
export interface InventoryValidationEntry {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly known: boolean | null;
|
|
58
|
+
readonly note?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read-only validation outcome for hub inventory ids. Never mutates the inventory. */
|
|
62
|
+
export interface InventoryValidationReport {
|
|
63
|
+
readonly entries: readonly InventoryValidationEntry[];
|
|
64
|
+
readonly skipped: boolean;
|
|
65
|
+
readonly note?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
/** Read-only pin and inventory report after adopt or resume. Never uninstalls. */
|
|
41
69
|
export interface HubHealthReport {
|
|
42
70
|
readonly marker: "present" | "missing";
|
|
@@ -44,10 +72,14 @@ export interface HubHealthReport {
|
|
|
44
72
|
readonly advisorPin: {
|
|
45
73
|
readonly dependencies?: string;
|
|
46
74
|
readonly devDependencies?: string;
|
|
75
|
+
readonly optionalDependencies?: string;
|
|
76
|
+
readonly peerDependencies?: string;
|
|
47
77
|
readonly live?: string;
|
|
48
78
|
};
|
|
49
79
|
readonly dualPin: boolean;
|
|
50
80
|
readonly extraClossys: readonly string[];
|
|
81
|
+
readonly pinFindings: readonly PinFinding[];
|
|
82
|
+
readonly degraded: boolean;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
export interface CwdObservation {
|
|
@@ -95,6 +127,8 @@ export interface WorkspacePlanAdopt {
|
|
|
95
127
|
readonly advisorVersion: string;
|
|
96
128
|
/** Absolute path of a populated inventory document to copy. Absent when cwd already has one. */
|
|
97
129
|
readonly inventorySource?: string;
|
|
130
|
+
/** Merged repository ids (on-disk first, then new ids from --inventory) written when both sources are populated. */
|
|
131
|
+
readonly mergedInventoryIds?: readonly string[];
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
export type WorkspacePlan = WorkspacePlanCreate | WorkspacePlanResume | WorkspacePlanAdopt;
|