@clossys/launcher 0.3.1 → 0.4.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 +1324 -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 +453 -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 +799 -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 +157 -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 +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -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/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 +2840 -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 +34 -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 +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -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 +198 -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 +840 -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 +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -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 +493 -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 +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -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 +572 -0
- package/src/admission.ts +816 -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 +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -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/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -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 +637 -0
- package/src/ledger-trust.ts +267 -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 +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -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 +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
package/src/host.ts
CHANGED
|
@@ -58,9 +58,19 @@ export function createNodeHost(cwd = process.cwd(), env: NodeJS.ProcessEnv = pro
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
+
readBytes: (path) => {
|
|
62
|
+
try {
|
|
63
|
+
return readFileSync(path);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
61
68
|
writeText: (path, contents) => {
|
|
62
69
|
writeFileSync(path, contents, "utf8");
|
|
63
70
|
},
|
|
71
|
+
writeBytes: (path, contents) => {
|
|
72
|
+
writeFileSync(path, contents);
|
|
73
|
+
},
|
|
64
74
|
mkdirp: (path) => {
|
|
65
75
|
mkdirSync(path, { recursive: true });
|
|
66
76
|
},
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The one rule for "is this the same GitHub repository" and "is this the
|
|
2
|
+
// same GitHub account" in Launcher (#1179). Every such decision -- the
|
|
3
|
+
// inventory's duplicate rule, a --repositories choice against the stored
|
|
4
|
+
// inventory, the appoint --inventory merge, the hub excluding itself from
|
|
5
|
+
// its own roster, matching a sibling checkout's origin, whether an id
|
|
6
|
+
// belongs to the hub's account, CLOSSYS_OWNER against the origin, and the
|
|
7
|
+
// drift report -- goes through the functions below and nothing else.
|
|
8
|
+
//
|
|
9
|
+
// GitHub compares owner and repository names without regard to letter case,
|
|
10
|
+
// and a bare inventory id names a repository of the hub's own owner, as
|
|
11
|
+
// Launcher's sibling resolution reads it. A folder path is never used as a
|
|
12
|
+
// repository's identity: on a case-insensitive file system two spellings of
|
|
13
|
+
// a path name the same folder, so a path comparison can disagree with the
|
|
14
|
+
// repository it holds.
|
|
15
|
+
|
|
16
|
+
/** An owner or repository name as GitHub compares it. */
|
|
17
|
+
function nameKey(name: string): string {
|
|
18
|
+
return name.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The identity of a repository id: a bare id is qualified with the hub's
|
|
23
|
+
* owner when that owner is known, and the result is compared as GitHub
|
|
24
|
+
* compares names. Without an owner, a bare id stays bare.
|
|
25
|
+
*/
|
|
26
|
+
export function inventoryKey(id: string, hubOwner?: string): string {
|
|
27
|
+
return nameKey(hubOwner !== undefined && !id.includes("/") ? `${hubOwner}/${id}` : id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether two repository ids name the same repository. */
|
|
31
|
+
export function sameRepository(left: string, right: string, hubOwner?: string): boolean {
|
|
32
|
+
return inventoryKey(left, hubOwner) === inventoryKey(right, hubOwner);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether two owner names name the same GitHub account. */
|
|
36
|
+
export function sameOwner(left: string, right: string): boolean {
|
|
37
|
+
return nameKey(left) === nameKey(right);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether a repository id names a repository of `hubOwner`'s account (a bare id always does). */
|
|
41
|
+
export function belongsToOwner(id: string, hubOwner: string): boolean {
|
|
42
|
+
const slash = id.indexOf("/");
|
|
43
|
+
return slash === -1 || sameOwner(id.slice(0, slash), hubOwner);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The first spelling of each distinct owner, in order: case variants of one account are one owner. */
|
|
47
|
+
export function distinctOwners(owners: Iterable<string>): string[] {
|
|
48
|
+
const kept: string[] = [];
|
|
49
|
+
for (const owner of owners) if (!kept.some((existing) => sameOwner(existing, owner))) kept.push(owner);
|
|
50
|
+
return kept;
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
readInventoryRepositories,
|
|
15
15
|
readLiveLauncherVersion,
|
|
16
16
|
reportHubHealth,
|
|
17
|
+
validateInventoryDocument,
|
|
17
18
|
CLOSSYS_DIR_REL,
|
|
18
19
|
CLOSSYS_README_REL,
|
|
19
20
|
DEFAULT_REPOSITORY_NAME,
|
|
@@ -24,17 +25,70 @@ export {
|
|
|
24
25
|
WORKSPACE_INVENTORY_REL,
|
|
25
26
|
WORKSPACE_MARKER_REL,
|
|
26
27
|
} from "./core.js";
|
|
27
|
-
export type { CloneMissingOutcome } from "./core.js";
|
|
28
|
+
export type { CloneMissingOutcome, InventoryValidation, PlanWorkspaceOptions } from "./core.js";
|
|
29
|
+
export type { InventoryReadOptions } from "./inventory-contract.js";
|
|
28
30
|
export { runDoctorChecks, renderDoctorReport } from "./doctor.js";
|
|
29
|
-
export { applyEngagementBrief, isPlanApproved, validateAdvisorPlan, validateEngagementBrief } from "./apply-plan.js";
|
|
30
|
-
export type {
|
|
31
|
+
export { applyEngagementBrief, approvedSubject, isPlanApproved, validateAdvisorPlan, validateEngagementBrief } from "./apply-plan.js";
|
|
32
|
+
export type {
|
|
33
|
+
AdvisorPlan, ApplyBriefResult, BlockerKind, EngagementBrief, EngagementBriefRole, EngagementContext, EngagementContextField, EngagementContextFieldId, GoalDirection, PlanBlocker,
|
|
34
|
+
PlanDecision, PlanKit, PlanPackageAct, PlanStaffing, ValidationResult,
|
|
35
|
+
} from "./apply-plan.js";
|
|
36
|
+
export { PLAN_DIGEST_EXCLUDED_FIELDS, canonicalDigest, canonicalJson, planDigest } from "./plan-digest.js";
|
|
37
|
+
export { CHANGE_SET_DIGEST_EXCLUDED_FIELDS, DERIVED_FILE_DIGEST_FIELDS, bundleDigest, changeSetDigest, changeSetDigestSubject } from "./change-set-digest.js";
|
|
38
|
+
export type { BundleDigestEntry } from "./change-set-digest.js";
|
|
39
|
+
export { AGENTS_GUIDE_PATH, AGENTS_GUIDE_TEXT, CLOSSYS_SKILL_PATTERNS, verifyAgentsGuide } from "./agents-guide.js";
|
|
40
|
+
export { validateApplyBundle, validateRepositoryChangeSet } from "./change-set-contract.js";
|
|
41
|
+
export type {
|
|
42
|
+
ApplyBundle, ApplyBundleRepository, ApplyCheck, ApplyCheckId, ApprovalBinding, ChangeSetDeferral, ChangeSetItem, ChangeSetPhase, ChangeSetRefusal, CheckVerdict, ContentDigest,
|
|
43
|
+
DependencyPlacement, DerivedFileChange, DiscoveryRoot, ExemptionSurfaceKind, FileChange, KeyChange, LedgerInvariant, LockfileName, PackageInvariant, PackageManagerKind,
|
|
44
|
+
PinnedPackage, RefusalReason, ReleaseAgeSurfaceKind, RepositoryChangeSet, RepositoryProfileObservation, RepositoryVisibility, RootEntryDeclaration, WholeFileChange,
|
|
45
|
+
WriteRecordSource,
|
|
46
|
+
} from "./change-set-contract.js";
|
|
47
|
+
export { ledgerSuccession, readInstalledLedger, renderInstalledLedger, serializeInstalledLedger, validateInstalledLedger } from "./ledger-contract.js";
|
|
48
|
+
export { reconcileWholeFile, trustInstalledLedger } from "./ledger-trust.js";
|
|
49
|
+
export type { LedgerTrust, LedgerTrustRule, PlanPackageActs, TrustInstalledLedgerOptions, WholeFileOutcome, WholeFileState } from "./ledger-trust.js";
|
|
50
|
+
export { BUNDLE_STORE_REL, CHANGE_SET_STORE_REL, readStoredApplyBundle, readStoredChangeSet, storeApplyBundle, storeChangeSet } from "./apply-store.js";
|
|
51
|
+
export { isRootEntryName, wouldViolateRootEntries } from "./root-entries.js";
|
|
52
|
+
export type { RootEntriesVerdict } from "./root-entries.js";
|
|
53
|
+
export { editReleaseAgeExemption, verifyReleaseAgeExemption } from "./release-age-edit.js";
|
|
54
|
+
export type { ReleaseAgeEdit, ReleaseAgeEditInput, ReleaseAgeEditRefusalReason, ReleaseAgeVerdict, ReleaseAgeVerifyInput } from "./release-age-edit.js";
|
|
55
|
+
export type { InstalledLedger, LedgerPackageIdentity, LedgerSuccession, LedgerViolation } from "./ledger-contract.js";
|
|
56
|
+
export { PUBLIC_PROBLEM_PLACEHOLDER, planApplyBundle, projectEngagementBrief, serializeEngagementBrief } from "./plan-bundle.js";
|
|
57
|
+
export type { PlanApplyBundleInputs, PlanApplyBundleResult, RepositoryObservation, SkippedRepositoryObservation } from "./plan-bundle.js";
|
|
58
|
+
export { observeRepository } from "./observe-repository.js";
|
|
59
|
+
export type { ObserveRepositoryInput, RepositoryObservationPorts } from "./observe-repository.js";
|
|
31
60
|
export type { DoctorCheckHost, DoctorReport, DoctorStepId, DoctorStepResult } from "./doctor.js";
|
|
32
61
|
export { checkCloudSessionBootstrap } from "./product-repository.js";
|
|
33
62
|
export type { CloudBootstrapCheck, CloudBootstrapReport } from "./product-repository.js";
|
|
63
|
+
// Setup templates: pure renderers for the files a setup change writes.
|
|
64
|
+
export {
|
|
65
|
+
renderSetupTemplate,
|
|
66
|
+
renderStarterRequest,
|
|
67
|
+
renderAdoptionDecisionWorkflow,
|
|
68
|
+
renderProductCiWorkflow,
|
|
69
|
+
STARTER_PIN_RANGE,
|
|
70
|
+
} from "./setup-templates.js";
|
|
71
|
+
export type {
|
|
72
|
+
SetupPackageManager,
|
|
73
|
+
StarterPinInput,
|
|
74
|
+
StarterRequestInput,
|
|
75
|
+
TemplateFile,
|
|
76
|
+
TemplateRefusal,
|
|
77
|
+
TemplateResult,
|
|
78
|
+
} from "./setup-templates.js";
|
|
79
|
+
export {
|
|
80
|
+
renderSnapshotCollector,
|
|
81
|
+
renderAdoptionEvidenceWorkflow,
|
|
82
|
+
renderPathScopeScript,
|
|
83
|
+
renderPathScopeWorkflow,
|
|
84
|
+
OWNED_PATH_PATTERNS,
|
|
85
|
+
} from "./setup-template-scripts.js";
|
|
34
86
|
export { reportInventoryDrift } from "./inventory-adoption.js";
|
|
35
87
|
export type { ExternalInventoryDeclaration, InventoryDriftReport } from "./inventory-adoption.js";
|
|
36
88
|
export { detectLinkedHosts, parseHostRecord, serializeHostRecord, HOSTS_REL } from "./hosts.js";
|
|
37
89
|
export type { DiscoveredHost, HostRecord } from "./hosts.js";
|
|
90
|
+
export { readChangeSetMarker, renderPullRequest } from "./pull-request-body.js";
|
|
91
|
+
export type { PullRequestRefusal, PullRequestRefusalReason, PullRequestText, RenderPullRequestInput } from "./pull-request-body.js";
|
|
38
92
|
export { parsePreferences, readHostModelProfile, resolveModelForTier } from "./model-profile.js";
|
|
39
93
|
export type {
|
|
40
94
|
BudgetPreference,
|
|
@@ -47,10 +101,14 @@ export type {
|
|
|
47
101
|
} from "./model-profile.js";
|
|
48
102
|
export type {
|
|
49
103
|
ApplyWorkspaceOptions,
|
|
104
|
+
ChosenInventory,
|
|
50
105
|
CommandResult,
|
|
51
106
|
CwdObservation,
|
|
52
107
|
DependencyBucket,
|
|
108
|
+
EngineInstallFinding,
|
|
109
|
+
EnginePinChange,
|
|
53
110
|
HubDocument,
|
|
111
|
+
HubEnginePin,
|
|
54
112
|
HubHealthReport,
|
|
55
113
|
HubMigrationState,
|
|
56
114
|
InventoryObservation,
|
|
@@ -69,3 +127,14 @@ export type {
|
|
|
69
127
|
WorkspaceRefusal,
|
|
70
128
|
WorkspaceState,
|
|
71
129
|
} from "./types.js";
|
|
130
|
+
export {
|
|
131
|
+
PROVENANCE_CHECK_BIN,
|
|
132
|
+
PROVENANCE_CHECK_MAX_BUFFER,
|
|
133
|
+
PROVENANCE_CHECK_TIMEOUT_MS,
|
|
134
|
+
checkSetProvenance,
|
|
135
|
+
registrySnapshotDigest,
|
|
136
|
+
} from "./provenance-gate.js";
|
|
137
|
+
export type { ProvenanceGateInput, ProvenanceGatePorts } from "./provenance-gate.js";
|
|
138
|
+
|
|
139
|
+
export { decideSetBinding, planPackagesFor, readHubAuthority } from "./admission.js";
|
|
140
|
+
export type { AdmissionDecision, AdmissionRefusal, HubAuthority, ReadinessRunner } from "./admission.js";
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// it. Launcher writes only what that inventory lacks (a later apply step,
|
|
5
5
|
// not this module) and reports drift instead of silently merging.
|
|
6
6
|
|
|
7
|
+
import { readContractDocument } from "./generated/contract-schema.generated.js";
|
|
8
|
+
import { sameRepository } from "./identity.js";
|
|
9
|
+
import { validateInventoryDocument } from "./inventory-contract.js";
|
|
7
10
|
import type { WorkspaceHost } from "./types.js";
|
|
8
11
|
|
|
9
12
|
export interface ExternalInventoryDeclaration {
|
|
@@ -11,33 +14,69 @@ export interface ExternalInventoryDeclaration {
|
|
|
11
14
|
readonly shape: "foundry" | "custom";
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A count plus each disagreeing or agreeing id's position, never the ids
|
|
19
|
+
* themselves: `externalInventory` at `declaration.path` and Launcher's own
|
|
20
|
+
* stored inventory are both document content a hub declares or writes, and
|
|
21
|
+
* this whole report reaches `formatHubHealth`'s JSON-dumped `health:` line
|
|
22
|
+
* on every resume of a hub that declares `externalInventory`, so an id kept
|
|
23
|
+
* here would still reach that message (#1179). A position names the array
|
|
24
|
+
* it indexes into: `externalInventory[<i>]` for `declaration.path`'s
|
|
25
|
+
* `repositories[<i>].id`, `repositories[<j>]` for the hub's own stored
|
|
26
|
+
* inventory's `repositories[<j>].id`.
|
|
27
|
+
*/
|
|
28
|
+
export interface InventoryDriftPositions {
|
|
29
|
+
readonly count: number;
|
|
30
|
+
readonly positions: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
export interface InventoryDriftReport {
|
|
15
34
|
readonly status: "no-external-source" | "reconciled" | "indeterminate";
|
|
16
|
-
readonly externalOnly:
|
|
17
|
-
readonly launcherOnly:
|
|
18
|
-
readonly agreeing:
|
|
35
|
+
readonly externalOnly: InventoryDriftPositions;
|
|
36
|
+
readonly launcherOnly: InventoryDriftPositions;
|
|
37
|
+
readonly agreeing: InventoryDriftPositions;
|
|
19
38
|
readonly note?: string;
|
|
20
39
|
}
|
|
21
40
|
|
|
41
|
+
const NO_DRIFT: InventoryDriftPositions = { count: 0, positions: [] };
|
|
42
|
+
|
|
22
43
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23
44
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
/** One `externalInventory` entry with a usable id, paired with its own index in that document's `repositories` array. */
|
|
48
|
+
interface ForeignId {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly index: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads a foundry-shaped inventory document's repository ids. Malformed or
|
|
55
|
+
* missing is null, never []. The file is read as bytes by the shared strict
|
|
56
|
+
* reader, so bytes that are not valid UTF-8, a repeated key, or a byte order
|
|
57
|
+
* mark make it unreadable rather than silently repaired (#1179). Its shape
|
|
58
|
+
* is read leniently on purpose: an external source is not Launcher's own
|
|
59
|
+
* document -- a non-object entry, or one with a missing, blank or non-string
|
|
60
|
+
* `id`, is skipped rather than refusing the whole document. Skipped entries
|
|
61
|
+
* still occupy a slot in `repositories`, so each kept id is paired with its
|
|
62
|
+
* own original array index, never a count of ids kept so far: the file's
|
|
63
|
+
* fourth entry (index 3) must be reported as `externalInventory[3]` even
|
|
64
|
+
* when it is the second entry actually kept (#1179).
|
|
65
|
+
*/
|
|
66
|
+
function readForeignIds(host: WorkspaceHost, path: string): readonly ForeignId[] | null {
|
|
67
|
+
const raw = host.readBytes(path);
|
|
29
68
|
if (raw === null) return null;
|
|
30
69
|
let parsed: unknown;
|
|
31
70
|
try {
|
|
32
|
-
parsed =
|
|
71
|
+
parsed = readContractDocument(raw);
|
|
33
72
|
} catch {
|
|
34
73
|
return null;
|
|
35
74
|
}
|
|
36
75
|
if (!isRecord(parsed) || parsed.schemaVersion !== 1 || !Array.isArray(parsed.repositories)) return null;
|
|
37
|
-
const ids:
|
|
38
|
-
|
|
39
|
-
if (isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "") ids.push(entry.id);
|
|
40
|
-
}
|
|
76
|
+
const ids: ForeignId[] = [];
|
|
77
|
+
parsed.repositories.forEach((entry, index) => {
|
|
78
|
+
if (isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "") ids.push({ id: entry.id, index });
|
|
79
|
+
});
|
|
41
80
|
return ids;
|
|
42
81
|
}
|
|
43
82
|
|
|
@@ -45,41 +84,80 @@ function readForeignIds(host: WorkspaceHost, path: string): readonly string[] |
|
|
|
45
84
|
* Compares the declared external inventory against the launcher-written
|
|
46
85
|
* inventory at `directory/launcherInventoryRelPath`. Read-only -- callers
|
|
47
86
|
* decide whether and how to write the reconciled set, as an explicit,
|
|
48
|
-
* approved apply step (same #1045 pattern as clone-on-approval).
|
|
87
|
+
* approved apply step (same #1045 pattern as clone-on-approval). Ids are
|
|
88
|
+
* compared with `sameRepository()` (identity.ts): case-insensitively, and,
|
|
89
|
+
* when `hubOwner` is given, with a bare id read as that owner's repository.
|
|
90
|
+
* The hub's own inventory is read by `validateInventoryDocument()`; when it
|
|
91
|
+
* is present but invalid the report is indeterminate.
|
|
49
92
|
*/
|
|
50
93
|
export function reportInventoryDrift(
|
|
51
94
|
host: WorkspaceHost,
|
|
52
95
|
directory: string,
|
|
53
96
|
declaration: ExternalInventoryDeclaration | undefined,
|
|
54
97
|
launcherInventoryRelPath: string,
|
|
98
|
+
hubOwner?: string,
|
|
55
99
|
): InventoryDriftReport {
|
|
56
100
|
if (declaration === undefined) {
|
|
57
|
-
return { status: "no-external-source", externalOnly:
|
|
101
|
+
return { status: "no-external-source", externalOnly: NO_DRIFT, launcherOnly: NO_DRIFT, agreeing: NO_DRIFT };
|
|
58
102
|
}
|
|
59
103
|
if (declaration.shape === "custom") {
|
|
60
104
|
return {
|
|
61
105
|
status: "indeterminate",
|
|
62
|
-
externalOnly:
|
|
63
|
-
launcherOnly:
|
|
64
|
-
agreeing:
|
|
65
|
-
note: `
|
|
106
|
+
externalOnly: NO_DRIFT,
|
|
107
|
+
launcherOnly: NO_DRIFT,
|
|
108
|
+
agreeing: NO_DRIFT,
|
|
109
|
+
note: `the declared \`externalInventory\` declares a shape launcher has no mapping for yet; launcher will not guess one. Reconcile by hand or file the mapping gap.`,
|
|
66
110
|
};
|
|
67
111
|
}
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
112
|
+
const externalEntries = readForeignIds(host, declaration.path);
|
|
113
|
+
if (externalEntries === null) {
|
|
70
114
|
return {
|
|
71
115
|
status: "indeterminate",
|
|
72
|
-
externalOnly:
|
|
73
|
-
launcherOnly:
|
|
74
|
-
agreeing:
|
|
75
|
-
note: `
|
|
116
|
+
externalOnly: NO_DRIFT,
|
|
117
|
+
launcherOnly: NO_DRIFT,
|
|
118
|
+
agreeing: NO_DRIFT,
|
|
119
|
+
note: `the declared \`externalInventory\` could not be read as a populated schemaVersion:1 inventory document.`,
|
|
76
120
|
};
|
|
77
121
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
122
|
+
// Launcher's own inventory is read by its own strict reader. A missing one
|
|
123
|
+
// lists nothing; one that is present but cannot be read makes the
|
|
124
|
+
// comparison indeterminate, never a comparison against an empty list.
|
|
125
|
+
const launcherPath = `${directory}/${launcherInventoryRelPath}`;
|
|
126
|
+
const launcherBytes = host.readBytes(launcherPath);
|
|
127
|
+
let launcherIds: readonly string[] = [];
|
|
128
|
+
if (launcherBytes !== null) {
|
|
129
|
+
const launcher = validateInventoryDocument(launcherBytes, hubOwner === undefined ? {} : { hubOwner });
|
|
130
|
+
if (!launcher.valid) {
|
|
131
|
+
return {
|
|
132
|
+
status: "indeterminate",
|
|
133
|
+
externalOnly: NO_DRIFT,
|
|
134
|
+
launcherOnly: NO_DRIFT,
|
|
135
|
+
agreeing: NO_DRIFT,
|
|
136
|
+
note: `the hub's own inventory ${launcher.reason}, so it cannot be compared with the declared \`externalInventory\`.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
launcherIds = launcher.ids;
|
|
140
|
+
}
|
|
141
|
+
// One repository identity, as everywhere in Launcher (identity.ts): a bare id is the hub owner's, and case is ignored.
|
|
142
|
+
const agrees = (id: string, others: readonly string[]) => others.some((other) => sameRepository(id, other, hubOwner));
|
|
143
|
+
const externalIds = externalEntries.map((entry) => entry.id);
|
|
144
|
+
// Every position below is the id's own index in the array a caller can already read
|
|
145
|
+
// (externalEntries.index is `externalInventory`'s document's own `repositories` index --
|
|
146
|
+
// readForeignIds() pairs it with the id precisely because that array is filtered, so a
|
|
147
|
+
// count of ids kept so far would not match the file (#1179); launcherIds is never
|
|
148
|
+
// filtered, so its own array index already is that position) -- never the id, per
|
|
149
|
+
// InventoryDriftPositions' own doc comment.
|
|
150
|
+
const externalOnly = externalEntries.reduce<{ count: number; positions: string[] }>(
|
|
151
|
+
(acc, entry) => (agrees(entry.id, launcherIds) ? acc : { count: acc.count + 1, positions: [...acc.positions, `externalInventory[${entry.index}]`] }),
|
|
152
|
+
{ count: 0, positions: [] },
|
|
153
|
+
);
|
|
154
|
+
const launcherOnly = launcherIds.reduce<{ count: number; positions: string[] }>(
|
|
155
|
+
(acc, id, index) => (agrees(id, externalIds) ? acc : { count: acc.count + 1, positions: [...acc.positions, `repositories[${index}]`] }),
|
|
156
|
+
{ count: 0, positions: [] },
|
|
157
|
+
);
|
|
158
|
+
const agreeing = externalEntries.reduce<{ count: number; positions: string[] }>(
|
|
159
|
+
(acc, entry) => (agrees(entry.id, launcherIds) ? { count: acc.count + 1, positions: [...acc.positions, `externalInventory[${entry.index}]`] } : acc),
|
|
160
|
+
{ count: 0, positions: [] },
|
|
161
|
+
);
|
|
84
162
|
return { status: "reconciled", externalOnly, launcherOnly, agreeing };
|
|
85
163
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Writing the hub inventory from the repositories a founder chose on
|
|
2
|
+
// Advisor's repository-choice card (issue #1179), so a founder never
|
|
3
|
+
// hand-writes JSON. `launcher --repositories` is the command; this module
|
|
4
|
+
// decides what that command may write, and never writes anything itself.
|
|
5
|
+
//
|
|
6
|
+
// The chosen ids and the document built from them are validated against
|
|
7
|
+
// docs/contracts/repository-inventory.json (in the public repository;
|
|
8
|
+
// that exact path does not ship in this package, but this package's
|
|
9
|
+
// build packs and ships its own copy of the contract) through the shared
|
|
10
|
+
// contract checker (./inventory-contract.ts) -- the same check every later read of
|
|
11
|
+
// the stored inventory applies, so what this writes, Launcher reads back.
|
|
12
|
+
//
|
|
13
|
+
// An inventory already on disk that names a different set of repositories
|
|
14
|
+
// is never merged into or overwritten silently: the choice is refused, with
|
|
15
|
+
// the difference in counts and positions, unless the caller passes an
|
|
16
|
+
// explicit replace approval (`--replace-inventory`).
|
|
17
|
+
//
|
|
18
|
+
// Every message this module builds -- the refusal and the success line
|
|
19
|
+
// alike -- names a repository only by its position, never by its id: a
|
|
20
|
+
// stored inventory file, and the `--repositories` argument, are both
|
|
21
|
+
// document-shaped input an agent may relay verbatim, and a repository id is
|
|
22
|
+
// exactly the kind of short string a hostile entry could shape as
|
|
23
|
+
// prompt-injection text (see ./inventory-contract.ts's own header). The
|
|
24
|
+
// caller looks the reported position up in its own copy of the inventory
|
|
25
|
+
// file or its own `--repositories` argument to learn which repository it
|
|
26
|
+
// names.
|
|
27
|
+
|
|
28
|
+
import { inventoryKey } from "./identity.js";
|
|
29
|
+
import { readInventoryDocument, renderInventoryDocument, validateInventoryDocument, validateInventoryValue, type InventoryEntry } from "./inventory-contract.js";
|
|
30
|
+
import type { ChosenInventory } from "./types.js";
|
|
31
|
+
|
|
32
|
+
export type ChosenInventoryResolution =
|
|
33
|
+
| { readonly kind: "resolved"; readonly chosen: ChosenInventory }
|
|
34
|
+
| { readonly kind: "refuse"; readonly message: string };
|
|
35
|
+
|
|
36
|
+
/** The command a refusal names for an explicit replace approval. */
|
|
37
|
+
export const REPLACE_INVENTORY_FLAG = "--replace-inventory";
|
|
38
|
+
|
|
39
|
+
function repositories(count: number): string {
|
|
40
|
+
return `${count} ${count === 1 ? "repository" : "repositories"}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Added ids are named by their position in the `--repositories` argument, never by the id itself. */
|
|
44
|
+
function listAddedPositions(positions: readonly number[]): string {
|
|
45
|
+
return positions.length === 0 ? "" : ` (${positions.map((index) => `--repositories[${index}]`).join(", ")})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Removed ids are named by their position in an inventory's `repositories`
|
|
50
|
+
* array, never by the id itself. `label` distinguishes which file that
|
|
51
|
+
* position indexes into: `"stored inventory"` for the refusal, where the
|
|
52
|
+
* file on disk is still the one the positions were computed against, and
|
|
53
|
+
* `"replaced inventory"` for the success line after `--replace-inventory`
|
|
54
|
+
* writes -- by the time that line prints, the file on disk is already the
|
|
55
|
+
* new one, so reusing "stored inventory" there would point a reader at the
|
|
56
|
+
* wrong document (#1179).
|
|
57
|
+
*/
|
|
58
|
+
function listRemovedPositions(positions: readonly number[], label: "stored inventory" | "replaced inventory"): string {
|
|
59
|
+
return positions.length === 0 ? "" : ` (${positions.map((index) => `repositories[${index}] in the ${label}`).join(", ")})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decides what `launcher --repositories` writes into a hub whose stored
|
|
64
|
+
* inventory is `onDisk` -- the file's exact bytes, or `null` when there is
|
|
65
|
+
* none.
|
|
66
|
+
*
|
|
67
|
+
* Every comparison uses `inventoryKey(id, hubOwner)`: a bare id names a
|
|
68
|
+
* repository of the hub's own owner, and letter case is ignored.
|
|
69
|
+
*
|
|
70
|
+
* - The chosen ids must form a valid inventory on their own: at least one
|
|
71
|
+
* id, each one satisfying the contract's repository id rule, and no two
|
|
72
|
+
* naming the same repository. A refusal names positions only.
|
|
73
|
+
* - The stored inventory is read by the same strict reader, with the same
|
|
74
|
+
* owner: one that fails its contract, including one listing a repository
|
|
75
|
+
* twice (for example `app` and `<hubOwner>/app`), is replaced only with
|
|
76
|
+
* `replace`, and then with the chosen ids alone. It is never merged.
|
|
77
|
+
* - No inventory, or an empty one: the chosen repositories are written.
|
|
78
|
+
* - An inventory naming exactly the same repositories: nothing is written.
|
|
79
|
+
* - An inventory naming a different set: refused, reporting how many
|
|
80
|
+
* repositories each side has and the positions that would be added and
|
|
81
|
+
* removed (never the ids themselves), unless `replace` is true. A
|
|
82
|
+
* replacement keeps each kept repository's existing entry as it was (its
|
|
83
|
+
* `packages` included).
|
|
84
|
+
*
|
|
85
|
+
* The document to write is checked again by the same reader every later
|
|
86
|
+
* read uses, before it is returned.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveChosenInventory(
|
|
89
|
+
onDisk: Uint8Array | string | null,
|
|
90
|
+
chosenIds: readonly string[],
|
|
91
|
+
hubOwner: string,
|
|
92
|
+
replace: boolean,
|
|
93
|
+
): ChosenInventoryResolution {
|
|
94
|
+
if (chosenIds.length === 0) {
|
|
95
|
+
return { kind: "refuse", message: "--repositories must name at least one repository" };
|
|
96
|
+
}
|
|
97
|
+
const chosenCheck = validateInventoryValue({ schemaVersion: 1, repositories: chosenIds.map((id) => ({ id })) }, { hubOwner });
|
|
98
|
+
if (!chosenCheck.valid) return { kind: "refuse", message: `--repositories is not a valid inventory: ${chosenCheck.reason}` };
|
|
99
|
+
|
|
100
|
+
let entries: InventoryEntry[];
|
|
101
|
+
let added: readonly string[] = chosenIds;
|
|
102
|
+
let removed: readonly string[] = [];
|
|
103
|
+
let addedPositions: readonly number[] = chosenIds.map((_, index) => index);
|
|
104
|
+
let removedPositions: readonly number[] = [];
|
|
105
|
+
let replaced: Extract<ChosenInventory, { kind: "write" }>["replaced"] = "nothing";
|
|
106
|
+
let previousCount = 0;
|
|
107
|
+
if (onDisk === null) {
|
|
108
|
+
entries = chosenIds.map((id) => ({ id }));
|
|
109
|
+
} else {
|
|
110
|
+
const stored = readInventoryDocument(onDisk, { hubOwner });
|
|
111
|
+
if (!stored.valid) {
|
|
112
|
+
if (!replace) {
|
|
113
|
+
return {
|
|
114
|
+
kind: "refuse",
|
|
115
|
+
message: `the hub inventory ${stored.reason}; to replace it with the ${repositories(chosenIds.length)} chosen, run again with ${REPLACE_INVENTORY_FLAG}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
entries = chosenIds.map((id) => ({ id }));
|
|
119
|
+
replaced = "invalid";
|
|
120
|
+
} else {
|
|
121
|
+
previousCount = stored.ids.length;
|
|
122
|
+
// Keys are unique on both sides: the reader and the chosen-id check above both refuse two ids with one key.
|
|
123
|
+
const storedByKey = new Map(stored.entries.map((entry) => [inventoryKey(entry.id, hubOwner), entry] as const));
|
|
124
|
+
const chosenKeys = new Set(chosenIds.map((id) => inventoryKey(id, hubOwner)));
|
|
125
|
+
added = chosenIds.filter((id) => !storedByKey.has(inventoryKey(id, hubOwner)));
|
|
126
|
+
removed = stored.ids.filter((id) => !chosenKeys.has(inventoryKey(id, hubOwner)));
|
|
127
|
+
// Positions, not ids: `added` indexes into the --repositories argument the caller passed; `removed` indexes into the stored inventory's own `repositories` array.
|
|
128
|
+
addedPositions = added.map((id) => chosenIds.indexOf(id));
|
|
129
|
+
removedPositions = removed.map((id) => stored.ids.indexOf(id));
|
|
130
|
+
if (stored.ids.length > 0 && added.length === 0 && removed.length === 0) {
|
|
131
|
+
return { kind: "resolved", chosen: { kind: "unchanged", count: stored.ids.length } };
|
|
132
|
+
}
|
|
133
|
+
if (stored.ids.length > 0) {
|
|
134
|
+
if (!replace) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "refuse",
|
|
137
|
+
message:
|
|
138
|
+
`the hub inventory already lists ${repositories(stored.ids.length)} and the choice has ${chosenIds.length}: ` +
|
|
139
|
+
`${added.length} to add${listAddedPositions(addedPositions)}, ${removed.length} to remove${listRemovedPositions(removedPositions, "stored inventory")}. ` +
|
|
140
|
+
`Launcher never merges or overwrites an inventory silently; to replace it with the choice, run again with ${REPLACE_INVENTORY_FLAG}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
replaced = "differing";
|
|
144
|
+
}
|
|
145
|
+
entries = chosenIds.map((id) => storedByKey.get(inventoryKey(id, hubOwner)) ?? { id });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const document = renderInventoryDocument(entries);
|
|
150
|
+
const written = validateInventoryDocument(document, { hubOwner });
|
|
151
|
+
if (!written.valid) return { kind: "refuse", message: `the inventory to write ${written.reason}` };
|
|
152
|
+
return {
|
|
153
|
+
kind: "resolved",
|
|
154
|
+
chosen: { kind: "write", document, count: written.ids.length, previousCount, added, removed, addedPositions, removedPositions, replaced },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One line for the apply message saying what happened to the inventory. */
|
|
159
|
+
export function describeChosenInventory(chosen: ChosenInventory): string {
|
|
160
|
+
if (chosen.kind === "unchanged") {
|
|
161
|
+
return `inventory: unchanged -- it already lists the ${repositories(chosen.count)} you chose`;
|
|
162
|
+
}
|
|
163
|
+
const count = repositories(chosen.count);
|
|
164
|
+
if (chosen.replaced === "invalid") return `inventory: replaced one that failed its contract with the ${count} you chose`;
|
|
165
|
+
if (chosen.replaced === "differing") {
|
|
166
|
+
return (
|
|
167
|
+
`inventory: replaced ${chosen.previousCount} with the ${count} you chose -- ` +
|
|
168
|
+
`${chosen.added.length} added${listAddedPositions(chosen.addedPositions)}, ${chosen.removed.length} removed${listRemovedPositions(chosen.removedPositions, "replaced inventory")}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return `inventory: wrote the ${count} you chose`;
|
|
172
|
+
}
|