@clossys/launcher 0.3.1 → 0.5.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/README.md +1373 -60
- package/contracts/conversation-contract.md +2 -2
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +467 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +804 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +163 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +403 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +781 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/existing-declaration-adoption.check.d.ts +2 -0
- package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
- package/dist/existing-declaration-adoption.check.js +10 -0
- package/dist/existing-declaration-adoption.check.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +3101 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +35 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +190 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +555 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +203 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +881 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +256 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +882 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +523 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +36 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +568 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +55 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +438 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +23 -7
- package/skill-catalogue/advisor/SKILL.md +59 -6
- package/skill-catalogue/architect/SKILL.md +2 -2
- package/skill-catalogue/bouncer/SKILL.md +2 -2
- package/skill-catalogue/builder/SKILL.md +2 -2
- package/skill-catalogue/butler/SKILL.md +2 -2
- package/skill-catalogue/controller/SKILL.md +2 -2
- package/skill-catalogue/customer/SKILL.md +2 -2
- package/skill-catalogue/designer/SKILL.md +4 -2
- package/skill-catalogue/giver/SKILL.md +2 -2
- package/skill-catalogue/influencer/SKILL.md +2 -2
- package/skill-catalogue/inspector/SKILL.md +2 -2
- package/skill-catalogue/integrator/SKILL.md +2 -2
- package/skill-catalogue/keeper/SKILL.md +2 -2
- package/skill-catalogue/launcher/SKILL.md +23 -7
- package/skill-catalogue/locksmith/SKILL.md +2 -2
- package/skill-catalogue/messenger/SKILL.md +2 -2
- package/skill-catalogue/observer/SKILL.md +2 -2
- package/skill-catalogue/publisher/SKILL.md +2 -2
- package/skill-catalogue/starter/SKILL.md +3 -2
- package/skill-catalogue/strategist/SKILL.md +12 -4
- package/skill-catalogue/writer/SKILL.md +2 -2
- package/src/admission-fixture.ts +585 -0
- package/src/admission.ts +819 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +170 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +987 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/existing-declaration-adoption.check.ts +12 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +3101 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +74 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +660 -0
- package/src/ledger-trust.ts +272 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +915 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +1014 -0
- package/src/plan-command.ts +532 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +580 -0
- package/src/setup-templates.ts +479 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The Launcher-owned guide the apply flow writes as AGENTS_GUIDE_PATH (RFC apply-approved-plan D30). Pure: no node builtin, no
|
|
2
|
+
// clock, no file read. The text is one constant -- no plan text, repository name or client detail -- so every repository gets the
|
|
3
|
+
// same bytes, and ownership is the installed-state ledger's digest of the whole file (D10): no marker sits inside the file.
|
|
4
|
+
|
|
5
|
+
/** Where the guide is written. */
|
|
6
|
+
export const AGENTS_GUIDE_PATH = "clossys/AGENTS.md";
|
|
7
|
+
|
|
8
|
+
/** The skill paths the guide carves out: the composed skills and the discovery links to them. */
|
|
9
|
+
export const CLOSSYS_SKILL_PATTERNS: readonly string[] = [".agents/skills/clossys-*", ".claude/skills/clossys-*", ".cursor/skills/clossys-*"];
|
|
10
|
+
|
|
11
|
+
/** The guide's bytes: LF line endings and one final line feed. */
|
|
12
|
+
export const AGENTS_GUIDE_TEXT = `# clossys/ guide
|
|
13
|
+
|
|
14
|
+
This directory and the \`clossys-*\` skills are owned by the Launcher. The Launcher writes them from an approved plan and checks
|
|
15
|
+
them byte for byte.
|
|
16
|
+
|
|
17
|
+
- Do not edit, rename or duplicate anything under \`clossys/\`.
|
|
18
|
+
- Do not edit, rename or duplicate a \`clossys-*\` skill. They are at:
|
|
19
|
+
${CLOSSYS_SKILL_PATTERNS.map((pattern) => ` - \`${pattern}\``).join("\n")}
|
|
20
|
+
- This repository's own skill policy carves out that namespace: a rule there about editing, renaming or adding skills does not
|
|
21
|
+
apply to a path that matches one of the patterns above.
|
|
22
|
+
- A change to any of them goes through a new approved plan, never a hand edit.
|
|
23
|
+
- Everything outside \`clossys/\` and those patterns stays under this repository's own rules.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
/** True only when the text is exactly the guide: a changed byte, a line-ending change or an added line is false. */
|
|
27
|
+
export function verifyAgentsGuide(text: string): boolean {
|
|
28
|
+
return text === AGENTS_GUIDE_TEXT;
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Compile-time assertions (checked by tsc, never run): no command option, and
|
|
2
|
+
// no input of plan, materialize, verify or body, can carry an approval binding. Admission
|
|
3
|
+
// computes the binding from the hub (#1178); a caller cannot supply one.
|
|
4
|
+
|
|
5
|
+
import type { ApplyCommandOptions } from "./apply-plan-cli.js";
|
|
6
|
+
import type { BodyInput } from "./body-command.js";
|
|
7
|
+
import type { RepositoryChangeSet } from "./change-set-contract.js";
|
|
8
|
+
import type { MaterializeInput, VerifyInput } from "./materialize.js";
|
|
9
|
+
import type { PlanCommandOptions } from "./plan-command.js";
|
|
10
|
+
|
|
11
|
+
const forged = { kind: "approved", subjectDigest: `sha256:${"f".repeat(64)}` } as const;
|
|
12
|
+
const set = undefined as unknown as RepositoryChangeSet;
|
|
13
|
+
|
|
14
|
+
// @ts-expect-error ApplyCommandOptions has no binding key
|
|
15
|
+
export const commandOptions: ApplyCommandOptions = { binding: forged };
|
|
16
|
+
// @ts-expect-error MaterializeInput has no binding key
|
|
17
|
+
export const materializeInput: MaterializeInput = { clone: "", hub: "", set, texts: {}, binding: forged };
|
|
18
|
+
// @ts-expect-error VerifyInput has no binding key
|
|
19
|
+
export const verifyInput: VerifyInput = { clone: "", hub: "", set, binding: forged };
|
|
20
|
+
// @ts-expect-error BodyInput has no binding key
|
|
21
|
+
export const bodyInput: BodyInput = { clone: "", hub: "", set, heldChangeSets: [], taskRecord: 1, binding: forged };
|
|
22
|
+
// @ts-expect-error PlanCommandOptions has no binding key
|
|
23
|
+
export const planOptions: PlanCommandOptions = { binding: forged };
|
|
24
|
+
// @ts-expect-error PlanCommandOptions has no approval key
|
|
25
|
+
export const planApproval: PlanCommandOptions = { approval: forged };
|
|
26
|
+
// @ts-expect-error PlanCommandOptions has no subjectDigest key
|
|
27
|
+
export const planSubject: PlanCommandOptions = { subjectDigest: forged.subjectDigest };
|
package/src/apply-plan-cli.ts
CHANGED
|
@@ -1,21 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
2
6
|
import { isDirectInvocation } from "./cli.js";
|
|
7
|
+
import { ContractDocumentError, readContractDocument } from "./generated/contract-schema.generated.js";
|
|
3
8
|
import { createNodeHost } from "./host.js";
|
|
4
9
|
import { applyEngagementBrief, validateAdvisorPlan, validateEngagementBrief, type AdvisorPlan, type EngagementBrief } from "./apply-plan.js";
|
|
10
|
+
import {
|
|
11
|
+
MAX_RESPONSE_BYTES,
|
|
12
|
+
REGISTRY_SNAPSHOT_REL,
|
|
13
|
+
RegistrySnapshotError,
|
|
14
|
+
requestedPackageNames,
|
|
15
|
+
takeRegistrySnapshot,
|
|
16
|
+
writeRegistrySnapshot,
|
|
17
|
+
type FetchSnapshotOptions,
|
|
18
|
+
} from "./registry-snapshot.js";
|
|
19
|
+
import { listStoredChangeSets } from "./apply-store.js";
|
|
20
|
+
import { planMain } from "./plan-command.js";
|
|
21
|
+
import { BODY_USAGE, BodyUsageError, bodyRepository, parseBodyArgs } from "./body-command.js";
|
|
22
|
+
import { materializeRepository, verifyRepository, type ApplyStepResult } from "./materialize.js";
|
|
23
|
+
import { formatStatus, statusRepository, type StatusPorts } from "./status.js";
|
|
24
|
+
import type { ReadinessRunner } from "./admission.js";
|
|
25
|
+
import type { RepositoryChangeSet } from "./change-set-contract.js";
|
|
26
|
+
import type { LockfileSpawn } from "./lockfile-regen.js";
|
|
5
27
|
|
|
6
28
|
export const APPLY_PLAN_USAGE = `Usage: launcher-apply-plan --plan <plan.json> --brief <brief.json> --repo <directory>
|
|
29
|
+
launcher-apply-plan plan [--agent codex|claude|cursor] [--adopt-existing <consent.json>] [--help]
|
|
30
|
+
launcher-apply-plan materialize --repo <id>
|
|
31
|
+
launcher-apply-plan verify --repo <id>
|
|
32
|
+
launcher-apply-plan status --repo <id>
|
|
33
|
+
launcher-apply-plan body --repo <id> --task-record <n> [--supersedes <n>]...
|
|
34
|
+
launcher-apply-plan snapshot --request <file> [--out <file>]
|
|
35
|
+
|
|
36
|
+
The plan subcommand is described by launcher-apply-plan plan --help, and the
|
|
37
|
+
snapshot subcommand by launcher-apply-plan snapshot --help.
|
|
7
38
|
|
|
8
39
|
Writes clossys/brief.json into <directory> from the given brief, once the
|
|
9
40
|
given plan's most recent decision is "approved". Refuses, and writes
|
|
10
|
-
nothing, otherwise.
|
|
41
|
+
nothing, otherwise. This is the brief-only path: it does not check what an
|
|
42
|
+
approval binds, so it accepts an approval with or without a subjectDigest.
|
|
11
43
|
|
|
12
44
|
Deterministic mechanics only: this does not decide whether a plan should be
|
|
13
45
|
approved (that is Advisor's job) and does not compute the brief's content
|
|
14
|
-
(that is @clossys/advisor's EngagementBrief
|
|
15
|
-
|
|
46
|
+
(that is @clossys/advisor's EngagementBrief) -- it validates both files
|
|
47
|
+
against the same plan and brief contracts Advisor uses, refusing any field
|
|
48
|
+
those contracts do not declare, writes the one file, and prints the plan's
|
|
49
|
+
canonical digest.
|
|
16
50
|
|
|
17
51
|
Exit codes: 0 = applied, 1 = refused (not approved, or a shape does not
|
|
18
|
-
validate), 2 = a given file could not be read as JSON
|
|
52
|
+
validate), 2 = a given file could not be read as strict JSON (unreadable,
|
|
53
|
+
not valid UTF-8, not valid JSON, or an object repeats a key).`;
|
|
19
54
|
|
|
20
55
|
export class ApplyPlanInputError extends Error {}
|
|
21
56
|
|
|
@@ -39,13 +74,23 @@ function parseArgs(argv: readonly string[]): { help: boolean; planPath?: string;
|
|
|
39
74
|
return { help: false, planPath, briefPath, repoDirectory };
|
|
40
75
|
}
|
|
41
76
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Reads a plan or brief file as strict JSON (#1475): invalid UTF-8, a JSON
|
|
79
|
+
* syntax error, or an object that repeats a key at any depth is refused, so
|
|
80
|
+
* the value validated and digested is exactly the one a reader of the file
|
|
81
|
+
* sees.
|
|
82
|
+
*/
|
|
83
|
+
function readJson(path: string, label: string): unknown {
|
|
84
|
+
let bytes: Uint8Array;
|
|
45
85
|
try {
|
|
46
|
-
|
|
86
|
+
bytes = readFileSync(path);
|
|
47
87
|
} catch {
|
|
48
|
-
throw new ApplyPlanInputError(`${label}
|
|
88
|
+
throw new ApplyPlanInputError(`${label} could not be read: ${path}`);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return readContractDocument(bytes);
|
|
92
|
+
} catch (cause) {
|
|
93
|
+
throw new ApplyPlanInputError(`${label} ${cause instanceof Error ? cause.message : String(cause)}: ${path}`);
|
|
49
94
|
}
|
|
50
95
|
}
|
|
51
96
|
|
|
@@ -58,8 +103,8 @@ export function main(argv: readonly string[], host: ReturnType<typeof createNode
|
|
|
58
103
|
let planRaw: unknown;
|
|
59
104
|
let briefRaw: unknown;
|
|
60
105
|
try {
|
|
61
|
-
planRaw = readJson(
|
|
62
|
-
briefRaw = readJson(
|
|
106
|
+
planRaw = readJson(parsed.planPath as string, "--plan");
|
|
107
|
+
briefRaw = readJson(parsed.briefPath as string, "--brief");
|
|
63
108
|
} catch (cause) {
|
|
64
109
|
console.error(`launcher-apply-plan: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
65
110
|
return 2;
|
|
@@ -80,15 +125,410 @@ export function main(argv: readonly string[], host: ReturnType<typeof createNode
|
|
|
80
125
|
return 1;
|
|
81
126
|
}
|
|
82
127
|
console.log(`wrote ${result.path}`);
|
|
128
|
+
console.log(`plan digest ${result.planDigest}`);
|
|
83
129
|
return 0;
|
|
84
130
|
}
|
|
85
131
|
|
|
86
|
-
|
|
132
|
+
export const SNAPSHOT_USAGE = `Usage: launcher-apply-plan snapshot --request <file> [--out <file>]
|
|
133
|
+
|
|
134
|
+
Takes the registry snapshot a plan's exact packages are resolved from
|
|
135
|
+
(#1178). <file> is the report advisor-package-request prints, saved to a
|
|
136
|
+
file: every name in it must be a package in this package's publishing scope,
|
|
137
|
+
named once. For each name, in name order, this fetches the package's full
|
|
138
|
+
registry document from the registry this package was built for, with no
|
|
139
|
+
registry credential, no .npmrc and no npm CLI, refusing any redirect, any response
|
|
140
|
+
over ${MAX_RESPONSE_BYTES / (1024 * 1024)} MiB and any request that takes too long. It records only what
|
|
141
|
+
the registry snapshot contract declares, validates the whole snapshot against
|
|
142
|
+
that contract, and writes it atomically to --out, by default
|
|
143
|
+
${REGISTRY_SNAPSHOT_REL} under the current directory (the hub).
|
|
144
|
+
|
|
145
|
+
This is the only step of applying a plan that reads the package registry. A
|
|
146
|
+
message names a package by its position in the request, names[<n>], never by
|
|
147
|
+
its name, and never quotes the request or a response.
|
|
148
|
+
|
|
149
|
+
Exit codes: 0 = the snapshot was written (or --help was shown), 2 = no snapshot was written
|
|
150
|
+
(a usage error, an unreadable or invalid request, or any registry answer
|
|
151
|
+
this step cannot record: a transport error, a timeout, a redirect, an
|
|
152
|
+
answer other than 200 or 404, an oversize or non-JSON body, or a snapshot
|
|
153
|
+
the contract refuses). A package the registry does not have (404) is
|
|
154
|
+
recorded as not-found, not refused. On exit 2 an earlier snapshot at the
|
|
155
|
+
output path is left untouched, and must not be used.`;
|
|
156
|
+
|
|
157
|
+
export interface SnapshotCommandOptions extends FetchSnapshotOptions {
|
|
158
|
+
/** The hub root the default --out is relative to, and --request and --out resolve against; the process's cwd by default. */
|
|
159
|
+
readonly cwd?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ApplyCommandOptions {
|
|
163
|
+
readonly cwd?: string;
|
|
164
|
+
readonly clone?: string;
|
|
165
|
+
readonly set?: RepositoryChangeSet;
|
|
166
|
+
readonly texts?: Readonly<Record<string, string>>;
|
|
167
|
+
readonly heldChangeSets?: readonly RepositoryChangeSet[];
|
|
168
|
+
readonly spawn?: LockfileSpawn;
|
|
169
|
+
/** The instant the execution authorization is judged at; the wall clock by default. */
|
|
170
|
+
readonly now?: () => Date;
|
|
171
|
+
readonly toolVersion?: string | null;
|
|
172
|
+
/** Runs the hub's advisor-execution-readiness; the hub's own installed executable by default. */
|
|
173
|
+
readonly runReadiness?: ReadinessRunner;
|
|
174
|
+
/** The read-only GitHub questions `status` asks; read-only `gh api` calls by default. */
|
|
175
|
+
readonly statusPorts?: StatusPorts;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const REPO_ID_SHAPE = /^[^/]+\/[^/]+$/u;
|
|
179
|
+
|
|
180
|
+
function parseRepoSubcommand(argv: readonly string[], label: string): { help: true } | { help: false; id: string } {
|
|
181
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) return { help: true };
|
|
182
|
+
if (argv.length !== 2 || argv[0] !== "--repo") throw new ApplyPlanInputError(`usage: launcher-apply-plan ${label} --repo <id>`);
|
|
183
|
+
const id = argv[1]!;
|
|
184
|
+
const slash = id.indexOf("/");
|
|
185
|
+
const name = slash === -1 ? "" : id.slice(slash + 1);
|
|
186
|
+
if (!REPO_ID_SHAPE.test(id) || name === "." || name === "..") throw new ApplyPlanInputError(`usage: launcher-apply-plan ${label} --repo <id>`);
|
|
187
|
+
return { help: false, id };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function spawnGit(cwd: string, args: string[]): string | null {
|
|
191
|
+
const run = spawnSync("git", ["-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", ...args], {
|
|
192
|
+
cwd,
|
|
193
|
+
encoding: "utf8",
|
|
194
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
195
|
+
});
|
|
196
|
+
if ((run.status ?? 1) !== 0) return null;
|
|
197
|
+
return (run.stdout ?? "").trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveApplyInputs(
|
|
201
|
+
id: string,
|
|
202
|
+
options: ApplyCommandOptions,
|
|
203
|
+
): { hub: string; clone: string; set: RepositoryChangeSet; held: RepositoryChangeSet[]; texts: Readonly<Record<string, string>> } | ApplyStepResult {
|
|
204
|
+
const hub = options.cwd ?? process.cwd();
|
|
205
|
+
if (options.set !== undefined && options.set.repository.id !== id) {
|
|
206
|
+
return { exitCode: 2, verdict: "indeterminate", reason: "change-set-absent" };
|
|
207
|
+
}
|
|
208
|
+
let held: RepositoryChangeSet[] = [];
|
|
209
|
+
try {
|
|
210
|
+
held = listStoredChangeSets(realpathSync(hub));
|
|
211
|
+
} catch {
|
|
212
|
+
held = [];
|
|
213
|
+
}
|
|
214
|
+
if (options.heldChangeSets !== undefined) held = [...options.heldChangeSets];
|
|
215
|
+
let set = options.set;
|
|
216
|
+
if (set === undefined) {
|
|
217
|
+
const matches = held.filter((entry) => entry.repository.id === id);
|
|
218
|
+
if (matches.length === 0) return { exitCode: 2, verdict: "indeterminate", reason: "change-set-absent" };
|
|
219
|
+
if (matches.length === 1) {
|
|
220
|
+
set = matches[0]!;
|
|
221
|
+
} else {
|
|
222
|
+
const clonePath = options.clone ?? resolve(dirname(realpathSync(hub)), id.slice(id.indexOf("/") + 1));
|
|
223
|
+
let root: string;
|
|
224
|
+
try {
|
|
225
|
+
root = realpathSync(clonePath);
|
|
226
|
+
} catch {
|
|
227
|
+
return { exitCode: 2, verdict: "indeterminate", reason: "change-set-absent" };
|
|
228
|
+
}
|
|
229
|
+
const branch = matches[0]!.repository.defaultBranch;
|
|
230
|
+
const tip = spawnGit(root, ["rev-parse", `refs/heads/${branch}`]);
|
|
231
|
+
if (tip === null) return { exitCode: 2, verdict: "indeterminate", reason: "change-set-absent" };
|
|
232
|
+
const filtered = matches.filter((entry) => entry.repository.baseCommit === tip);
|
|
233
|
+
if (filtered.length !== 1) return { exitCode: 2, verdict: "indeterminate", reason: "change-set-absent" };
|
|
234
|
+
set = filtered[0]!;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const clone = options.clone ?? resolve(dirname(realpathSync(hub)), id.slice(id.indexOf("/") + 1));
|
|
238
|
+
const storedTexts =
|
|
239
|
+
set.texts === undefined ? {} : Object.fromEntries(set.texts.map((row) => [row.path, row.text] as const));
|
|
240
|
+
return { hub, clone, set, held, texts: options.texts ?? storedTexts };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const MATERIALIZE_HELP = `Usage: launcher-apply-plan materialize --repo <id>
|
|
244
|
+
|
|
245
|
+
Writes a stored repository change set into the repository's local clone.
|
|
246
|
+
|
|
247
|
+
Refuses, and writes nothing, unless the plan committed at the hub's HEAD
|
|
248
|
+
approves a bundle that holds the change set, or the change set is an apply set
|
|
249
|
+
admitted under the one-approval rule: it follows the approved setup set and
|
|
250
|
+
changes nothing that approval did not already cover. When the change set has
|
|
251
|
+
package acts, the execution authorization in the hub's committed assessment
|
|
252
|
+
must also be current at the time of the run. The approval a change set's
|
|
253
|
+
ledger records is decided from the hub alone; it is never taken from an
|
|
254
|
+
option or defaulted.`;
|
|
255
|
+
const VERIFY_HELP = `Usage: launcher-apply-plan verify --repo <id>
|
|
256
|
+
|
|
257
|
+
Reports whether the repository's local clone matches its stored change set.
|
|
258
|
+
Verify re-checks everything materialize checks before writing, including the
|
|
259
|
+
committed approval and, for a change set with package acts, that the execution
|
|
260
|
+
authorization is still current at the time of the run: a clone whose approval
|
|
261
|
+
was withdrawn or whose authorization expired no longer verifies.`;
|
|
262
|
+
|
|
263
|
+
const STATUS_HELP = `Usage: launcher-apply-plan status --repo <id>
|
|
264
|
+
|
|
265
|
+
Reports what the pull request for the repository's stored change set is doing,
|
|
266
|
+
from read-only evidence: the open pull requests, the default branch's tip, and
|
|
267
|
+
the commits already in the local clone. It needs a full clone: a partial clone
|
|
268
|
+
is refused as indeterminate (partial-clone) before any object is read. It
|
|
269
|
+
changes nothing but the fetch of the default branch into its remote-tracking ref
|
|
270
|
+
that verify also makes: it does not fetch a pull request's head, check anything
|
|
271
|
+
out, or write a file or an index.
|
|
272
|
+
|
|
273
|
+
The state is one of: proposed (an open pull request of this change set, made
|
|
274
|
+
by the person running this, whose body is the one the body command recorded and whose
|
|
275
|
+
head passes every check verify makes),
|
|
276
|
+
applied (the default branch already holds the change set), planned (neither),
|
|
277
|
+
diverged (its pull request or its body does not match), superseded (an older change set of
|
|
278
|
+
this repository has a pull request, even beside this one's) or indeterminate
|
|
279
|
+
(something could not be read or trusted, including a partial clone, a change set
|
|
280
|
+
the body command never recorded a body for, any open pull request whose body names the
|
|
281
|
+
marker word but is not the person's own, and a listing of 100 or more open pull
|
|
282
|
+
requests). It prints the state, a fixed reason
|
|
283
|
+
and #<number> for each pull request it is about, and nothing else. proposed
|
|
284
|
+
does not check the head's ancestry to the base, and nothing in this unit does.
|
|
285
|
+
|
|
286
|
+
Exit codes: 0 = proposed or applied, 1 = diverged, 2 = anything else.`;
|
|
287
|
+
|
|
288
|
+
function printApplyOutcome(label: string, outcome: ApplyStepResult): number {
|
|
289
|
+
const suffix = outcome.exitCode === 0 ? outcome.verdict : `${outcome.verdict} (${outcome.reason})`;
|
|
290
|
+
const detail = outcome.detail === undefined ? "" : `; ${outcome.detail}`;
|
|
291
|
+
if (outcome.exitCode === 0) console.log(`launcher-apply-plan ${label}: ${suffix}${detail}`);
|
|
292
|
+
else console.error(`launcher-apply-plan ${label}: ${suffix}${detail}`);
|
|
293
|
+
return outcome.exitCode;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function materializeMain(argv: readonly string[], options: ApplyCommandOptions = {}): Promise<number> {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = parseRepoSubcommand(argv, "materialize");
|
|
299
|
+
if (parsed.help) {
|
|
300
|
+
console.log(MATERIALIZE_HELP);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
const resolved = resolveApplyInputs(parsed.id, options);
|
|
304
|
+
if ("exitCode" in resolved) return printApplyOutcome("materialize", resolved);
|
|
305
|
+
const outcome = await materializeRepository({
|
|
306
|
+
clone: resolved.clone,
|
|
307
|
+
hub: resolved.hub,
|
|
308
|
+
set: resolved.set,
|
|
309
|
+
texts: resolved.texts,
|
|
310
|
+
heldChangeSets: resolved.held,
|
|
311
|
+
spawn: options.spawn,
|
|
312
|
+
now: options.now,
|
|
313
|
+
toolVersion: options.toolVersion,
|
|
314
|
+
runReadiness: options.runReadiness,
|
|
315
|
+
});
|
|
316
|
+
return printApplyOutcome("materialize", outcome);
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
console.error(`launcher-apply-plan materialize: ${cause instanceof ApplyPlanInputError ? cause.message : "usage: launcher-apply-plan materialize --repo <id>"}`);
|
|
319
|
+
return 2;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function verifyMain(argv: readonly string[], options: ApplyCommandOptions = {}): Promise<number> {
|
|
324
|
+
try {
|
|
325
|
+
const parsed = parseRepoSubcommand(argv, "verify");
|
|
326
|
+
if (parsed.help) {
|
|
327
|
+
console.log(VERIFY_HELP);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
const resolved = resolveApplyInputs(parsed.id, options);
|
|
331
|
+
if ("exitCode" in resolved) return printApplyOutcome("verify", resolved);
|
|
332
|
+
const outcome = await verifyRepository({
|
|
333
|
+
clone: resolved.clone,
|
|
334
|
+
hub: resolved.hub,
|
|
335
|
+
set: resolved.set,
|
|
336
|
+
heldChangeSets: resolved.held,
|
|
337
|
+
now: options.now,
|
|
338
|
+
runReadiness: options.runReadiness,
|
|
339
|
+
});
|
|
340
|
+
return printApplyOutcome("verify", outcome);
|
|
341
|
+
} catch (cause) {
|
|
342
|
+
console.error(`launcher-apply-plan verify: ${cause instanceof ApplyPlanInputError ? cause.message : "usage: launcher-apply-plan verify --repo <id>"}`);
|
|
343
|
+
return 2;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function statusMain(argv: readonly string[], options: ApplyCommandOptions = {}): Promise<number> {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = parseRepoSubcommand(argv, "status");
|
|
350
|
+
if (parsed.help) {
|
|
351
|
+
console.log(STATUS_HELP);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
const resolved = resolveApplyInputs(parsed.id, options);
|
|
355
|
+
if ("exitCode" in resolved) {
|
|
356
|
+
console.log(formatStatus({ state: "indeterminate", reason: resolved.reason }));
|
|
357
|
+
return 2;
|
|
358
|
+
}
|
|
359
|
+
const outcome = await statusRepository({
|
|
360
|
+
clone: resolved.clone,
|
|
361
|
+
hub: resolved.hub,
|
|
362
|
+
set: resolved.set,
|
|
363
|
+
heldChangeSets: resolved.held,
|
|
364
|
+
now: options.now,
|
|
365
|
+
runReadiness: options.runReadiness,
|
|
366
|
+
ports: options.statusPorts,
|
|
367
|
+
});
|
|
368
|
+
console.log(formatStatus(outcome));
|
|
369
|
+
return outcome.exitCode;
|
|
370
|
+
} catch (cause) {
|
|
371
|
+
console.error(`launcher-apply-plan status: ${cause instanceof ApplyPlanInputError ? cause.message : "usage: launcher-apply-plan status --repo <id>"}`);
|
|
372
|
+
return 2;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const BODY_HELP = `Usage: ${BODY_USAGE}
|
|
377
|
+
|
|
378
|
+
Prints the body of the pull request for the repository's stored change set, and
|
|
379
|
+
nothing else, and records the SHA-256 of exactly those bytes as the change set's
|
|
380
|
+
pullRequest.bodySha256. The approval the body shows is decided from the hub at
|
|
381
|
+
the time of the run, as materialize decides it; it is never taken from an
|
|
382
|
+
option. A planned bundle the hub stored for the change set must hold that same
|
|
383
|
+
approval. --task-record is the number of the task-record issue in the
|
|
384
|
+
repository; each --supersedes is the number of the pull request of an older
|
|
385
|
+
change set of this repository that this one replaces, and needs another stored
|
|
386
|
+
change set of the repository. A change set already bound to another body is
|
|
387
|
+
refused, and one already bound to this body prints it again. There is no way to
|
|
388
|
+
undo a binding: a mistyped --task-record or --supersedes leaves the change set
|
|
389
|
+
refused as body-bound. Pass the output to the pull request as a file with
|
|
390
|
+
--body-file; --body "$(...)" drops the final line feed the recorded hash covers.
|
|
391
|
+
|
|
392
|
+
Exit codes: 0 = the body was printed, 1 = refused (a fixed token on standard
|
|
393
|
+
error, nothing on standard output), 2 = indeterminate or a usage error.`;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The `body` subcommand. Standard output is the body and only the body, after it is recorded; a refusal prints one line of fixed
|
|
397
|
+
* tokens to standard error and never an argument.
|
|
398
|
+
*/
|
|
399
|
+
export async function bodyMain(argv: readonly string[], options: ApplyCommandOptions = {}): Promise<number> {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = parseBodyArgs(argv);
|
|
402
|
+
if (parsed.help) {
|
|
403
|
+
console.log(BODY_HELP);
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
const resolved = resolveApplyInputs(parsed.id, options);
|
|
407
|
+
if ("exitCode" in resolved) {
|
|
408
|
+
console.error(`launcher-apply-plan body: indeterminate (${resolved.reason ?? "refused"})`);
|
|
409
|
+
return 2;
|
|
410
|
+
}
|
|
411
|
+
const outcome = await bodyRepository({
|
|
412
|
+
clone: resolved.clone,
|
|
413
|
+
hub: resolved.hub,
|
|
414
|
+
set: resolved.set,
|
|
415
|
+
heldChangeSets: resolved.held,
|
|
416
|
+
taskRecord: parsed.taskRecord,
|
|
417
|
+
supersedes: parsed.supersedes,
|
|
418
|
+
now: options.now,
|
|
419
|
+
runReadiness: options.runReadiness,
|
|
420
|
+
});
|
|
421
|
+
if (outcome.exitCode !== 0) {
|
|
422
|
+
console.error(`launcher-apply-plan body: ${outcome.exitCode === 1 ? "refused" : "indeterminate"} (${outcome.reason})`);
|
|
423
|
+
return outcome.exitCode;
|
|
424
|
+
}
|
|
425
|
+
process.stdout.write(outcome.body);
|
|
426
|
+
return 0;
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
console.error(cause instanceof BodyUsageError ? `launcher-apply-plan body: usage: ${BODY_USAGE}` : "launcher-apply-plan body: indeterminate (body-failed)");
|
|
429
|
+
return 2;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function parseSnapshotArgs(argv: readonly string[]): { help: true } | { help: false; requestPath: string; outPath?: string } {
|
|
434
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) return { help: true };
|
|
435
|
+
const flags = new Map<string, string>();
|
|
436
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
437
|
+
const name = argv[index] as string;
|
|
438
|
+
const value = argv[index + 1];
|
|
439
|
+
if ((name !== "--request" && name !== "--out") || value === undefined || flags.has(name)) {
|
|
440
|
+
throw new RegistrySnapshotError("usage: launcher-apply-plan snapshot --request <file> [--out <file>]");
|
|
441
|
+
}
|
|
442
|
+
flags.set(name, value);
|
|
443
|
+
}
|
|
444
|
+
const requestPath = flags.get("--request");
|
|
445
|
+
if (requestPath === undefined) throw new RegistrySnapshotError("--request is required");
|
|
446
|
+
const outPath = flags.get("--out");
|
|
447
|
+
return outPath === undefined ? { help: false, requestPath } : { help: false, requestPath, outPath };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Reads the request strictly; a refusal names the rule and a position, never the file's text. */
|
|
451
|
+
function readSnapshotRequest(path: string): unknown {
|
|
452
|
+
let bytes: Uint8Array;
|
|
453
|
+
try {
|
|
454
|
+
if (!statSync(path).isFile()) throw new RegistrySnapshotError("the --request file is not a file");
|
|
455
|
+
bytes = readFileSync(path);
|
|
456
|
+
} catch (cause) {
|
|
457
|
+
if (cause instanceof RegistrySnapshotError) throw cause;
|
|
458
|
+
throw new RegistrySnapshotError("the --request file could not be read");
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
return readContractDocument(bytes);
|
|
462
|
+
} catch (cause) {
|
|
463
|
+
if (!(cause instanceof ContractDocumentError)) throw new RegistrySnapshotError("the --request file is not strict JSON");
|
|
464
|
+
const where = cause.position === undefined ? "" : ` at position ${cause.position}`;
|
|
465
|
+
const why = cause.reason === "encoding" ? "is not valid UTF-8" : cause.reason === "repeated-key" ? "repeats a key in one object" : "is not valid JSON";
|
|
466
|
+
throw new RegistrySnapshotError(`the --request file ${why}${where}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The `snapshot` subcommand. Never throws: every refusal prints one line and
|
|
472
|
+
* returns 2, and nothing is written unless the whole snapshot validates.
|
|
473
|
+
*/
|
|
474
|
+
export async function snapshotMain(argv: readonly string[], options: SnapshotCommandOptions = {}): Promise<number> {
|
|
475
|
+
try {
|
|
476
|
+
const parsed = parseSnapshotArgs(argv);
|
|
477
|
+
if (parsed.help) {
|
|
478
|
+
console.log(SNAPSHOT_USAGE);
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
const cwd = options.cwd ?? process.cwd();
|
|
482
|
+
const names = requestedPackageNames(readSnapshotRequest(resolve(cwd, parsed.requestPath)));
|
|
483
|
+
const snapshot = await takeRegistrySnapshot(names, options);
|
|
484
|
+
const out = resolve(cwd, parsed.outPath ?? REGISTRY_SNAPSHOT_REL);
|
|
485
|
+
try {
|
|
486
|
+
writeRegistrySnapshot(out, snapshot);
|
|
487
|
+
} catch (cause) {
|
|
488
|
+
if (cause instanceof RegistrySnapshotError) throw cause;
|
|
489
|
+
throw new RegistrySnapshotError(`the snapshot could not be written to ${out}`);
|
|
490
|
+
}
|
|
491
|
+
const notFound = snapshot.packages.filter((entry) => entry.status === "not-found").length;
|
|
492
|
+
console.log(`wrote ${out}: ${snapshot.packages.length} package(s), ${snapshot.packages.length - notFound} found, ${notFound} not found`);
|
|
493
|
+
return 0;
|
|
494
|
+
} catch (cause) {
|
|
495
|
+
const reason = cause instanceof RegistrySnapshotError ? cause.message : `failed unexpectedly (${cause instanceof Error ? cause.name : typeof cause})`;
|
|
496
|
+
console.error(`launcher-apply-plan snapshot: ${reason}; no snapshot was written`);
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function run(): Promise<void> {
|
|
502
|
+
const argv = process.argv.slice(2);
|
|
503
|
+
if (argv[0] === "plan") {
|
|
504
|
+
process.exitCode = await planMain(argv.slice(1));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (argv[0] === "materialize") {
|
|
508
|
+
process.exitCode = await materializeMain(argv.slice(1));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (argv[0] === "verify") {
|
|
512
|
+
process.exitCode = await verifyMain(argv.slice(1));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (argv[0] === "status") {
|
|
516
|
+
process.exitCode = await statusMain(argv.slice(1));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (argv[0] === "body") {
|
|
520
|
+
process.exitCode = await bodyMain(argv.slice(1));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (argv[0] === "snapshot") {
|
|
524
|
+
process.exitCode = await snapshotMain(argv.slice(1));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
87
527
|
try {
|
|
88
|
-
process.exitCode = main(
|
|
528
|
+
process.exitCode = main(argv, createNodeHost());
|
|
89
529
|
} catch (cause) {
|
|
90
530
|
console.error(`launcher-apply-plan: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
91
531
|
process.exitCode = 2;
|
|
92
532
|
}
|
|
93
533
|
}
|
|
94
|
-
if (isDirectInvocation(import.meta.url, process.argv[1])) run();
|
|
534
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) void run();
|