@clossys/launcher 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +209 -20
- package/contracts/conversation-contract.md +40 -0
- package/dist/apply-plan-cli.d.ts +7 -0
- package/dist/apply-plan-cli.d.ts.map +1 -0
- package/dist/apply-plan-cli.js +96 -0
- package/dist/apply-plan-cli.js.map +1 -0
- package/dist/apply-plan.d.ts +79 -0
- package/dist/apply-plan.d.ts.map +1 -0
- package/dist/apply-plan.js +129 -0
- package/dist/apply-plan.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +3 -0
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -11
- package/dist/cli.js.map +1 -1
- package/dist/contract.d.ts +28 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +78 -0
- package/dist/contract.js.map +1 -0
- package/dist/core.d.ts +38 -6
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +289 -42
- package/dist/core.js.map +1 -1
- package/dist/doctor-cli.d.ts +4 -0
- package/dist/doctor-cli.d.ts.map +1 -0
- package/dist/doctor-cli.js +32 -0
- package/dist/doctor-cli.js.map +1 -0
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +68 -0
- package/dist/doctor.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +3 -0
- package/dist/host.js.map +1 -1
- package/dist/hosts.d.ts +14 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +61 -0
- package/dist/hosts.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +20 -0
- package/dist/inventory-adoption.d.ts.map +1 -0
- package/dist/inventory-adoption.js +67 -0
- package/dist/inventory-adoption.js.map +1 -0
- package/dist/manifest.d.ts +20 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +106 -0
- package/dist/manifest.js.map +1 -0
- package/dist/model-profile.d.ts +46 -0
- package/dist/model-profile.d.ts.map +1 -0
- package/dist/model-profile.js +98 -0
- package/dist/model-profile.js.map +1 -0
- package/dist/product-repository.d.ts +26 -0
- package/dist/product-repository.d.ts.map +1 -0
- package/dist/product-repository.js +49 -0
- package/dist/product-repository.js.map +1 -0
- package/dist/skills.d.ts +20 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +98 -7
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +57 -1
- package/dist/types.d.ts.map +1 -1
- package/model-profiles/claude-code.json +10 -0
- package/model-profiles/codex.json +10 -0
- package/model-profiles/cursor.json +10 -0
- package/package.json +9 -4
- package/skeleton/README.md +5 -0
- package/skeleton/package.json +1 -1
- package/skill/SKILL.md +53 -0
- package/skill-catalogue/advisor/SKILL.md +3 -1
- package/skill-catalogue/controller/SKILL.md +8 -0
- package/skill-catalogue/customer/SKILL.md +92 -0
- package/skill-catalogue/designer/SKILL.md +18 -2
- package/skill-catalogue/inspector/SKILL.md +1 -1
- package/skill-catalogue/publisher/SKILL.md +17 -3
- package/skill-catalogue/strategist/SKILL.md +37 -3
- package/skill-catalogue/writer/SKILL.md +7 -2
- package/src/apply-plan-cli.ts +94 -0
- package/src/apply-plan.ts +172 -0
- package/src/check-cli.ts +3 -0
- package/src/cli.ts +45 -10
- package/src/contract.ts +81 -0
- package/src/core.ts +337 -37
- package/src/doctor-cli.ts +33 -0
- package/src/doctor.ts +145 -0
- package/src/host.ts +3 -0
- package/src/hosts.ts +79 -0
- package/src/index.ts +33 -0
- package/src/inventory-adoption.ts +85 -0
- package/src/manifest.ts +103 -0
- package/src/model-profile.ts +148 -0
- package/src/product-repository.ts +73 -0
- package/src/skills.ts +113 -8
- package/src/types.ts +58 -1
- /package/skeleton/{.clossys → clossys/.state}/inventory.json +0 -0
- /package/skeleton/{.clossys → clossys/.state}/workspace.json +0 -0
package/src/core.ts
CHANGED
|
@@ -19,12 +19,25 @@ import type {
|
|
|
19
19
|
WorkspacePlanCreate,
|
|
20
20
|
WorkspaceRefusal,
|
|
21
21
|
} from "./types.js";
|
|
22
|
-
import { composeSkills, type SkillCompositionResult } from "./skills.js";
|
|
22
|
+
import { composeSkills, SKILLS_MANIFEST_REL, type SkillCompositionResult } from "./skills.js";
|
|
23
|
+
import { parseSkillManifest, summarizeSkillsManifest } from "./manifest.js";
|
|
24
|
+
import { detectLinkedHosts, serializeHostRecord, HOSTS_REL, type DiscoveredHost } from "./hosts.js";
|
|
25
|
+
import { reportInventoryDrift } from "./inventory-adoption.js";
|
|
23
26
|
|
|
24
27
|
export const DEFAULT_REPOSITORY_NAME = "workspace";
|
|
25
|
-
|
|
26
|
-
export const
|
|
28
|
+
/** The one visible, per-repository Clossys folder (#1171). Every role's output lives under it. */
|
|
29
|
+
export const CLOSSYS_DIR_REL = "clossys";
|
|
30
|
+
/** Machine files only: hub marker, inventory, skills manifest. Visible (not dot-hidden) so it is easy to find, but not a place a person edits by hand. */
|
|
31
|
+
export const STATE_DIR_REL = join(CLOSSYS_DIR_REL, ".state");
|
|
32
|
+
export const WORKSPACE_MARKER_REL = join(STATE_DIR_REL, "workspace.json");
|
|
33
|
+
export const WORKSPACE_INVENTORY_REL = join(STATE_DIR_REL, "inventory.json");
|
|
34
|
+
export const CLOSSYS_README_REL = join(CLOSSYS_DIR_REL, "README.md");
|
|
35
|
+
/** Pre-#1171 machine-state directory. Resume migrates it automatically; see `locateHub`. */
|
|
36
|
+
export const LEGACY_STATE_DIR_REL = ".clossys";
|
|
37
|
+
export const LEGACY_WORKSPACE_MARKER_REL = join(LEGACY_STATE_DIR_REL, "workspace.json");
|
|
38
|
+
export const LEGACY_WORKSPACE_INVENTORY_REL = join(LEGACY_STATE_DIR_REL, "inventory.json");
|
|
27
39
|
export const ADVISOR_PACKAGE = "@clossys/advisor";
|
|
40
|
+
export const LAUNCHER_PACKAGE = "@clossys/launcher";
|
|
28
41
|
|
|
29
42
|
const DEPENDENCY_BUCKETS: readonly DependencyBucket[] = [
|
|
30
43
|
"dependencies",
|
|
@@ -56,6 +69,10 @@ is not how we signal incompatibility — \`@clossys-advisor\` is the hiring chec
|
|
|
56
69
|
Run \`npx @clossys/launcher\` again for hub health and to refresh voices on
|
|
57
70
|
clones next to the hub, not as how you talk to packages.
|
|
58
71
|
|
|
72
|
+
The person in this folder is a founder, not an engineer. Speak like a
|
|
73
|
+
person. Do not dump machine identifiers, JSON, hashes, or grant fields
|
|
74
|
+
unless they ask.
|
|
75
|
+
|
|
59
76
|
Advisor is read-only until the sponsor approves a next action.
|
|
60
77
|
`;
|
|
61
78
|
|
|
@@ -75,6 +92,21 @@ This repository is the account hub for Foundry packages. It inventories
|
|
|
75
92
|
where packages are installed and coordinates engagement. It is not a
|
|
76
93
|
product application and does not need the whole catalogue installed here.
|
|
77
94
|
|
|
95
|
+
The person in this folder is a founder, not an engineer. Speak like a
|
|
96
|
+
person. Do not dump machine identifiers, JSON, hashes, or grant fields
|
|
97
|
+
unless they ask.
|
|
98
|
+
|
|
99
|
+
When there is a next step, say only:
|
|
100
|
+
|
|
101
|
+
1. Where we are (one sentence).
|
|
102
|
+
2. What you should do next (one sentence).
|
|
103
|
+
3. What we will not do until you say yes.
|
|
104
|
+
4. Whether anything will be saved to git (usually no).
|
|
105
|
+
|
|
106
|
+
Wait for a plain yes before changing files. "Approved" in chat is
|
|
107
|
+
permission for that one step only. It is not a lasting grant and it
|
|
108
|
+
does not become a commit unless someone later saves a file.
|
|
109
|
+
|
|
78
110
|
Open this folder in your coding agent. Advisor is read-only until you
|
|
79
111
|
approve a next action.
|
|
80
112
|
|
|
@@ -167,8 +199,8 @@ export function isHubDocument(value: unknown): value is HubDocument {
|
|
|
167
199
|
return true;
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
function
|
|
171
|
-
const raw = host.readText(
|
|
202
|
+
function readHubAt(host: WorkspaceHost, path: string): HubDocument | undefined {
|
|
203
|
+
const raw = host.readText(path);
|
|
172
204
|
if (raw === null) return undefined;
|
|
173
205
|
try {
|
|
174
206
|
const parsed: unknown = JSON.parse(raw);
|
|
@@ -178,7 +210,31 @@ function readHub(host: WorkspaceHost, directory: string): HubDocument | undefine
|
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
212
|
|
|
181
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Locates the hub marker across the `.clossys/` -> `clossys/.state/`
|
|
215
|
+
* migration (#1171). `clean`: only the current path has a marker. `legacy`:
|
|
216
|
+
* only the old path does; resume migrates it (see `migrateLegacyHubState`).
|
|
217
|
+
* `indeterminate`: both paths carry a parseable marker; launcher never
|
|
218
|
+
* merges them silently, so `planWorkspace` refuses instead. `none`: neither
|
|
219
|
+
* path has one.
|
|
220
|
+
*/
|
|
221
|
+
function locateHub(
|
|
222
|
+
host: WorkspaceHost,
|
|
223
|
+
directory: string,
|
|
224
|
+
): { document?: HubDocument; migration: "clean" | "legacy" | "indeterminate" | "none" } {
|
|
225
|
+
const current = readHubAt(host, join(directory, WORKSPACE_MARKER_REL));
|
|
226
|
+
const legacy = readHubAt(host, join(directory, LEGACY_WORKSPACE_MARKER_REL));
|
|
227
|
+
if (current !== undefined && legacy !== undefined) return { migration: "indeterminate" };
|
|
228
|
+
if (current !== undefined) return { document: current, migration: "clean" };
|
|
229
|
+
if (legacy !== undefined) return { document: legacy, migration: "legacy" };
|
|
230
|
+
return { migration: "none" };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readHub(host: WorkspaceHost, directory: string): HubDocument | undefined {
|
|
234
|
+
return locateHub(host, directory).document;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Classifies a generated hub inventory (packed template skeleton/clossys/.state/inventory.json; the generated path does not ship) without inventing repositories. */
|
|
182
238
|
export function inspectInventory(raw: string | null): InventoryObservation {
|
|
183
239
|
if (raw === null) return { status: "missing", count: 0 };
|
|
184
240
|
try {
|
|
@@ -232,6 +288,17 @@ export function hasAdvisorPin(manifest: unknown): boolean {
|
|
|
232
288
|
}
|
|
233
289
|
|
|
234
290
|
/** Collects GitHub owner, cwd shape, default-hub presence, and the public Advisor pin. */
|
|
291
|
+
/**
|
|
292
|
+
* Reads the public `@clossys/launcher` registry version, used only to grade
|
|
293
|
+
* catalogue-sourced skill staleness in the health report (#1183). A missing
|
|
294
|
+
* or unparseable read leaves staleness ungraded rather than refusing.
|
|
295
|
+
*/
|
|
296
|
+
export function readLiveLauncherVersion(host: WorkspaceHost): string | undefined {
|
|
297
|
+
const viewed = host.run("npm", ["view", LAUNCHER_PACKAGE, "version"]);
|
|
298
|
+
const version = viewed.stdout.trim();
|
|
299
|
+
return viewed.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
235
302
|
export function observeWorkspace(host: WorkspaceHost): WorkspaceObservation {
|
|
236
303
|
const cwd = host.cwd;
|
|
237
304
|
const ghAvailable = commandAvailable(host, "gh");
|
|
@@ -268,14 +335,17 @@ export function observeWorkspace(host: WorkspaceHost): WorkspaceObservation {
|
|
|
268
335
|
const viewed = host.run("gh", ["repo", "view", `${ownerGuess}/${DEFAULT_REPOSITORY_NAME}`, "--json", "name"]);
|
|
269
336
|
if (viewed.status === 0) remoteDefaultHub = { owner: ownerGuess, repository: DEFAULT_REPOSITORY_NAME };
|
|
270
337
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
338
|
+
const viewedAdvisor = host.run("npm", ["view", ADVISOR_PACKAGE, "version"]);
|
|
339
|
+
const version = viewedAdvisor.stdout.trim();
|
|
340
|
+
if (viewedAdvisor.status === 0 && /^\d+\.\d+\.\d+$/.test(version)) advisorVersion = version;
|
|
341
|
+
|
|
342
|
+
const hubLocation = locateHub(host, cwd);
|
|
343
|
+
// While only the legacy `.clossys/` marker exists, its sibling inventory is
|
|
344
|
+
// the one resume will migrate; read from there so planning sees it too.
|
|
345
|
+
const inventoryRaw =
|
|
346
|
+
hubLocation.migration === "legacy"
|
|
347
|
+
? host.readText(join(cwd, LEGACY_WORKSPACE_INVENTORY_REL))
|
|
348
|
+
: host.readText(join(cwd, WORKSPACE_INVENTORY_REL));
|
|
279
349
|
|
|
280
350
|
const cwdObservation: CwdObservation = {
|
|
281
351
|
absolutePath: cwd,
|
|
@@ -283,9 +353,10 @@ export function observeWorkspace(host: WorkspaceHost): WorkspaceObservation {
|
|
|
283
353
|
git,
|
|
284
354
|
...(githubOwner === undefined ? {} : { githubOwner }),
|
|
285
355
|
...(githubRepository === undefined ? {} : { githubRepository }),
|
|
286
|
-
...(
|
|
356
|
+
...(hubLocation.document === undefined ? {} : { hub: hubLocation.document }),
|
|
357
|
+
...(hubLocation.migration === "none" ? {} : { hubMigration: hubLocation.migration }),
|
|
287
358
|
looksLikeFoundry: looksLikeFoundry(host, cwd),
|
|
288
|
-
inventory: inspectInventory(
|
|
359
|
+
inventory: inspectInventory(inventoryRaw),
|
|
289
360
|
};
|
|
290
361
|
|
|
291
362
|
return {
|
|
@@ -355,7 +426,7 @@ function resolveAdoptInventory(
|
|
|
355
426
|
if (!onDiskPopulated && !trimmed) {
|
|
356
427
|
return refuse(
|
|
357
428
|
"violated",
|
|
358
|
-
"appointing requires a populated generated hub inventory (packed template skeleton/.
|
|
429
|
+
"appointing requires a populated generated hub inventory (packed template skeleton/clossys/.state/inventory.json; the generated path does not ship), or --inventory <path> to a populated inventory document",
|
|
359
430
|
);
|
|
360
431
|
}
|
|
361
432
|
if (!trimmed) return {};
|
|
@@ -391,6 +462,12 @@ export function planWorkspace(
|
|
|
391
462
|
options: { inventoryPath?: string } = {},
|
|
392
463
|
): WorkspaceDecision {
|
|
393
464
|
const { cwd } = observation;
|
|
465
|
+
if (cwd.hubMigration === "indeterminate") {
|
|
466
|
+
return refuse(
|
|
467
|
+
"indeterminate",
|
|
468
|
+
`both ${WORKSPACE_MARKER_REL} and the legacy ${LEGACY_WORKSPACE_MARKER_REL} are present; launcher never merges them silently -- remove one before resuming`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
394
471
|
const envOwner = observation.envOwner ?? (host.env.CLOSSYS_OWNER?.trim() || undefined);
|
|
395
472
|
if (
|
|
396
473
|
envOwner !== undefined &&
|
|
@@ -418,6 +495,8 @@ export function planWorkspace(
|
|
|
418
495
|
repository: repoNameFromSlug(cwd.hub.repository, DEFAULT_REPOSITORY_NAME),
|
|
419
496
|
directory: cwd.absolutePath,
|
|
420
497
|
clone: false,
|
|
498
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
499
|
+
...(cwd.hubMigration === "legacy" ? { migrateFrom: "legacy" as const } : {}),
|
|
421
500
|
};
|
|
422
501
|
}
|
|
423
502
|
if (cwd.git && cwd.githubOwner && cwd.githubRepository) {
|
|
@@ -454,6 +533,7 @@ export function planWorkspace(
|
|
|
454
533
|
repository: observation.remoteDefaultHub.repository,
|
|
455
534
|
directory: cwd.absolutePath,
|
|
456
535
|
clone: true,
|
|
536
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
457
537
|
};
|
|
458
538
|
}
|
|
459
539
|
if (!observation.ghAvailable) {
|
|
@@ -510,12 +590,18 @@ function clossysNames(bucket: unknown, extra: Set<string>): string | undefined {
|
|
|
510
590
|
return advisor;
|
|
511
591
|
}
|
|
512
592
|
|
|
513
|
-
/**
|
|
593
|
+
/**
|
|
594
|
+
* Pins live Advisor in `devDependencies` only. Relocates a pin left in any
|
|
595
|
+
* other bucket and overwrites a frozen version. Does not touch other
|
|
596
|
+
* `@clossys/*` names. A dedicated `{owner}/workspace` hub is named
|
|
597
|
+
* `@owner/workspace`.
|
|
598
|
+
*/
|
|
514
599
|
function mergeAdvisorPin(
|
|
515
600
|
host: WorkspaceHost,
|
|
516
601
|
directory: string,
|
|
517
602
|
skeletonRoot: string,
|
|
518
603
|
advisorVersion: string,
|
|
604
|
+
owner: string,
|
|
519
605
|
repository: string,
|
|
520
606
|
): void {
|
|
521
607
|
const path = join(directory, "package.json");
|
|
@@ -523,7 +609,7 @@ function mergeAdvisorPin(
|
|
|
523
609
|
if (raw === null) {
|
|
524
610
|
const skeleton = host.readText(join(skeletonRoot, "package.json"));
|
|
525
611
|
if (skeleton === null) throw new Error("missing skeleton package.json");
|
|
526
|
-
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner
|
|
612
|
+
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner, repository, advisorVersion }));
|
|
527
613
|
return;
|
|
528
614
|
}
|
|
529
615
|
let manifest: Record<string, unknown>;
|
|
@@ -534,12 +620,21 @@ function mergeAdvisorPin(
|
|
|
534
620
|
} catch {
|
|
535
621
|
throw new Error("existing package.json is unreadable JSON");
|
|
536
622
|
}
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
623
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
624
|
+
if (bucket === "devDependencies") continue;
|
|
625
|
+
const current = manifest[bucket];
|
|
626
|
+
if (!isRecord(current) || !(ADVISOR_PACKAGE in current)) continue;
|
|
627
|
+
const next = { ...current };
|
|
628
|
+
delete next[ADVISOR_PACKAGE];
|
|
629
|
+
if (Object.keys(next).length === 0) delete manifest[bucket];
|
|
630
|
+
else manifest[bucket] = next;
|
|
631
|
+
}
|
|
540
632
|
const devDependencies = isRecord(manifest.devDependencies) ? { ...manifest.devDependencies } : {};
|
|
541
633
|
devDependencies[ADVISOR_PACKAGE] = advisorVersion;
|
|
542
634
|
manifest.devDependencies = devDependencies;
|
|
635
|
+
if (repository === DEFAULT_REPOSITORY_NAME) {
|
|
636
|
+
manifest.name = `@${owner}/${repository}`;
|
|
637
|
+
}
|
|
543
638
|
host.writeText(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
544
639
|
}
|
|
545
640
|
|
|
@@ -615,7 +710,7 @@ function adoptHubFiles(host: WorkspaceHost, skeletonRoot: string, plan: Workspac
|
|
|
615
710
|
const ignore = host.readText(join(skeletonRoot, ".gitignore"));
|
|
616
711
|
if (ignore !== null) writeSkeletonFile(host, plan.directory, ".gitignore", ignore);
|
|
617
712
|
}
|
|
618
|
-
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.repository);
|
|
713
|
+
mergeAdvisorPin(host, plan.directory, skeletonRoot, plan.advisorVersion, plan.owner, plan.repository);
|
|
619
714
|
}
|
|
620
715
|
|
|
621
716
|
function requireZero(result: CommandResult, label: string): void {
|
|
@@ -630,7 +725,14 @@ function requireZero(result: CommandResult, label: string): void {
|
|
|
630
725
|
* registry version, marking a pin older than live as a stale-pin finding and a
|
|
631
726
|
* degraded report.
|
|
632
727
|
*/
|
|
633
|
-
export function reportHubHealth(
|
|
728
|
+
export function reportHubHealth(
|
|
729
|
+
host: WorkspaceHost,
|
|
730
|
+
directory: string,
|
|
731
|
+
liveAdvisorVersion?: string,
|
|
732
|
+
liveLauncherVersion?: string,
|
|
733
|
+
retiredThisRun: readonly string[] = [],
|
|
734
|
+
migration?: HubHealthReport["migration"],
|
|
735
|
+
): HubHealthReport {
|
|
634
736
|
const extra = new Set<string>();
|
|
635
737
|
const pins: Partial<Record<DependencyBucket, string>> = {};
|
|
636
738
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
@@ -657,6 +759,11 @@ export function reportHubHealth(host: WorkspaceHost, directory: string, liveAdvi
|
|
|
657
759
|
? [{ bucket: bucket as DependencyBucket, pinned, grade: "stale", note: `pinned ${pinned} is older than live ${liveAdvisorVersion}` }]
|
|
658
760
|
: [];
|
|
659
761
|
});
|
|
762
|
+
const skillsManifest = summarizeSkillsManifest(
|
|
763
|
+
parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL))),
|
|
764
|
+
liveLauncherVersion,
|
|
765
|
+
retiredThisRun,
|
|
766
|
+
);
|
|
660
767
|
return {
|
|
661
768
|
marker: readHub(host, directory) === undefined ? "missing" : "present",
|
|
662
769
|
inventory: inspectInventory(host.readText(join(directory, WORKSPACE_INVENTORY_REL))),
|
|
@@ -670,7 +777,15 @@ export function reportHubHealth(host: WorkspaceHost, directory: string, liveAdvi
|
|
|
670
777
|
dualPin: Object.values(pins).filter((value) => value !== undefined).length > 1,
|
|
671
778
|
extraClossys: [...extra].sort(),
|
|
672
779
|
pinFindings,
|
|
673
|
-
degraded:
|
|
780
|
+
degraded:
|
|
781
|
+
pinFindings.some((finding) => finding.grade === "stale") ||
|
|
782
|
+
Object.values(pins).filter((value) => value !== undefined).length > 1 ||
|
|
783
|
+
pins.devDependencies === undefined ||
|
|
784
|
+
pins.dependencies !== undefined ||
|
|
785
|
+
pins.optionalDependencies !== undefined ||
|
|
786
|
+
pins.peerDependencies !== undefined,
|
|
787
|
+
...(migration === undefined ? {} : { migration }),
|
|
788
|
+
skillsManifest,
|
|
674
789
|
};
|
|
675
790
|
}
|
|
676
791
|
|
|
@@ -705,7 +820,28 @@ export function formatHubHealth(report: HubHealthReport): string {
|
|
|
705
820
|
for (const skip of report.skillComposition.rosterSkipped ?? []) {
|
|
706
821
|
skillParts.push(`skill roster skipped (${skip.inventoryId}): ${skip.note}`);
|
|
707
822
|
}
|
|
823
|
+
if (report.skillComposition.retired !== undefined && report.skillComposition.retired.length > 0) {
|
|
824
|
+
skillParts.push(`skills retired: ${report.skillComposition.retired.map((name) => `clossys-${name}`).join(", ")}`);
|
|
825
|
+
}
|
|
708
826
|
}
|
|
827
|
+
const skillsManifestLine =
|
|
828
|
+
report.skillsManifest === undefined
|
|
829
|
+
? undefined
|
|
830
|
+
: report.skillsManifest.status === "missing"
|
|
831
|
+
? "skills manifest: missing"
|
|
832
|
+
: `skills: ${report.skillsManifest.stale} out of date, ${report.skillsManifest.retired} retired (${report.skillsManifest.total} composed)`;
|
|
833
|
+
const migrationLine =
|
|
834
|
+
report.migration === undefined ? undefined : `migration: moved hub state from ${report.migration.from} to ${report.migration.to}`;
|
|
835
|
+
const linkedHostsLine =
|
|
836
|
+
report.linkedHosts === undefined
|
|
837
|
+
? undefined
|
|
838
|
+
: `linked hosts: ${report.linkedHosts.length === 0 ? "none detected" : report.linkedHosts.join(", ")}`;
|
|
839
|
+
const inventoryDriftLine =
|
|
840
|
+
report.inventoryDrift === undefined
|
|
841
|
+
? undefined
|
|
842
|
+
: report.inventoryDrift.status === "indeterminate"
|
|
843
|
+
? `inventory drift: indeterminate${report.inventoryDrift.note === undefined ? "" : ` -- ${report.inventoryDrift.note}`}`
|
|
844
|
+
: `inventory drift: external-only ${report.inventoryDrift.externalOnly.length}, launcher-only ${report.inventoryDrift.launcherOnly.length}, agreeing ${report.inventoryDrift.agreeing.length}`;
|
|
709
845
|
return [
|
|
710
846
|
`hub marker: ${report.marker}`,
|
|
711
847
|
`inventory: ${inventory}`,
|
|
@@ -714,6 +850,10 @@ export function formatHubHealth(report: HubHealthReport): string {
|
|
|
714
850
|
`extra @clossys/*: ${extra}`,
|
|
715
851
|
`pin findings: ${findingLine}`,
|
|
716
852
|
`degraded: ${report.degraded ? "yes" : "no"}`,
|
|
853
|
+
...(migrationLine === undefined ? [] : [migrationLine]),
|
|
854
|
+
...(linkedHostsLine === undefined ? [] : [linkedHostsLine]),
|
|
855
|
+
...(inventoryDriftLine === undefined ? [] : [inventoryDriftLine]),
|
|
856
|
+
...(skillsManifestLine === undefined ? [] : [skillsManifestLine]),
|
|
717
857
|
...(skillParts.length === 0 ? [] : skillParts),
|
|
718
858
|
`health: ${JSON.stringify(report)}`,
|
|
719
859
|
].join("\n");
|
|
@@ -724,11 +864,19 @@ function withHealth(
|
|
|
724
864
|
directory: string,
|
|
725
865
|
headline: string,
|
|
726
866
|
liveAdvisorVersion?: string,
|
|
727
|
-
skillComposition?: SkillCompositionResult,
|
|
867
|
+
skillComposition?: SkillCompositionResult & { linkedHosts?: readonly DiscoveredHost[] },
|
|
868
|
+
liveLauncherVersion?: string,
|
|
869
|
+
migration?: HubHealthReport["migration"],
|
|
870
|
+
inventoryDrift?: HubHealthReport["inventoryDrift"],
|
|
728
871
|
): WorkspaceApplyResult {
|
|
872
|
+
const base = reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, skillComposition?.retired ?? [], migration);
|
|
873
|
+
const rosterSkipped = skillComposition?.rosterSkipped ?? [];
|
|
729
874
|
const health: HubHealthReport = {
|
|
730
|
-
...
|
|
875
|
+
...base,
|
|
731
876
|
...(skillComposition === undefined ? {} : { skillComposition }),
|
|
877
|
+
...(skillComposition?.linkedHosts === undefined ? {} : { linkedHosts: skillComposition.linkedHosts }),
|
|
878
|
+
...(inventoryDrift === undefined || inventoryDrift.status === "no-external-source" ? {} : { inventoryDrift }),
|
|
879
|
+
degraded: base.degraded || rosterSkipped.length > 0,
|
|
732
880
|
};
|
|
733
881
|
return {
|
|
734
882
|
state: "satisfied",
|
|
@@ -862,39 +1010,153 @@ function resolveSisterCloneTargets(
|
|
|
862
1010
|
return { targets, skipped };
|
|
863
1011
|
}
|
|
864
1012
|
|
|
1013
|
+
export interface CloneMissingOutcome {
|
|
1014
|
+
readonly inventoryId: string;
|
|
1015
|
+
readonly result: "cloned" | "skipped-other-reason" | "failed";
|
|
1016
|
+
readonly note: string;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Explicit, approved action (#1179, the #1045 pattern): clones every
|
|
1021
|
+
* inventoried repository that resolveSisterCloneTargets's own skip pass
|
|
1022
|
+
* identified as "just needs a clone" (CLONE_NOT_BESIDE_HUB_NOTE), and only
|
|
1023
|
+
* those -- every other skip reason (wrong account, foundry supplier tree,
|
|
1024
|
+
* origin mismatch, invalid slug) is left exactly as skipped, never
|
|
1025
|
+
* attempted. Never called from resume's default path; only from the
|
|
1026
|
+
* --clone-missing flag. Reverses the launcher README's own no-clone
|
|
1027
|
+
* default for exactly this one approved action.
|
|
1028
|
+
*/
|
|
1029
|
+
export function cloneMissingInventoryRepositories(
|
|
1030
|
+
host: WorkspaceHost,
|
|
1031
|
+
hubDirectory: string,
|
|
1032
|
+
hubOwner: string,
|
|
1033
|
+
): readonly CloneMissingOutcome[] {
|
|
1034
|
+
const { skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
1035
|
+
const parent = dirname(resolve(hubDirectory));
|
|
1036
|
+
const outcomes: CloneMissingOutcome[] = [];
|
|
1037
|
+
for (const skip of skipped) {
|
|
1038
|
+
if (skip.note !== CLONE_NOT_BESIDE_HUB_NOTE) {
|
|
1039
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "skipped-other-reason", note: skip.note });
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const parsed = parseInventoryRepositoryId(skip.inventoryId, hubOwner);
|
|
1043
|
+
if (parsed === null) {
|
|
1044
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "failed", note: "inventory id is not a valid repository slug" });
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const siblingPath = join(parent, parsed.repository);
|
|
1048
|
+
const result = host.run("gh", ["repo", "clone", `${hubOwner}/${parsed.repository}`, siblingPath]);
|
|
1049
|
+
if (result.status === 0) {
|
|
1050
|
+
outcomes.push({ inventoryId: skip.inventoryId, result: "cloned", note: `cloned to ${siblingPath}` });
|
|
1051
|
+
} else {
|
|
1052
|
+
outcomes.push({
|
|
1053
|
+
inventoryId: skip.inventoryId,
|
|
1054
|
+
result: "failed",
|
|
1055
|
+
note: `gh repo clone exited ${result.status ?? "null"}: ${result.stderr.trim() || "no stderr"}`,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return outcomes;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
865
1062
|
function hubRosterId(host: WorkspaceHost, hubDirectory: string, hubOwner: string, hubRepository: string): string {
|
|
866
1063
|
const document = readHub(host, hubDirectory);
|
|
867
1064
|
if (document !== undefined) return document.repository;
|
|
868
1065
|
return `${hubOwner}/${hubRepository}`;
|
|
869
1066
|
}
|
|
870
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Regenerates the generated `README.md` at the root of `clossys/`: an index of which `clossys/<role>/`
|
|
1070
|
+
* folders are active here, and what `.state/` holds. Written on every apply
|
|
1071
|
+
* so it never drifts from what is actually on disk (#1171).
|
|
1072
|
+
*/
|
|
1073
|
+
function writeClossysReadme(host: WorkspaceHost, directory: string): void {
|
|
1074
|
+
const root = join(directory, CLOSSYS_DIR_REL);
|
|
1075
|
+
const roles = host
|
|
1076
|
+
.isDirectory(root)
|
|
1077
|
+
? host
|
|
1078
|
+
.readDir(root)
|
|
1079
|
+
.filter((name) => name !== ".state" && name !== "README.md" && host.isDirectory(join(root, name)))
|
|
1080
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1081
|
+
: [];
|
|
1082
|
+
const hasBrief = host.exists(join(root, "brief.json"));
|
|
1083
|
+
const lines = [
|
|
1084
|
+
"# clossys/",
|
|
1085
|
+
"",
|
|
1086
|
+
"Generated by `@clossys/launcher`. This file is rewritten on every launcher",
|
|
1087
|
+
`run to reflect what is active here; do not edit it by hand. Last generated: ${host.now()}.`,
|
|
1088
|
+
"",
|
|
1089
|
+
"## Engagement brief",
|
|
1090
|
+
"",
|
|
1091
|
+
...(hasBrief
|
|
1092
|
+
? ["`clossys/brief.json` — why each role is staffed here, its goals, handoffs, and sequence (owner: @clossys/advisor)."]
|
|
1093
|
+
: ["No engagement brief yet. `@clossys-advisor` writes `clossys/brief.json` once a plan is approved."]),
|
|
1094
|
+
"",
|
|
1095
|
+
"## Active roles",
|
|
1096
|
+
"",
|
|
1097
|
+
...(roles.length === 0
|
|
1098
|
+
? ["No role folder is active here yet."]
|
|
1099
|
+
: roles.map((role) => `- \`clossys/${role}/\` — @clossys-${role}`)),
|
|
1100
|
+
"",
|
|
1101
|
+
"## Machine state",
|
|
1102
|
+
"",
|
|
1103
|
+
"`clossys/.state/` holds machine files only: the hub marker, the inventory,",
|
|
1104
|
+
"and the skills manifest. It is visible so it is easy to find, but it is not",
|
|
1105
|
+
"a place a person edits by hand.",
|
|
1106
|
+
"",
|
|
1107
|
+
];
|
|
1108
|
+
writeSkeletonFile(host, directory, CLOSSYS_README_REL, lines.join("\n"));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Reads which coding-agent hosts already had skill discovery linked in
|
|
1113
|
+
* `directory` BEFORE this call, then records that snapshot to
|
|
1114
|
+
* `clossys/.state/hosts.json` (#1180). Deliberately called ahead of
|
|
1115
|
+
* `composeSkills`, which unconditionally stamps discovery links for every
|
|
1116
|
+
* host once it runs -- reading afterward would report "all hosts" on every
|
|
1117
|
+
* apply and make the record meaningless.
|
|
1118
|
+
*/
|
|
1119
|
+
function recordLinkedHosts(host: WorkspaceHost, directory: string): readonly DiscoveredHost[] {
|
|
1120
|
+
const linkedHosts = detectLinkedHosts(host, directory);
|
|
1121
|
+
writeSkeletonFile(
|
|
1122
|
+
host,
|
|
1123
|
+
directory,
|
|
1124
|
+
HOSTS_REL,
|
|
1125
|
+
serializeHostRecord({ schemaVersion: 1, linkedHosts, recordedAt: host.now() }),
|
|
1126
|
+
);
|
|
1127
|
+
return linkedHosts;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
871
1130
|
function composeSkillRoster(
|
|
872
1131
|
host: WorkspaceHost,
|
|
873
1132
|
hubDirectory: string,
|
|
874
1133
|
hubOwner: string,
|
|
875
1134
|
hubRepository: string,
|
|
876
|
-
options: { launcherPackageRoot: string; skillCatalogueRoot?: string },
|
|
1135
|
+
options: { launcherPackageRoot: string; skillCatalogueRoot?: string; contractPath?: string },
|
|
877
1136
|
): SkillCompositionResult & {
|
|
878
1137
|
readonly rosterTargets: readonly string[];
|
|
879
1138
|
readonly rosterSkipped: readonly { readonly inventoryId: string; readonly note: string }[];
|
|
1139
|
+
readonly linkedHosts: readonly DiscoveredHost[];
|
|
880
1140
|
} {
|
|
881
|
-
const
|
|
1141
|
+
const composeOptions = {
|
|
882
1142
|
launcherPackageRoot: options.launcherPackageRoot,
|
|
883
1143
|
...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
|
|
884
|
-
|
|
1144
|
+
...(options.contractPath === undefined ? {} : { contractPath: options.contractPath }),
|
|
1145
|
+
};
|
|
1146
|
+
const linkedHosts = recordLinkedHosts(host, hubDirectory);
|
|
1147
|
+
const hubSkill = composeSkills(host, hubDirectory, composeOptions);
|
|
885
1148
|
writeConsumerAgentsIfNeeded(host, hubDirectory);
|
|
1149
|
+
writeClossysReadme(host, hubDirectory);
|
|
886
1150
|
const hubId = hubRosterId(host, hubDirectory, hubOwner, hubRepository);
|
|
887
1151
|
const rosterTargets: string[] = [hubId];
|
|
888
1152
|
const { targets, skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
889
1153
|
for (const target of targets) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
...(options.skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot: options.skillCatalogueRoot }),
|
|
893
|
-
});
|
|
1154
|
+
recordLinkedHosts(host, target.directory);
|
|
1155
|
+
composeSkills(host, target.directory, composeOptions);
|
|
894
1156
|
writeSisterConsumerAgentsIfNeeded(host, target.directory);
|
|
895
1157
|
rosterTargets.push(target.inventoryId);
|
|
896
1158
|
}
|
|
897
|
-
return { ...hubSkill, rosterTargets, rosterSkipped: skipped };
|
|
1159
|
+
return { ...hubSkill, rosterTargets, rosterSkipped: skipped, linkedHosts };
|
|
898
1160
|
}
|
|
899
1161
|
|
|
900
1162
|
function finishHubApply(
|
|
@@ -906,12 +1168,40 @@ function finishHubApply(
|
|
|
906
1168
|
hubRepository: string,
|
|
907
1169
|
liveAdvisorVersion?: string,
|
|
908
1170
|
skillCatalogueRoot?: string,
|
|
1171
|
+
contractPath?: string,
|
|
1172
|
+
liveLauncherVersion?: string,
|
|
1173
|
+
migration?: HubHealthReport["migration"],
|
|
909
1174
|
): WorkspaceApplyResult {
|
|
910
1175
|
const skillComposition = composeSkillRoster(host, directory, hubOwner, hubRepository, {
|
|
911
1176
|
launcherPackageRoot,
|
|
912
1177
|
...(skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot }),
|
|
1178
|
+
...(contractPath === undefined ? {} : { contractPath }),
|
|
913
1179
|
});
|
|
914
|
-
|
|
1180
|
+
// #1216: when the hub marker declares an external inventory, report drift against
|
|
1181
|
+
// it on every apply (create's fresh marker never declares one, so this is a no-op there).
|
|
1182
|
+
const hubDocument = readHub(host, directory);
|
|
1183
|
+
const inventoryDrift = reportInventoryDrift(host, directory, hubDocument?.externalInventory, WORKSPACE_INVENTORY_REL);
|
|
1184
|
+
return withHealth(host, directory, headline, liveAdvisorVersion, skillComposition, liveLauncherVersion, migration, inventoryDrift);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Migrates a legacy `.clossys/` hub marker (and its sibling inventory, when
|
|
1189
|
+
* present) to `clossys/.state/`, then removes the old directory. Called only
|
|
1190
|
+
* when `locateHub` found the marker at the legacy path and nowhere else
|
|
1191
|
+
* (`plan.migrateFrom === "legacy"`); a hub with markers at both paths is
|
|
1192
|
+
* refused by `planWorkspace` before apply ever runs, so this never merges
|
|
1193
|
+
* two hub states.
|
|
1194
|
+
*/
|
|
1195
|
+
function migrateLegacyHubState(host: WorkspaceHost, directory: string): HubHealthReport["migration"] {
|
|
1196
|
+
const markerRaw = host.readText(join(directory, LEGACY_WORKSPACE_MARKER_REL));
|
|
1197
|
+
if (markerRaw === null) return undefined;
|
|
1198
|
+
writeSkeletonFile(host, directory, WORKSPACE_MARKER_REL, markerRaw.endsWith("\n") ? markerRaw : `${markerRaw}\n`);
|
|
1199
|
+
const inventoryRaw = host.readText(join(directory, LEGACY_WORKSPACE_INVENTORY_REL));
|
|
1200
|
+
if (inventoryRaw !== null) {
|
|
1201
|
+
writeSkeletonFile(host, directory, WORKSPACE_INVENTORY_REL, inventoryRaw.endsWith("\n") ? inventoryRaw : `${inventoryRaw}\n`);
|
|
1202
|
+
}
|
|
1203
|
+
host.remove(join(directory, LEGACY_STATE_DIR_REL));
|
|
1204
|
+
return { status: "migrated", from: LEGACY_STATE_DIR_REL, to: STATE_DIR_REL };
|
|
915
1205
|
}
|
|
916
1206
|
|
|
917
1207
|
/** Applies a create, resume, or adopt plan through the host. Resume refreshes composed skills and stale AGENTS.md guidance. */
|
|
@@ -923,6 +1213,8 @@ export function applyWorkspacePlan(
|
|
|
923
1213
|
): WorkspaceApplyResult {
|
|
924
1214
|
const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
|
|
925
1215
|
const skillCatalogueRoot = options.skillCatalogueRoot;
|
|
1216
|
+
const contractPath = options.contractPath;
|
|
1217
|
+
const liveLauncherVersion = options.liveLauncherVersion;
|
|
926
1218
|
if (plan.action === "resume") {
|
|
927
1219
|
if (plan.clone) {
|
|
928
1220
|
requireZero(
|
|
@@ -930,6 +1222,7 @@ export function applyWorkspacePlan(
|
|
|
930
1222
|
"gh repo clone",
|
|
931
1223
|
);
|
|
932
1224
|
}
|
|
1225
|
+
const migration = plan.migrateFrom === "legacy" ? migrateLegacyHubState(host, plan.directory) : undefined;
|
|
933
1226
|
return finishHubApply(
|
|
934
1227
|
host,
|
|
935
1228
|
plan.directory,
|
|
@@ -937,8 +1230,11 @@ export function applyWorkspacePlan(
|
|
|
937
1230
|
launcherPackageRoot,
|
|
938
1231
|
plan.owner,
|
|
939
1232
|
plan.repository,
|
|
940
|
-
|
|
1233
|
+
plan.advisorVersion,
|
|
941
1234
|
skillCatalogueRoot,
|
|
1235
|
+
contractPath,
|
|
1236
|
+
liveLauncherVersion,
|
|
1237
|
+
migration,
|
|
942
1238
|
);
|
|
943
1239
|
}
|
|
944
1240
|
if (plan.action === "create") {
|
|
@@ -960,6 +1256,8 @@ export function applyWorkspacePlan(
|
|
|
960
1256
|
plan.repository,
|
|
961
1257
|
plan.advisorVersion,
|
|
962
1258
|
skillCatalogueRoot,
|
|
1259
|
+
contractPath,
|
|
1260
|
+
liveLauncherVersion,
|
|
963
1261
|
);
|
|
964
1262
|
}
|
|
965
1263
|
adoptHubFiles(host, skeletonRoot, plan);
|
|
@@ -972,6 +1270,8 @@ export function applyWorkspacePlan(
|
|
|
972
1270
|
plan.repository,
|
|
973
1271
|
plan.advisorVersion,
|
|
974
1272
|
skillCatalogueRoot,
|
|
1273
|
+
contractPath,
|
|
1274
|
+
liveLauncherVersion,
|
|
975
1275
|
);
|
|
976
1276
|
}
|
|
977
1277
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { isDirectInvocation } from "./cli.js";
|
|
3
|
+
import { createNodeHost } from "./host.js";
|
|
4
|
+
import { renderDoctorReport, runDoctorChecks } from "./doctor.js";
|
|
5
|
+
|
|
6
|
+
export const DOCTOR_USAGE = `Usage: launcher-doctor
|
|
7
|
+
|
|
8
|
+
Read-only. Checks the prerequisites a client needs before the hub exists:
|
|
9
|
+
git, the GitHub command-line tool, whether you are signed in, Node.js, and
|
|
10
|
+
npm. Reports the first thing that is missing, in plain language, with the
|
|
11
|
+
next action to take -- never a dump of everything at once.
|
|
12
|
+
|
|
13
|
+
Exit codes: 0 = ready, 2 = at least one prerequisite is missing.`;
|
|
14
|
+
|
|
15
|
+
export function main(argv: readonly string[]): number {
|
|
16
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
17
|
+
console.log(DOCTOR_USAGE);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (argv.length !== 0) {
|
|
21
|
+
console.error("launcher-doctor: takes no arguments");
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
const host = createNodeHost();
|
|
25
|
+
const report = runDoctorChecks(host);
|
|
26
|
+
console.log(renderDoctorReport(report));
|
|
27
|
+
return report.allSatisfied ? 0 : 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(): void {
|
|
31
|
+
process.exitCode = main(process.argv.slice(2));
|
|
32
|
+
}
|
|
33
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) run();
|