@clossys/launcher 0.3.0 → 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 +1328 -60
- package/contracts/conversation-contract.md +2 -1
- 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 -5
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +20 -16
- package/skill-catalogue/advisor/SKILL.md +100 -11
- package/skill-catalogue/architect/SKILL.md +2 -13
- package/skill-catalogue/bouncer/SKILL.md +2 -13
- package/skill-catalogue/builder/SKILL.md +2 -13
- package/skill-catalogue/butler/SKILL.md +2 -13
- package/skill-catalogue/controller/SKILL.md +2 -13
- package/skill-catalogue/customer/SKILL.md +4 -13
- package/skill-catalogue/designer/SKILL.md +9 -14
- package/skill-catalogue/giver/SKILL.md +2 -13
- package/skill-catalogue/influencer/SKILL.md +2 -13
- package/skill-catalogue/inspector/SKILL.md +2 -13
- package/skill-catalogue/integrator/SKILL.md +2 -13
- package/skill-catalogue/keeper/SKILL.md +2 -13
- package/skill-catalogue/launcher/SKILL.md +20 -16
- package/skill-catalogue/locksmith/SKILL.md +2 -13
- package/skill-catalogue/messenger/SKILL.md +2 -13
- package/skill-catalogue/observer/SKILL.md +2 -13
- package/skill-catalogue/publisher/SKILL.md +11 -17
- package/skill-catalogue/starter/SKILL.md +3 -13
- package/skill-catalogue/strategist/SKILL.md +14 -17
- package/skill-catalogue/writer/SKILL.md +7 -14
- 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/CHANGELOG.md +0 -131
package/dist/core.js
CHANGED
|
@@ -4,6 +4,9 @@ import { composeSkills, SKILLS_MANIFEST_REL } from "./skills.js";
|
|
|
4
4
|
import { parseSkillManifest, summarizeSkillsManifest } from "./manifest.js";
|
|
5
5
|
import { detectLinkedHosts, serializeHostRecord, HOSTS_REL } from "./hosts.js";
|
|
6
6
|
import { reportInventoryDrift } from "./inventory-adoption.js";
|
|
7
|
+
import { belongsToOwner, distinctOwners, sameOwner, sameRepository } from "./identity.js";
|
|
8
|
+
import { isValidInventoryId, readInventoryDocument, renderInventoryDocument, validateInventoryDocument } from "./inventory-contract.js";
|
|
9
|
+
import { describeChosenInventory, resolveChosenInventory } from "./inventory-choice.js";
|
|
7
10
|
export const DEFAULT_REPOSITORY_NAME = "workspace";
|
|
8
11
|
/** The one visible, per-repository Clossys folder (#1171). Every role's output lives under it. */
|
|
9
12
|
export const CLOSSYS_DIR_REL = "clossys";
|
|
@@ -17,7 +20,10 @@ export const LEGACY_STATE_DIR_REL = ".clossys";
|
|
|
17
20
|
export const LEGACY_WORKSPACE_MARKER_REL = join(LEGACY_STATE_DIR_REL, "workspace.json");
|
|
18
21
|
export const LEGACY_WORKSPACE_INVENTORY_REL = join(LEGACY_STATE_DIR_REL, "inventory.json");
|
|
19
22
|
export const ADVISOR_PACKAGE = "@clossys/advisor";
|
|
23
|
+
export const INTEGRATOR_PACKAGE = "@clossys/integrator";
|
|
20
24
|
export const LAUNCHER_PACKAGE = "@clossys/launcher";
|
|
25
|
+
/** The engines a hub pins: each exactly, once, in `devDependencies`, at its live registry version. */
|
|
26
|
+
export const HUB_ENGINE_PACKAGES = [ADVISOR_PACKAGE, INTEGRATOR_PACKAGE];
|
|
21
27
|
const DEPENDENCY_BUCKETS = [
|
|
22
28
|
"dependencies",
|
|
23
29
|
"devDependencies",
|
|
@@ -38,6 +44,29 @@ export const CONSUMER_AGENTS_MD = `# Account workspace
|
|
|
38
44
|
|
|
39
45
|
This folder is the account hub for Foundry packages.
|
|
40
46
|
|
|
47
|
+
After \`npx @clossys/launcher\`, the \`@clossys-*\` team is composed in this
|
|
48
|
+
hub. Talk with \`@clossys-advisor\` and \`@clossys-<package>\` here. A launcher
|
|
49
|
+
run writes nothing into a product repository; once a repository is staffed
|
|
50
|
+
in an approved plan, it gets \`@clossys-advisor\` and the voices of the roles
|
|
51
|
+
staffed there, with that plan's setup pull request. A missing \`@\` mention
|
|
52
|
+
is a bug only here in the hub; in a product repository, a role that is not
|
|
53
|
+
staffed there is expected to be absent. \`@clossys-advisor\` is the hiring
|
|
54
|
+
check.
|
|
55
|
+
|
|
56
|
+
Run \`npx @clossys/launcher\` again for hub health and to refresh the voices
|
|
57
|
+
in this hub, not as how you talk to packages.
|
|
58
|
+
|
|
59
|
+
The person in this folder is a founder, not an engineer. Speak like a
|
|
60
|
+
person. Do not dump machine identifiers, JSON, hashes, or grant fields
|
|
61
|
+
unless they ask.
|
|
62
|
+
|
|
63
|
+
Advisor is read-only until the sponsor approves a next action.
|
|
64
|
+
`;
|
|
65
|
+
/** The hub guidance written while a launcher run still composed the team into checkouts beside the hub; resume refreshes it. */
|
|
66
|
+
export const SIBLING_COMPOSING_CONSUMER_AGENTS_MD = `# Account workspace
|
|
67
|
+
|
|
68
|
+
This folder is the account hub for Foundry packages.
|
|
69
|
+
|
|
41
70
|
After \`npx @clossys/launcher\`, the same \`@clossys-*\` team is composed in
|
|
42
71
|
every inventoried checkout beside this hub. Talk with \`@clossys-advisor\` and
|
|
43
72
|
\`@clossys-<package>\` here or in a product repository. A missing \`@\` mention
|
|
@@ -52,14 +81,6 @@ unless they ask.
|
|
|
52
81
|
|
|
53
82
|
Advisor is read-only until the sponsor approves a next action.
|
|
54
83
|
`;
|
|
55
|
-
/** Canned guidance for inventoried product checkouts (not the hub). */
|
|
56
|
-
export const SISTER_CONSUMER_AGENTS_MD = `# Product repository
|
|
57
|
-
|
|
58
|
-
This repository is part of the same account engagement. The same
|
|
59
|
-
\`@clossys-<package>\` team is here for intro and questions;
|
|
60
|
-
\`@clossys-advisor\` decides hiring and compatibility. This folder is not the
|
|
61
|
-
hub — engines are hired per repository, not dumped here.
|
|
62
|
-
`;
|
|
63
84
|
/** Previous generate-time guidance; used to refresh stale hub AGENTS.md on resume. */
|
|
64
85
|
export const LEGACY_CONSUMER_AGENTS_MD = `# Account workspace
|
|
65
86
|
|
|
@@ -174,7 +195,7 @@ export function isHubDocument(value) {
|
|
|
174
195
|
const parsed = value.repository.includes("/")
|
|
175
196
|
? { owner: value.repository.split("/")[0], repository: value.repository.split("/")[1] }
|
|
176
197
|
: null;
|
|
177
|
-
if (!parsed?.owner || !parsed.repository || parsed.owner
|
|
198
|
+
if (!parsed?.owner || !parsed.repository || !sameOwner(parsed.owner, value.owner) || !REPO.test(parsed.repository))
|
|
178
199
|
return false;
|
|
179
200
|
return true;
|
|
180
201
|
}
|
|
@@ -195,8 +216,9 @@ function readHubAt(host, path) {
|
|
|
195
216
|
* migration (#1171). `clean`: only the current path has a marker. `legacy`:
|
|
196
217
|
* only the old path does; resume migrates it (see `migrateLegacyHubState`).
|
|
197
218
|
* `indeterminate`: both paths carry a parseable marker; launcher never
|
|
198
|
-
* merges them silently, so `planWorkspace` refuses instead
|
|
199
|
-
*
|
|
219
|
+
* merges them silently, so `planWorkspace` refuses instead (and, for a hub
|
|
220
|
+
* cloned from an empty directory, `classifyClonedHub` at apply time).
|
|
221
|
+
* `none`: neither path has one.
|
|
200
222
|
*/
|
|
201
223
|
function locateHub(host, directory) {
|
|
202
224
|
const current = readHubAt(host, join(directory, WORKSPACE_MARKER_REL));
|
|
@@ -212,21 +234,26 @@ function locateHub(host, directory) {
|
|
|
212
234
|
function readHub(host, directory) {
|
|
213
235
|
return locateHub(host, directory).document;
|
|
214
236
|
}
|
|
215
|
-
/**
|
|
216
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The inventory document's shape lives in docs/contracts/repository-inventory.json
|
|
239
|
+
* (in the public repository; that exact path does not ship in this
|
|
240
|
+
* package, but this package's build packs and ships its own copy of the
|
|
241
|
+
* contract) and is checked, on every read and write, by
|
|
242
|
+
* `validateInventoryDocument()` through the shared contract checker
|
|
243
|
+
* (./inventory-contract.ts, #1334, #1179).
|
|
244
|
+
*/
|
|
245
|
+
export { validateInventoryDocument } from "./inventory-contract.js";
|
|
246
|
+
/**
|
|
247
|
+
* Classifies a generated hub inventory (packed template skeleton/clossys/.state/inventory.json; the generated path does not ship) without inventing repositories. Malformed input is "invalid", never silently folded into "empty" (#1334).
|
|
248
|
+
* Pass the file's exact bytes (`WorkspaceHost.readBytes()`), and the hub's owner when it is known, so a bare id and `<owner>/<id>` count as one repository (#1179).
|
|
249
|
+
*/
|
|
250
|
+
export function inspectInventory(raw, hubOwner) {
|
|
217
251
|
if (raw === null)
|
|
218
252
|
return { status: "missing", count: 0 };
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
const count = parsed.repositories.length;
|
|
225
|
-
return { status: count > 0 ? "populated" : "empty", count };
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return { status: "empty", count: 0 };
|
|
229
|
-
}
|
|
253
|
+
const validated = validateInventoryDocument(raw, hubOwner === undefined ? {} : { hubOwner });
|
|
254
|
+
if (!validated.valid)
|
|
255
|
+
return { status: "invalid", count: 0, reason: validated.reason };
|
|
256
|
+
return { status: validated.ids.length > 0 ? "populated" : "empty", count: validated.ids.length };
|
|
230
257
|
}
|
|
231
258
|
function looksLikeFoundry(host, directory) {
|
|
232
259
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
@@ -263,19 +290,33 @@ function commandAvailable(host, command) {
|
|
|
263
290
|
export function hasAdvisorPin(manifest) {
|
|
264
291
|
if (!isRecord(manifest))
|
|
265
292
|
return false;
|
|
266
|
-
return DEPENDENCY_BUCKETS.some((bucket) =>
|
|
293
|
+
return DEPENDENCY_BUCKETS.some((bucket) => enginePinsIn(manifest[bucket], new Set())[ADVISOR_PACKAGE] !== undefined);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The one place Launcher learns a package's live version: the public npm
|
|
297
|
+
* registry (`npm view <name> version`). A failed or unparseable read is
|
|
298
|
+
* `undefined`, never a guess.
|
|
299
|
+
*/
|
|
300
|
+
function readRegistryVersion(host, name) {
|
|
301
|
+
const viewed = host.run("npm", ["view", name, "version"]);
|
|
302
|
+
const version = viewed.stdout.trim();
|
|
303
|
+
return viewed.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : undefined;
|
|
267
304
|
}
|
|
268
|
-
/** Collects GitHub owner, cwd shape, default-hub presence, and the public Advisor pin. */
|
|
269
305
|
/**
|
|
270
306
|
* Reads the public `@clossys/launcher` registry version, used only to grade
|
|
271
307
|
* catalogue-sourced skill staleness in the health report (#1183). A missing
|
|
272
308
|
* or unparseable read leaves staleness ungraded rather than refusing.
|
|
273
309
|
*/
|
|
274
310
|
export function readLiveLauncherVersion(host) {
|
|
275
|
-
|
|
276
|
-
const version = viewed.stdout.trim();
|
|
277
|
-
return viewed.status === 0 && /^\d+\.\d+\.\d+$/.test(version) ? version : undefined;
|
|
311
|
+
return readRegistryVersion(host, LAUNCHER_PACKAGE);
|
|
278
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Collects GitHub owner, cwd shape, default-hub presence, and the public
|
|
315
|
+
* Advisor and Integrator versions. Default-hub presence means only that
|
|
316
|
+
* `{owner}/workspace` exists on GitHub; its contents (marker, legacy marker,
|
|
317
|
+
* or none) are not observed here but classified after the clone, at apply
|
|
318
|
+
* time (#1585).
|
|
319
|
+
*/
|
|
279
320
|
export function observeWorkspace(host) {
|
|
280
321
|
const cwd = host.cwd;
|
|
281
322
|
const ghAvailable = commandAvailable(host, "gh");
|
|
@@ -292,39 +333,39 @@ export function observeWorkspace(host) {
|
|
|
292
333
|
githubRepository = parsed.repository;
|
|
293
334
|
}
|
|
294
335
|
}
|
|
295
|
-
|
|
336
|
+
// Owners are compared as GitHub compares them (identity.ts): two spellings of one account are one candidate.
|
|
337
|
+
const seenOwners = [];
|
|
296
338
|
const envOwnerRaw = host.env.CLOSSYS_OWNER?.trim();
|
|
297
339
|
const envOwner = envOwnerRaw && OWNER.test(envOwnerRaw) ? envOwnerRaw : undefined;
|
|
298
340
|
if (ghAvailable) {
|
|
299
341
|
const user = host.run("gh", ["api", "user", "--jq", ".login"]);
|
|
300
342
|
const login = user.stdout.trim();
|
|
301
343
|
if (user.status === 0 && OWNER.test(login))
|
|
302
|
-
|
|
344
|
+
seenOwners.push(login);
|
|
303
345
|
for (const org of stdoutLines(host.run("gh", ["org", "list"]))) {
|
|
304
346
|
if (OWNER.test(org))
|
|
305
|
-
|
|
347
|
+
seenOwners.push(org);
|
|
306
348
|
}
|
|
307
349
|
}
|
|
308
350
|
if (githubOwner)
|
|
309
|
-
|
|
351
|
+
seenOwners.push(githubOwner);
|
|
352
|
+
const candidates = new Set(distinctOwners(seenOwners));
|
|
310
353
|
let remoteDefaultHub;
|
|
311
|
-
let advisorVersion;
|
|
312
354
|
const ownerGuess = envOwner ?? (candidates.size === 1 ? [...candidates][0] : githubOwner);
|
|
313
355
|
if (ghAvailable && ownerGuess) {
|
|
314
356
|
const viewed = host.run("gh", ["repo", "view", `${ownerGuess}/${DEFAULT_REPOSITORY_NAME}`, "--json", "name"]);
|
|
315
357
|
if (viewed.status === 0)
|
|
316
358
|
remoteDefaultHub = { owner: ownerGuess, repository: DEFAULT_REPOSITORY_NAME };
|
|
317
359
|
}
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
if (viewedAdvisor.status === 0 && /^\d+\.\d+\.\d+$/.test(version))
|
|
321
|
-
advisorVersion = version;
|
|
360
|
+
const advisorVersion = readRegistryVersion(host, ADVISOR_PACKAGE);
|
|
361
|
+
const integratorVersion = readRegistryVersion(host, INTEGRATOR_PACKAGE);
|
|
322
362
|
const hubLocation = locateHub(host, cwd);
|
|
323
363
|
// While only the legacy `.clossys/` marker exists, its sibling inventory is
|
|
324
364
|
// the one resume will migrate; read from there so planning sees it too.
|
|
325
|
-
const
|
|
326
|
-
? host.
|
|
327
|
-
: host.
|
|
365
|
+
const inventoryBytes = hubLocation.migration === "legacy"
|
|
366
|
+
? host.readBytes(join(cwd, LEGACY_WORKSPACE_INVENTORY_REL))
|
|
367
|
+
: host.readBytes(join(cwd, WORKSPACE_INVENTORY_REL));
|
|
368
|
+
const inventoryOwner = hubLocation.document?.owner ?? githubOwner;
|
|
328
369
|
const cwdObservation = {
|
|
329
370
|
absolutePath: cwd,
|
|
330
371
|
empty: isEffectivelyEmpty(entries),
|
|
@@ -334,7 +375,7 @@ export function observeWorkspace(host) {
|
|
|
334
375
|
...(hubLocation.document === undefined ? {} : { hub: hubLocation.document }),
|
|
335
376
|
...(hubLocation.migration === "none" ? {} : { hubMigration: hubLocation.migration }),
|
|
336
377
|
looksLikeFoundry: looksLikeFoundry(host, cwd),
|
|
337
|
-
inventory: inspectInventory(
|
|
378
|
+
inventory: inspectInventory(inventoryBytes, inventoryOwner),
|
|
338
379
|
};
|
|
339
380
|
return {
|
|
340
381
|
cwd: cwdObservation,
|
|
@@ -342,6 +383,7 @@ export function observeWorkspace(host) {
|
|
|
342
383
|
...(envOwner === undefined ? {} : { envOwner }),
|
|
343
384
|
...(remoteDefaultHub === undefined ? {} : { remoteDefaultHub }),
|
|
344
385
|
...(advisorVersion === undefined ? {} : { advisorVersion }),
|
|
386
|
+
...(integratorVersion === undefined ? {} : { integratorVersion }),
|
|
345
387
|
ghAvailable,
|
|
346
388
|
gitAvailable,
|
|
347
389
|
};
|
|
@@ -368,23 +410,37 @@ function resolveOwner(observation, host) {
|
|
|
368
410
|
* Decides create, resume, or adopt from a cwd observation.
|
|
369
411
|
* Appointing means: run this from the GitHub repository that should own the hub.
|
|
370
412
|
*/
|
|
371
|
-
|
|
372
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Reads repository ids from a `schemaVersion: 1` inventory document, routed
|
|
415
|
+
* through `validateInventoryDocument` -- the same schema check `--inventory`
|
|
416
|
+
* and `inspectInventory` apply, so a caller here can never end up trusting a
|
|
417
|
+
* document neither of those would have accepted (#1334). Throws, naming the
|
|
418
|
+
* offending field, on anything present but invalid; a missing file is `[]`,
|
|
419
|
+
* not a throw -- an absent inventory is a fact about the hub, not a
|
|
420
|
+
* malformed one. The file is read as bytes; `hubOwner`, when given, makes a
|
|
421
|
+
* bare id and `<hubOwner>/<id>` one repository, so a document listing both
|
|
422
|
+
* is refused.
|
|
423
|
+
*/
|
|
424
|
+
export function readInventoryRepositories(host, source, label, hubOwner) {
|
|
425
|
+
const raw = host.readBytes(source);
|
|
373
426
|
if (raw === null)
|
|
374
427
|
return [];
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
428
|
+
const validated = validateInventoryDocument(raw, hubOwner === undefined ? {} : { hubOwner });
|
|
429
|
+
if (!validated.valid)
|
|
430
|
+
throw new Error(`${label} ${validated.reason}`);
|
|
431
|
+
return validated.ids;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* How a founder gives Launcher the repositories a hub covers (#1179): they
|
|
435
|
+
* choose them on Advisor's repository-choice card, and `--repositories`
|
|
436
|
+
* writes the inventory. Named by every refusal that needs an inventory,
|
|
437
|
+
* instead of `--inventory <path>`, which still works but asks for a
|
|
438
|
+
* document a founder will not write.
|
|
439
|
+
*/
|
|
440
|
+
function chooseRepositoriesHint(extraFlag = "") {
|
|
441
|
+
return ("choose the repositories this hub covers on Advisor's repository card " +
|
|
442
|
+
"(`npx -p @clossys/advisor advisor-repository-card`), built from the repositories GitHub lists for your sign-in, then run " +
|
|
443
|
+
`\`launcher --repositories <owner/name>[,<owner/name>...]${extraFlag}\`, and Launcher writes the inventory for you`);
|
|
388
444
|
}
|
|
389
445
|
/**
|
|
390
446
|
* Resolves which repositories the adopt plan inventories. When the on-disk hub
|
|
@@ -398,35 +454,92 @@ function resolveAdoptInventory(host, cwd, inventoryPath) {
|
|
|
398
454
|
const trimmed = inventoryPath?.trim();
|
|
399
455
|
const onDiskPopulated = cwd.inventory?.status === "populated";
|
|
400
456
|
if (!onDiskPopulated && !trimmed) {
|
|
401
|
-
|
|
457
|
+
if (cwd.inventory?.status === "invalid") {
|
|
458
|
+
return refuse("violated", `the on-disk hub inventory ${cwd.inventory.reason ?? "does not conform to the inventory contract"} -- to replace it, ${chooseRepositoriesHint(" --replace-inventory")}`);
|
|
459
|
+
}
|
|
460
|
+
return refuse("violated", `appointing needs the repositories this hub covers, and it has no inventory yet: ${chooseRepositoriesHint()}`);
|
|
402
461
|
}
|
|
403
462
|
if (!trimmed)
|
|
404
463
|
return {};
|
|
405
464
|
const resolved = resolve(cwd.absolutePath, trimmed);
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
return refuse("violated",
|
|
465
|
+
const importedRaw = host.readBytes(resolved);
|
|
466
|
+
if (importedRaw === null) {
|
|
467
|
+
return refuse("violated", `--inventory does not point at a readable file: ${resolved}`);
|
|
468
|
+
}
|
|
469
|
+
const owner = cwd.githubOwner === undefined ? {} : { hubOwner: cwd.githubOwner };
|
|
470
|
+
const imported = readInventoryDocument(importedRaw, owner);
|
|
471
|
+
if (!imported.valid) {
|
|
472
|
+
return refuse("violated", `--inventory at ${resolved} ${imported.reason}`);
|
|
473
|
+
}
|
|
474
|
+
if (imported.ids.length === 0) {
|
|
475
|
+
return refuse("violated", `--inventory at ${resolved} must be a populated inventory document (nonempty repositories)`);
|
|
409
476
|
}
|
|
410
|
-
if (!onDiskPopulated)
|
|
411
|
-
return {
|
|
477
|
+
if (!onDiskPopulated) {
|
|
478
|
+
return {
|
|
479
|
+
inventorySource: resolved,
|
|
480
|
+
...(cwd.inventory?.status === "invalid" ? { replacesInvalidInventory: true } : {}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const onDiskRaw = host.readBytes(join(cwd.absolutePath, WORKSPACE_INVENTORY_REL));
|
|
484
|
+
const onDisk = onDiskRaw === null ? undefined : readInventoryDocument(onDiskRaw, owner);
|
|
485
|
+
if (onDisk !== undefined && !onDisk.valid) {
|
|
486
|
+
return refuse("violated", `the on-disk hub inventory ${onDisk.reason}`);
|
|
487
|
+
}
|
|
488
|
+
const onDiskEntries = onDisk !== undefined && onDisk.valid ? onDisk.entries : [];
|
|
489
|
+
// Merge whole entries, not ids: an entry's `packages` travels with it.
|
|
490
|
+
// Entries are keyed by Launcher's one repository identity (identity.ts),
|
|
491
|
+
// the same rule validateInventoryDocument() applies to ids, so the merge
|
|
492
|
+
// can never write a document the validator then refuses; the first
|
|
493
|
+
// occurrence -- the on-disk spelling and entry -- wins (#1334, #1179).
|
|
412
494
|
const merged = [];
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
seen.add(id);
|
|
417
|
-
merged.push(id);
|
|
418
|
-
}
|
|
495
|
+
for (const entry of [...onDiskEntries, ...imported.entries]) {
|
|
496
|
+
if (!merged.some((kept) => sameRepository(kept.id, entry.id, cwd.githubOwner)))
|
|
497
|
+
merged.push(entry);
|
|
419
498
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
499
|
+
return {
|
|
500
|
+
mergedInventoryIds: merged.map((entry) => entry.id),
|
|
501
|
+
mergedInventoryRepositories: merged,
|
|
502
|
+
mergedInventoryDocument: renderInventoryDocument(merged),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Resolves `--repositories` against the inventory stored in `directory`, or
|
|
507
|
+
* returns a refusal. See `resolveChosenInventory()`.
|
|
508
|
+
*/
|
|
509
|
+
function resolveChosenRepositories(host, directory, owner, repositories, replaceInventory) {
|
|
510
|
+
const resolution = resolveChosenInventory(host.readBytes(join(directory, WORKSPACE_INVENTORY_REL)), repositories, owner, replaceInventory);
|
|
511
|
+
if (resolution.kind === "refuse")
|
|
512
|
+
return refuse("violated", resolution.message);
|
|
513
|
+
return { chosenInventory: resolution.chosen };
|
|
514
|
+
}
|
|
515
|
+
/** The hub engine versions a plan carries, only those the observation read. */
|
|
516
|
+
function observedEngineVersions(observation) {
|
|
517
|
+
return {
|
|
518
|
+
...(observation.advisorVersion === undefined ? {} : { advisorVersion: observation.advisorVersion }),
|
|
519
|
+
...(observation.integratorVersion === undefined ? {} : { integratorVersion: observation.integratorVersion }),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Create and appoint pin both hub engines, so each needs its live version:
|
|
524
|
+
* the refusal names the first one the registry did not answer for.
|
|
525
|
+
*/
|
|
526
|
+
function requireEngineVersions(observation) {
|
|
527
|
+
if (!observation.advisorVersion) {
|
|
528
|
+
return refuse("indeterminate", `cannot read a public ${ADVISOR_PACKAGE} version from the npm registry`);
|
|
529
|
+
}
|
|
530
|
+
if (!observation.integratorVersion) {
|
|
531
|
+
return refuse("indeterminate", `cannot read a public ${INTEGRATOR_PACKAGE} version from the npm registry`);
|
|
425
532
|
}
|
|
426
|
-
return {
|
|
533
|
+
return { advisorVersion: observation.advisorVersion, integratorVersion: observation.integratorVersion };
|
|
427
534
|
}
|
|
428
535
|
export function planWorkspace(observation, host, options = {}) {
|
|
429
536
|
const { cwd } = observation;
|
|
537
|
+
if (options.repositories !== undefined && options.inventoryPath !== undefined) {
|
|
538
|
+
return refuse("violated", "--repositories and --inventory each supply the whole inventory; use one, not both");
|
|
539
|
+
}
|
|
540
|
+
if (options.replaceInventory === true && options.repositories === undefined) {
|
|
541
|
+
return refuse("violated", "--replace-inventory approves replacing the inventory with --repositories, and means nothing without it");
|
|
542
|
+
}
|
|
430
543
|
if (cwd.hubMigration === "indeterminate") {
|
|
431
544
|
return refuse("indeterminate", `both ${WORKSPACE_MARKER_REL} and the legacy ${LEGACY_WORKSPACE_MARKER_REL} are present; launcher never merges them silently -- remove one before resuming`);
|
|
432
545
|
}
|
|
@@ -436,26 +549,50 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
436
549
|
cwd.hub === undefined &&
|
|
437
550
|
cwd.git &&
|
|
438
551
|
cwd.githubOwner !== undefined &&
|
|
439
|
-
cwd.githubOwner
|
|
552
|
+
!sameOwner(cwd.githubOwner, envOwner)) {
|
|
440
553
|
return refuse("violated", `CLOSSYS_OWNER is "${envOwner}" but the github.com origin here is owned by "${cwd.githubOwner}"; refusing to appoint a marker for the wrong account`);
|
|
441
554
|
}
|
|
442
555
|
if (cwd.looksLikeFoundry) {
|
|
443
556
|
return refuse("violated", "refusing to scaffold a hub inside the Foundry supplier tree; run from the GitHub repository you want to appoint, or from an empty directory");
|
|
444
557
|
}
|
|
445
558
|
if (cwd.hub) {
|
|
559
|
+
let chosen;
|
|
560
|
+
if (options.repositories !== undefined) {
|
|
561
|
+
if (cwd.hubMigration === "legacy") {
|
|
562
|
+
return refuse("violated", "this hub's state is still in the legacy .clossys/ folder; run launcher once without --repositories to move it to clossys/.state/, then choose the repositories again");
|
|
563
|
+
}
|
|
564
|
+
const resolved = resolveChosenRepositories(host, cwd.absolutePath, cwd.hub.owner, options.repositories, options.replaceInventory === true);
|
|
565
|
+
if ("action" in resolved)
|
|
566
|
+
return resolved;
|
|
567
|
+
chosen = resolved;
|
|
568
|
+
}
|
|
446
569
|
return {
|
|
447
570
|
action: "resume",
|
|
448
571
|
owner: cwd.hub.owner,
|
|
449
572
|
repository: repoNameFromSlug(cwd.hub.repository, DEFAULT_REPOSITORY_NAME),
|
|
450
573
|
directory: cwd.absolutePath,
|
|
451
574
|
clone: false,
|
|
452
|
-
...(observation
|
|
575
|
+
...observedEngineVersions(observation),
|
|
453
576
|
...(cwd.hubMigration === "legacy" ? { migrateFrom: "legacy" } : {}),
|
|
577
|
+
...(chosen ?? {}),
|
|
454
578
|
};
|
|
455
579
|
}
|
|
456
580
|
if (cwd.git && cwd.githubOwner && cwd.githubRepository) {
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
const engines = requireEngineVersions(observation);
|
|
582
|
+
if ("action" in engines)
|
|
583
|
+
return engines;
|
|
584
|
+
if (options.repositories !== undefined) {
|
|
585
|
+
const resolved = resolveChosenRepositories(host, cwd.absolutePath, cwd.githubOwner, options.repositories, options.replaceInventory === true);
|
|
586
|
+
if ("action" in resolved)
|
|
587
|
+
return resolved;
|
|
588
|
+
return {
|
|
589
|
+
action: "adopt",
|
|
590
|
+
owner: cwd.githubOwner,
|
|
591
|
+
repository: cwd.githubRepository,
|
|
592
|
+
directory: cwd.absolutePath,
|
|
593
|
+
...engines,
|
|
594
|
+
chosenInventory: resolved.chosenInventory,
|
|
595
|
+
};
|
|
459
596
|
}
|
|
460
597
|
const imported = resolveAdoptInventory(host, cwd, options.inventoryPath);
|
|
461
598
|
if ("action" in imported)
|
|
@@ -465,9 +602,12 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
465
602
|
owner: cwd.githubOwner,
|
|
466
603
|
repository: cwd.githubRepository,
|
|
467
604
|
directory: cwd.absolutePath,
|
|
468
|
-
|
|
605
|
+
...engines,
|
|
469
606
|
...(imported.inventorySource === undefined ? {} : { inventorySource: imported.inventorySource }),
|
|
470
607
|
...(imported.mergedInventoryIds === undefined ? {} : { mergedInventoryIds: imported.mergedInventoryIds }),
|
|
608
|
+
...(imported.mergedInventoryRepositories === undefined ? {} : { mergedInventoryRepositories: imported.mergedInventoryRepositories }),
|
|
609
|
+
...(imported.mergedInventoryDocument === undefined ? {} : { mergedInventoryDocument: imported.mergedInventoryDocument }),
|
|
610
|
+
...(imported.replacesInvalidInventory === undefined ? {} : { replacesInvalidInventory: imported.replacesInvalidInventory }),
|
|
471
611
|
};
|
|
472
612
|
}
|
|
473
613
|
if (cwd.git) {
|
|
@@ -476,31 +616,37 @@ export function planWorkspace(observation, host, options = {}) {
|
|
|
476
616
|
if (!cwd.empty) {
|
|
477
617
|
return refuse("violated", "the current directory is not empty and is not a GitHub repository; run from the repo you want to appoint, or from an empty directory");
|
|
478
618
|
}
|
|
619
|
+
if (options.repositories !== undefined) {
|
|
620
|
+
return refuse("violated", "--repositories writes the inventory of a hub checkout, and this directory is empty; run launcher here first to create or clone the hub, then choose its repositories from inside it");
|
|
621
|
+
}
|
|
479
622
|
const ownerResult = resolveOwner(observation, host);
|
|
480
623
|
if ("action" in ownerResult)
|
|
481
624
|
return ownerResult;
|
|
482
|
-
if (observation.remoteDefaultHub && observation.remoteDefaultHub.owner
|
|
625
|
+
if (observation.remoteDefaultHub && sameOwner(observation.remoteDefaultHub.owner, ownerResult.owner)) {
|
|
626
|
+
// A clone plan: whether `{owner}/workspace` is already a hub (marked, legacy-marked) or an existing
|
|
627
|
+
// repository to appoint (unmarked) is not known until it is cloned, so applyWorkspacePlan classifies
|
|
628
|
+
// it then (classifyClonedHub, #1585) and may take the adopt path instead of resuming.
|
|
483
629
|
return {
|
|
484
630
|
action: "resume",
|
|
485
631
|
owner: observation.remoteDefaultHub.owner,
|
|
486
632
|
repository: observation.remoteDefaultHub.repository,
|
|
487
633
|
directory: cwd.absolutePath,
|
|
488
634
|
clone: true,
|
|
489
|
-
...(observation
|
|
635
|
+
...observedEngineVersions(observation),
|
|
490
636
|
};
|
|
491
637
|
}
|
|
492
638
|
if (!observation.ghAvailable) {
|
|
493
639
|
return refuse("indeterminate", "`gh` is required to create a GitHub repository for a new hub");
|
|
494
640
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
641
|
+
const engines = requireEngineVersions(observation);
|
|
642
|
+
if ("action" in engines)
|
|
643
|
+
return engines;
|
|
498
644
|
return {
|
|
499
645
|
action: "create",
|
|
500
646
|
owner: ownerResult.owner,
|
|
501
647
|
repository: DEFAULT_REPOSITORY_NAME,
|
|
502
648
|
directory: cwd.absolutePath,
|
|
503
|
-
|
|
649
|
+
...engines,
|
|
504
650
|
};
|
|
505
651
|
}
|
|
506
652
|
function containedPath(root, relativePath) {
|
|
@@ -515,76 +661,243 @@ function substitute(contents, plan) {
|
|
|
515
661
|
return contents
|
|
516
662
|
.replaceAll("__OWNER__", plan.owner)
|
|
517
663
|
.replaceAll("__REPOSITORY_NAME__", plan.repository)
|
|
518
|
-
.replaceAll("__ADVISOR_VERSION__", plan.advisorVersion ?? "0.0.0")
|
|
664
|
+
.replaceAll("__ADVISOR_VERSION__", plan.advisorVersion ?? "0.0.0")
|
|
665
|
+
.replaceAll("__INTEGRATOR_VERSION__", plan.integratorVersion ?? "0.0.0");
|
|
519
666
|
}
|
|
520
667
|
function writeSkeletonFile(host, directory, relativePath, contents) {
|
|
521
668
|
const target = containedPath(directory, relativePath);
|
|
522
669
|
host.mkdirp(dirname(target));
|
|
523
670
|
host.writeText(target, contents);
|
|
524
671
|
}
|
|
672
|
+
/** Writes exact bytes, for a document copied rather than composed, so nothing is decoded and re-encoded on the way. */
|
|
673
|
+
function writeSkeletonBytes(host, directory, relativePath, contents) {
|
|
674
|
+
const target = containedPath(directory, relativePath);
|
|
675
|
+
host.mkdirp(dirname(target));
|
|
676
|
+
host.writeBytes(target, contents);
|
|
677
|
+
}
|
|
678
|
+
function withFinalNewline(bytes) {
|
|
679
|
+
if (bytes.length > 0 && bytes[bytes.length - 1] === 0x0a)
|
|
680
|
+
return bytes;
|
|
681
|
+
const out = new Uint8Array(bytes.length + 1);
|
|
682
|
+
out.set(bytes);
|
|
683
|
+
out[bytes.length] = 0x0a;
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
525
686
|
function pinString(value) {
|
|
526
687
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
527
688
|
}
|
|
528
|
-
|
|
689
|
+
/**
|
|
690
|
+
* The hub engine pins in one dependency bucket, by engine. Every other
|
|
691
|
+
* `@clossys/*` name found there is added to `extra`.
|
|
692
|
+
*/
|
|
693
|
+
function enginePinsIn(bucket, extra) {
|
|
694
|
+
const pins = {};
|
|
529
695
|
if (!isRecord(bucket))
|
|
530
|
-
return
|
|
531
|
-
let advisor;
|
|
696
|
+
return pins;
|
|
532
697
|
for (const [name, version] of Object.entries(bucket)) {
|
|
533
|
-
|
|
534
|
-
|
|
698
|
+
const engine = HUB_ENGINE_PACKAGES.find((candidate) => candidate === name);
|
|
699
|
+
if (engine !== undefined) {
|
|
700
|
+
const pinned = pinString(version);
|
|
701
|
+
if (pinned !== undefined)
|
|
702
|
+
pins[engine] = pinned;
|
|
535
703
|
continue;
|
|
536
704
|
}
|
|
537
705
|
if (name.startsWith("@clossys/"))
|
|
538
706
|
extra.add(name);
|
|
539
707
|
}
|
|
540
|
-
return
|
|
708
|
+
return pins;
|
|
709
|
+
}
|
|
710
|
+
function engineVersionEntries(versions) {
|
|
711
|
+
const entries = [];
|
|
712
|
+
if (versions.advisorVersion !== undefined)
|
|
713
|
+
entries.push([ADVISOR_PACKAGE, versions.advisorVersion]);
|
|
714
|
+
if (versions.integratorVersion !== undefined)
|
|
715
|
+
entries.push([INTEGRATOR_PACKAGE, versions.integratorVersion]);
|
|
716
|
+
return entries;
|
|
717
|
+
}
|
|
718
|
+
/** A `package.json` body parsed to a JSON object, or `undefined` when it is unparseable, an array, or a primitive. */
|
|
719
|
+
function parseManifestObject(raw) {
|
|
720
|
+
try {
|
|
721
|
+
const parsed = JSON.parse(raw);
|
|
722
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The manifest refusals of `mergeHubEnginePins` in "appoint" mode, raised by
|
|
730
|
+
* `adoptHubFiles` before its first write so a refusal leaves the checkout
|
|
731
|
+
* untouched: a `package.json` that is a symbolic link (live or dangling, never
|
|
732
|
+
* resolved, so the write cannot land outside the checkout), a present one that
|
|
733
|
+
* is unreadable or not a JSON object, or a missing one with no skeleton
|
|
734
|
+
* manifest to write in its place.
|
|
735
|
+
*/
|
|
736
|
+
function assertManifestAppointable(host, directory, skeletonRoot) {
|
|
737
|
+
const manifestPath = join(directory, "package.json");
|
|
738
|
+
if (host.isSymlink(manifestPath)) {
|
|
739
|
+
throw new Error("existing package.json is a symbolic link; replace it with a regular file before appointing");
|
|
740
|
+
}
|
|
741
|
+
const raw = host.readText(manifestPath);
|
|
742
|
+
if (raw === null) {
|
|
743
|
+
// readText collapses every read error to null, so tell "absent" from
|
|
744
|
+
// "present but unreadable" (a directory, no read permission).
|
|
745
|
+
if (host.exists(manifestPath)) {
|
|
746
|
+
throw new Error("existing package.json is present but cannot be read (is it a directory, or unreadable?)");
|
|
747
|
+
}
|
|
748
|
+
if (host.readText(join(skeletonRoot, "package.json")) === null)
|
|
749
|
+
throw new Error("missing skeleton package.json");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (parseManifestObject(raw) === undefined)
|
|
753
|
+
throw new Error("existing package.json is unreadable JSON");
|
|
541
754
|
}
|
|
542
755
|
/**
|
|
543
|
-
* Pins
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
756
|
+
* Pins each hub engine (Advisor and Integrator) exactly, in `devDependencies`
|
|
757
|
+
* only, and returns what it changed. It only raises a pin: one older than
|
|
758
|
+
* live, or not a plain version (a range, a tag), becomes the live version;
|
|
759
|
+
* one newer than live is kept as it is. A pin left in any other bucket is
|
|
760
|
+
* moved to `devDependencies`. Other `@clossys/*` names are not touched. The
|
|
761
|
+
* manifest is written as 2-space-indented JSON with a final LF.
|
|
762
|
+
*
|
|
763
|
+
* `appoint` writes the packed skeleton manifest when the hub has none,
|
|
764
|
+
* refuses a manifest that is not a JSON object, names a dedicated
|
|
765
|
+
* `{owner}/workspace` hub `@owner/workspace` only when its manifest has no
|
|
766
|
+
* own `name` (an existing name is never overwritten, #1585), and rewrites
|
|
767
|
+
* the manifest.
|
|
768
|
+
* `resume` changes only the engine pins (never the `name`), writes the
|
|
769
|
+
* manifest only when a pin changed, and leaves a missing or unreadable
|
|
770
|
+
* manifest as it is (the health report then shows the engine pins as
|
|
771
|
+
* missing).
|
|
547
772
|
*/
|
|
548
|
-
function
|
|
773
|
+
function mergeHubEnginePins(host, directory, skeletonRoot, versions, owner, repository, mode) {
|
|
549
774
|
const path = join(directory, "package.json");
|
|
550
775
|
const raw = host.readText(path);
|
|
551
776
|
if (raw === null) {
|
|
777
|
+
if (mode === "resume")
|
|
778
|
+
return [];
|
|
552
779
|
const skeleton = host.readText(join(skeletonRoot, "package.json"));
|
|
553
780
|
if (skeleton === null)
|
|
554
781
|
throw new Error("missing skeleton package.json");
|
|
555
|
-
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner, repository,
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
let manifest;
|
|
559
|
-
try {
|
|
560
|
-
const parsed = JSON.parse(raw);
|
|
561
|
-
if (!isRecord(parsed))
|
|
562
|
-
throw new Error("package.json is not an object");
|
|
563
|
-
manifest = parsed;
|
|
782
|
+
writeSkeletonFile(host, directory, "package.json", substitute(skeleton, { owner, repository, ...versions }));
|
|
783
|
+
return engineVersionEntries(versions).map(([engine, version]) => ({ package: engine, to: version }));
|
|
564
784
|
}
|
|
565
|
-
|
|
785
|
+
const manifest = parseManifestObject(raw);
|
|
786
|
+
if (manifest === undefined) {
|
|
787
|
+
if (mode === "resume")
|
|
788
|
+
return [];
|
|
566
789
|
throw new Error("existing package.json is unreadable JSON");
|
|
567
790
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
791
|
+
const changes = [];
|
|
792
|
+
for (const [engine, live] of engineVersionEntries(versions)) {
|
|
793
|
+
const devPin = isRecord(manifest.devDependencies) ? pinString(manifest.devDependencies[engine]) : undefined;
|
|
794
|
+
let movedFrom;
|
|
795
|
+
let movedPin;
|
|
796
|
+
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
797
|
+
if (bucket === "devDependencies")
|
|
798
|
+
continue;
|
|
799
|
+
const current = manifest[bucket];
|
|
800
|
+
if (!isRecord(current) || !(engine in current))
|
|
801
|
+
continue;
|
|
802
|
+
if (movedFrom === undefined) {
|
|
803
|
+
movedFrom = bucket;
|
|
804
|
+
movedPin = pinString(current[engine]);
|
|
805
|
+
}
|
|
806
|
+
const next = { ...current };
|
|
807
|
+
delete next[engine];
|
|
808
|
+
if (Object.keys(next).length === 0)
|
|
809
|
+
delete manifest[bucket];
|
|
810
|
+
else
|
|
811
|
+
manifest[bucket] = next;
|
|
812
|
+
}
|
|
813
|
+
const existing = devPin ?? movedPin;
|
|
814
|
+
// Only raise: a pin newer than live stays, as the health grader treats it as current.
|
|
815
|
+
const to = existing !== undefined && compareVersions(existing, live) === 1 ? existing : live;
|
|
816
|
+
const devDependencies = isRecord(manifest.devDependencies) ? { ...manifest.devDependencies } : {};
|
|
817
|
+
devDependencies[engine] = to;
|
|
818
|
+
manifest.devDependencies = devDependencies;
|
|
819
|
+
if (to !== devPin || movedFrom !== undefined) {
|
|
820
|
+
changes.push({
|
|
821
|
+
package: engine,
|
|
822
|
+
...(existing === undefined ? {} : { from: existing }),
|
|
823
|
+
to,
|
|
824
|
+
...(devPin === undefined && movedFrom !== undefined ? { movedFrom } : {}),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Only a nameless manifest is named: an existing `name` is the project's own and is never overwritten (#1585).
|
|
829
|
+
if (mode === "appoint" && repository === DEFAULT_REPOSITORY_NAME && !Object.hasOwn(manifest, "name")) {
|
|
585
830
|
manifest.name = `@${owner}/${repository}`;
|
|
586
831
|
}
|
|
832
|
+
if (mode === "resume" && changes.length === 0)
|
|
833
|
+
return changes;
|
|
587
834
|
host.writeText(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
835
|
+
return changes;
|
|
836
|
+
}
|
|
837
|
+
/** Lockfiles Launcher recognises in a hub, with the install command that updates each. */
|
|
838
|
+
const HUB_LOCKFILES = [
|
|
839
|
+
// npm reads npm-shrinkwrap.json instead of package-lock.json when both exist.
|
|
840
|
+
["npm-shrinkwrap.json", "npm install"],
|
|
841
|
+
["package-lock.json", "npm install"],
|
|
842
|
+
["pnpm-lock.yaml", "pnpm install"],
|
|
843
|
+
["yarn.lock", "yarn install"],
|
|
844
|
+
["bun.lock", "bun install"],
|
|
845
|
+
["bun.lockb", "bun install"],
|
|
846
|
+
];
|
|
847
|
+
function describeEnginePinChange(change) {
|
|
848
|
+
const moved = change.movedFrom === undefined ? "" : ` (moved from ${change.movedFrom} to devDependencies)`;
|
|
849
|
+
if (change.from === undefined)
|
|
850
|
+
return `${change.package} added at ${change.to}${moved}`;
|
|
851
|
+
if (change.from === change.to)
|
|
852
|
+
return `${change.package} ${change.to}${moved}`;
|
|
853
|
+
return `${change.package} ${change.from} -> ${change.to}${moved}`;
|
|
854
|
+
}
|
|
855
|
+
/** The one next step after a run changed engine pins: install with the hub's package manager, then commit the manifest with its lockfile. */
|
|
856
|
+
function enginePinNextStep(host, directory) {
|
|
857
|
+
const lockfile = HUB_LOCKFILES.find(([name]) => host.exists(join(directory, name)));
|
|
858
|
+
return lockfile === undefined
|
|
859
|
+
? "run your package manager's install in the hub, then commit package.json together with the lockfile it writes"
|
|
860
|
+
: `run \`${lockfile[1]}\` in the hub, then commit package.json together with ${lockfile[0]}`;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Whether the hub's lockfile resolves its engine pins. An npm lockfile is
|
|
864
|
+
* read: each engine pinned to a plain version in `devDependencies` must
|
|
865
|
+
* resolve to that version. Any other lockfile is not read, so it counts as
|
|
866
|
+
* not resolving the engines this run changed. No lockfile, no finding.
|
|
867
|
+
*/
|
|
868
|
+
function engineInstallFinding(host, directory, changes) {
|
|
869
|
+
const found = HUB_LOCKFILES.find(([name]) => host.exists(join(directory, name)));
|
|
870
|
+
if (found === undefined)
|
|
871
|
+
return undefined;
|
|
872
|
+
const [lockfile, command] = found;
|
|
873
|
+
const changed = [...new Set(changes.map((change) => change.package))];
|
|
874
|
+
let packages = changed;
|
|
875
|
+
if (lockfile === "package-lock.json" || lockfile === "npm-shrinkwrap.json") {
|
|
876
|
+
const lock = readJson(host, join(directory, lockfile));
|
|
877
|
+
const manifest = readJson(host, join(directory, "package.json"));
|
|
878
|
+
const devDependencies = isRecord(manifest) && isRecord(manifest.devDependencies) ? manifest.devDependencies : {};
|
|
879
|
+
if (isRecord(lock)) {
|
|
880
|
+
packages = HUB_ENGINE_PACKAGES.filter((engine) => {
|
|
881
|
+
const pinned = pinString(devDependencies[engine]);
|
|
882
|
+
if (pinned === undefined || compareVersions(pinned, pinned) === null)
|
|
883
|
+
return false;
|
|
884
|
+
const lockPackages = isRecord(lock.packages) ? lock.packages : {};
|
|
885
|
+
const lockDependencies = isRecord(lock.dependencies) ? lock.dependencies : {};
|
|
886
|
+
const entry = lockPackages[`node_modules/${engine}`] ?? lockDependencies[engine];
|
|
887
|
+
const locked = isRecord(entry) ? pinString(entry.version) : undefined;
|
|
888
|
+
return locked === undefined || compareVersions(locked, pinned) !== 0;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (packages.length === 0)
|
|
893
|
+
return undefined;
|
|
894
|
+
return {
|
|
895
|
+
kind: "engine-pins-changed-install-needed",
|
|
896
|
+
lockfile,
|
|
897
|
+
command,
|
|
898
|
+
packages,
|
|
899
|
+
note: `${lockfile} does not resolve the pinned ${packages.join(" and ")} yet; run \`${command}\` in the hub, then commit package.json together with ${lockfile}`,
|
|
900
|
+
};
|
|
588
901
|
}
|
|
589
902
|
function copySkeleton(host, skeletonRoot, plan) {
|
|
590
903
|
for (const relativePath of SKELETON_FILES) {
|
|
@@ -616,8 +929,59 @@ function assertCleanTree(host, directory) {
|
|
|
616
929
|
throw new Error(`${where} has uncommitted changes (git status --porcelain is nonempty); commit or stash them before appointing it as the account hub`);
|
|
617
930
|
}
|
|
618
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* An inventory document Launcher composed (from a choice, or a merge),
|
|
934
|
+
* checked again at write time by the same function every later read of it
|
|
935
|
+
* uses, so a document Launcher writes is always one Launcher reads back.
|
|
936
|
+
*/
|
|
937
|
+
function revalidatedDocument(document, hubOwner, label = "the chosen inventory") {
|
|
938
|
+
const validated = validateInventoryDocument(document, { hubOwner });
|
|
939
|
+
if (!validated.valid)
|
|
940
|
+
throw new Error(`${label} ${validated.reason}`);
|
|
941
|
+
return document;
|
|
942
|
+
}
|
|
619
943
|
function adoptHubFiles(host, skeletonRoot, plan) {
|
|
620
944
|
assertCleanTree(host, plan.directory);
|
|
945
|
+
// Resolve and strictly re-validate the inventory document BEFORE writing
|
|
946
|
+
// anything, including the hub marker -- planWorkspace already validated it
|
|
947
|
+
// once, but re-checking here (rather than trusting the earlier result)
|
|
948
|
+
// means a document that changed on disk between plan and apply still
|
|
949
|
+
// cannot land a mismatched shape, and it means this function alone
|
|
950
|
+
// guarantees "fail before any file is touched" (#1334).
|
|
951
|
+
let inventoryDocument;
|
|
952
|
+
if (plan.action === "adopt" && plan.chosenInventory !== undefined) {
|
|
953
|
+
if (plan.chosenInventory.kind === "write")
|
|
954
|
+
inventoryDocument = revalidatedDocument(plan.chosenInventory.document, plan.owner);
|
|
955
|
+
}
|
|
956
|
+
else if (plan.action === "adopt" && plan.mergedInventoryDocument !== undefined) {
|
|
957
|
+
inventoryDocument = revalidatedDocument(plan.mergedInventoryDocument, plan.owner, "the merged inventory");
|
|
958
|
+
}
|
|
959
|
+
else if (plan.action === "adopt" && plan.mergedInventoryRepositories !== undefined) {
|
|
960
|
+
// A plan carrying the merged entries without the rendered document: each entry is written whole, `packages`
|
|
961
|
+
// included, in the same layout renderInventoryDocument() writes; the contract check decides whether it is valid.
|
|
962
|
+
const repositories = plan.mergedInventoryRepositories;
|
|
963
|
+
inventoryDocument = revalidatedDocument(`${JSON.stringify({ schemaVersion: 1, repositories }, null, 2)}\n`, plan.owner, "the merged inventory");
|
|
964
|
+
}
|
|
965
|
+
else if ("mergedInventoryIds" in plan && Array.isArray(plan.mergedInventoryIds)) {
|
|
966
|
+
// A plan built by hand with ids only: there are no entries to keep, so each id is written alone.
|
|
967
|
+
inventoryDocument = revalidatedDocument(renderInventoryDocument(plan.mergedInventoryIds.map((id) => ({ id }))), plan.owner, "the merged inventory");
|
|
968
|
+
}
|
|
969
|
+
else if ("inventorySource" in plan && typeof plan.inventorySource === "string") {
|
|
970
|
+
const raw = host.readBytes(plan.inventorySource);
|
|
971
|
+
if (raw === null)
|
|
972
|
+
throw new Error(`inventory source is not readable: ${plan.inventorySource}`);
|
|
973
|
+
const validated = validateInventoryDocument(raw, { hubOwner: plan.owner });
|
|
974
|
+
if (!validated.valid)
|
|
975
|
+
throw new Error(`inventory source at ${plan.inventorySource} ${validated.reason}`);
|
|
976
|
+
if (validated.ids.length === 0) {
|
|
977
|
+
throw new Error(`inventory source at ${plan.inventorySource} must be a populated inventory document`);
|
|
978
|
+
}
|
|
979
|
+
// Copied byte for byte: the bytes just validated are the bytes written.
|
|
980
|
+
inventoryDocument = withFinalNewline(raw);
|
|
981
|
+
}
|
|
982
|
+
// The manifest is read and checked here too, for the same reason: mergeHubEnginePins refuses it only after
|
|
983
|
+
// the marker and the composed files are written, which would leave a half-appointed checkout.
|
|
984
|
+
assertManifestAppointable(host, plan.directory, skeletonRoot);
|
|
621
985
|
const marker = {
|
|
622
986
|
schemaVersion: 1,
|
|
623
987
|
kind: "account-hub",
|
|
@@ -625,17 +989,10 @@ function adoptHubFiles(host, skeletonRoot, plan) {
|
|
|
625
989
|
repository: `${plan.owner}/${plan.repository}`,
|
|
626
990
|
};
|
|
627
991
|
writeSkeletonFile(host, plan.directory, WORKSPACE_MARKER_REL, `${JSON.stringify(marker, null, 2)}\n`);
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
else if ("inventorySource" in plan && typeof plan.inventorySource === "string") {
|
|
633
|
-
const raw = host.readText(plan.inventorySource);
|
|
634
|
-
if (raw === null || inspectInventory(raw).status !== "populated") {
|
|
635
|
-
throw new Error("inventory source is not a populated inventory document");
|
|
636
|
-
}
|
|
637
|
-
writeSkeletonFile(host, plan.directory, WORKSPACE_INVENTORY_REL, raw.endsWith("\n") ? raw : `${raw}\n`);
|
|
638
|
-
}
|
|
992
|
+
if (typeof inventoryDocument === "string")
|
|
993
|
+
writeSkeletonFile(host, plan.directory, WORKSPACE_INVENTORY_REL, inventoryDocument);
|
|
994
|
+
else if (inventoryDocument !== undefined)
|
|
995
|
+
writeSkeletonBytes(host, plan.directory, WORKSPACE_INVENTORY_REL, inventoryDocument);
|
|
639
996
|
if (host.readText(join(plan.directory, "AGENTS.md")) === null) {
|
|
640
997
|
writeSkeletonFile(host, plan.directory, "AGENTS.md", CONSUMER_AGENTS_MD);
|
|
641
998
|
}
|
|
@@ -655,7 +1012,7 @@ function adoptHubFiles(host, skeletonRoot, plan) {
|
|
|
655
1012
|
if (ignore !== null)
|
|
656
1013
|
writeSkeletonFile(host, plan.directory, ".gitignore", ignore);
|
|
657
1014
|
}
|
|
658
|
-
|
|
1015
|
+
return mergeHubEnginePins(host, plan.directory, skeletonRoot, { advisorVersion: plan.advisorVersion, integratorVersion: plan.integratorVersion }, plan.owner, plan.repository, "appoint");
|
|
659
1016
|
}
|
|
660
1017
|
function requireZero(result, label) {
|
|
661
1018
|
if (result.status !== 0) {
|
|
@@ -664,22 +1021,30 @@ function requireZero(result, label) {
|
|
|
664
1021
|
}
|
|
665
1022
|
/**
|
|
666
1023
|
* Read-only pin and inventory report. Does not install or uninstall. Scans all
|
|
667
|
-
* four dependency buckets
|
|
668
|
-
*
|
|
669
|
-
*
|
|
1024
|
+
* four dependency buckets for both hub engines (Advisor and Integrator);
|
|
1025
|
+
* grades each pinned version against that engine's live registry version,
|
|
1026
|
+
* when known, marking a pin older than live as a stale-pin finding and a
|
|
1027
|
+
* degraded report. `liveIntegratorVersion` is the last parameter so earlier
|
|
1028
|
+
* callers keep their argument positions.
|
|
670
1029
|
*/
|
|
671
|
-
export function reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, retiredThisRun = [], migration) {
|
|
1030
|
+
export function reportHubHealth(host, directory, liveAdvisorVersion, liveLauncherVersion, retiredThisRun = [], migration, liveIntegratorVersion) {
|
|
672
1031
|
const extra = new Set();
|
|
673
|
-
const pins = {
|
|
1032
|
+
const pins = {
|
|
1033
|
+
[ADVISOR_PACKAGE]: {},
|
|
1034
|
+
[INTEGRATOR_PACKAGE]: {},
|
|
1035
|
+
};
|
|
674
1036
|
const manifestRaw = host.readText(join(directory, "package.json"));
|
|
675
1037
|
if (manifestRaw !== null) {
|
|
676
1038
|
try {
|
|
677
1039
|
const parsed = JSON.parse(manifestRaw);
|
|
678
1040
|
if (isRecord(parsed)) {
|
|
679
1041
|
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
1042
|
+
const found = enginePinsIn(parsed[bucket], extra);
|
|
1043
|
+
for (const engine of HUB_ENGINE_PACKAGES) {
|
|
1044
|
+
const pinned = found[engine];
|
|
1045
|
+
if (pinned !== undefined)
|
|
1046
|
+
pins[engine][bucket] = pinned;
|
|
1047
|
+
}
|
|
683
1048
|
}
|
|
684
1049
|
}
|
|
685
1050
|
}
|
|
@@ -687,71 +1052,114 @@ export function reportHubHealth(host, directory, liveAdvisorVersion, liveLaunche
|
|
|
687
1052
|
/* unreadable manifest is reported as missing pins */
|
|
688
1053
|
}
|
|
689
1054
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
1055
|
+
const live = {
|
|
1056
|
+
[ADVISOR_PACKAGE]: liveAdvisorVersion,
|
|
1057
|
+
[INTEGRATOR_PACKAGE]: liveIntegratorVersion,
|
|
1058
|
+
};
|
|
1059
|
+
const pinFindings = HUB_ENGINE_PACKAGES.flatMap((engine) => DEPENDENCY_BUCKETS.flatMap((bucket) => {
|
|
1060
|
+
const pinned = pins[engine][bucket];
|
|
1061
|
+
const liveVersion = live[engine];
|
|
1062
|
+
if (pinned === undefined || liveVersion === undefined)
|
|
692
1063
|
return [];
|
|
693
|
-
const comparison = compareVersions(pinned,
|
|
1064
|
+
const comparison = compareVersions(pinned, liveVersion);
|
|
694
1065
|
if (comparison === null) {
|
|
695
|
-
return [{
|
|
1066
|
+
return [{ package: engine, bucket, pinned, grade: "indeterminate", note: `cannot compare ${pinned} with live ${liveVersion}` }];
|
|
696
1067
|
}
|
|
697
1068
|
return comparison < 0
|
|
698
|
-
? [{
|
|
1069
|
+
? [{ package: engine, bucket, pinned, grade: "stale", note: `pinned ${pinned} is older than live ${liveVersion}` }]
|
|
699
1070
|
: [];
|
|
700
|
-
});
|
|
1071
|
+
}));
|
|
1072
|
+
const pinCount = (engine) => Object.values(pins[engine]).filter((value) => value !== undefined).length;
|
|
1073
|
+
const misplaced = (engine) => pins[engine].devDependencies === undefined ||
|
|
1074
|
+
pins[engine].dependencies !== undefined ||
|
|
1075
|
+
pins[engine].optionalDependencies !== undefined ||
|
|
1076
|
+
pins[engine].peerDependencies !== undefined;
|
|
1077
|
+
const dualPin = HUB_ENGINE_PACKAGES.some((engine) => pinCount(engine) > 1);
|
|
701
1078
|
const skillsManifest = summarizeSkillsManifest(parseSkillManifest(host.readText(join(directory, SKILLS_MANIFEST_REL))), liveLauncherVersion, retiredThisRun);
|
|
1079
|
+
const enginePin = (engine) => {
|
|
1080
|
+
const found = pins[engine];
|
|
1081
|
+
const liveVersion = live[engine];
|
|
1082
|
+
return {
|
|
1083
|
+
...(found.dependencies === undefined ? {} : { dependencies: found.dependencies }),
|
|
1084
|
+
...(found.devDependencies === undefined ? {} : { devDependencies: found.devDependencies }),
|
|
1085
|
+
...(found.optionalDependencies === undefined ? {} : { optionalDependencies: found.optionalDependencies }),
|
|
1086
|
+
...(found.peerDependencies === undefined ? {} : { peerDependencies: found.peerDependencies }),
|
|
1087
|
+
...(liveVersion === undefined ? {} : { live: liveVersion }),
|
|
1088
|
+
};
|
|
1089
|
+
};
|
|
1090
|
+
const inventory = inspectInventory(host.readBytes(join(directory, WORKSPACE_INVENTORY_REL)), readHub(host, directory)?.owner);
|
|
702
1091
|
return {
|
|
703
1092
|
marker: readHub(host, directory) === undefined ? "missing" : "present",
|
|
704
|
-
inventory
|
|
705
|
-
advisorPin:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
...(pins.optionalDependencies === undefined ? {} : { optionalDependencies: pins.optionalDependencies }),
|
|
709
|
-
...(pins.peerDependencies === undefined ? {} : { peerDependencies: pins.peerDependencies }),
|
|
710
|
-
...(liveAdvisorVersion === undefined ? {} : { live: liveAdvisorVersion }),
|
|
711
|
-
},
|
|
712
|
-
dualPin: Object.values(pins).filter((value) => value !== undefined).length > 1,
|
|
1093
|
+
inventory,
|
|
1094
|
+
advisorPin: enginePin(ADVISOR_PACKAGE),
|
|
1095
|
+
integratorPin: enginePin(INTEGRATOR_PACKAGE),
|
|
1096
|
+
dualPin,
|
|
713
1097
|
extraClossys: [...extra].sort(),
|
|
714
1098
|
pinFindings,
|
|
715
1099
|
degraded: pinFindings.some((finding) => finding.grade === "stale") ||
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
pins.optionalDependencies !== undefined ||
|
|
720
|
-
pins.peerDependencies !== undefined,
|
|
1100
|
+
dualPin ||
|
|
1101
|
+
HUB_ENGINE_PACKAGES.some(misplaced) ||
|
|
1102
|
+
inventory.status === "invalid",
|
|
721
1103
|
...(migration === undefined ? {} : { migration }),
|
|
722
1104
|
skillsManifest,
|
|
723
1105
|
};
|
|
724
1106
|
}
|
|
725
|
-
|
|
1107
|
+
function formatEnginePin(pin) {
|
|
726
1108
|
const pinParts = [];
|
|
727
1109
|
for (const bucket of DEPENDENCY_BUCKETS) {
|
|
728
|
-
const pinned =
|
|
1110
|
+
const pinned = pin[bucket];
|
|
729
1111
|
if (pinned !== undefined)
|
|
730
1112
|
pinParts.push(`${bucket} ${pinned}`);
|
|
731
1113
|
}
|
|
732
|
-
const
|
|
733
|
-
|
|
1114
|
+
const pinned = pinParts.length === 0 ? "missing" : pinParts.join(" and ");
|
|
1115
|
+
return `${pinned}${pin.live === undefined ? "" : `; live ${pin.live}`}`;
|
|
1116
|
+
}
|
|
1117
|
+
export function formatHubHealth(report) {
|
|
734
1118
|
const extra = report.extraClossys.length === 0 ? "none" : report.extraClossys.join(", ");
|
|
735
|
-
const inventory = report.inventory.status === "populated"
|
|
736
|
-
|
|
1119
|
+
const inventory = report.inventory.status === "populated"
|
|
1120
|
+
? `populated (${report.inventory.count})`
|
|
1121
|
+
: report.inventory.status === "invalid"
|
|
1122
|
+
? `invalid${report.inventory.reason === undefined ? "" : ` -- ${report.inventory.reason}`}`
|
|
1123
|
+
: report.inventory.status;
|
|
1124
|
+
const findings = report.pinFindings.map((finding) => finding.note !== undefined
|
|
1125
|
+
? `${finding.package} ${finding.bucket} ${finding.note}`
|
|
1126
|
+
: `${finding.package} ${finding.bucket} ${finding.grade}`);
|
|
737
1127
|
const findingLine = findings.length === 0 ? "none" : findings.join("; ");
|
|
1128
|
+
const enginePinLines = report.enginePins === undefined
|
|
1129
|
+
? []
|
|
1130
|
+
: [
|
|
1131
|
+
`engine pins changed in package.json: ${report.enginePins.changed.map(describeEnginePinChange).join("; ")}`,
|
|
1132
|
+
`next: ${report.enginePins.nextStep}`,
|
|
1133
|
+
];
|
|
1134
|
+
const installLine = report.installNeeded === undefined ? [] : [`install needed (${report.installNeeded.kind}): ${report.installNeeded.note}`];
|
|
738
1135
|
const skillParts = [];
|
|
739
1136
|
if (report.skillComposition !== undefined) {
|
|
740
1137
|
skillParts.push(report.skillComposition.composed.length === 0
|
|
741
1138
|
? "skills composed: none"
|
|
742
1139
|
: `skills composed: ${report.skillComposition.composed.map((name) => `clossys-${name}`).join(", ")}`);
|
|
743
1140
|
for (const skip of report.skillComposition.skipped) {
|
|
1141
|
+
// skip.packageDir names one of this package's own known package directories, never document text (see skills.ts).
|
|
744
1142
|
skillParts.push(`skill skipped (${skip.packageDir}): ${skip.note}`);
|
|
745
1143
|
}
|
|
1144
|
+
// rosterTargets and siblings' inventoryId are already position-safe by
|
|
1145
|
+
// the time they reach here: composeSkillRoster redacts every
|
|
1146
|
+
// stored-inventory id to `inventoryPositionLabel()`'s position label
|
|
1147
|
+
// before putting it in this report, because this whole report is also
|
|
1148
|
+
// JSON-dumped into the `health:` line below -- an id left raw in the
|
|
1149
|
+
// structured report would still reach the message that way even if this
|
|
1150
|
+
// prose line named it safely.
|
|
746
1151
|
if (report.skillComposition.rosterTargets !== undefined && report.skillComposition.rosterTargets.length > 0) {
|
|
747
1152
|
skillParts.push(`skill roster written: ${report.skillComposition.rosterTargets.join(", ")}`);
|
|
748
1153
|
}
|
|
749
|
-
for (const
|
|
750
|
-
skillParts.push(`
|
|
1154
|
+
for (const sibling of report.skillComposition.siblings ?? []) {
|
|
1155
|
+
skillParts.push(`sibling (${sibling.inventoryId}): ${sibling.note}`);
|
|
751
1156
|
}
|
|
752
1157
|
if (report.skillComposition.retired !== undefined && report.skillComposition.retired.length > 0) {
|
|
753
1158
|
skillParts.push(`skills retired: ${report.skillComposition.retired.map((name) => `clossys-${name}`).join(", ")}`);
|
|
754
1159
|
}
|
|
1160
|
+
for (const kept of report.skillComposition.preserved ?? []) {
|
|
1161
|
+
skillParts.push(`skill preserved (clossys-${kept.packageDir}, not ${kept.action === "rewrite" ? "rewritten" : "retired"}): ${kept.note}`);
|
|
1162
|
+
}
|
|
755
1163
|
}
|
|
756
1164
|
const skillsManifestLine = report.skillsManifest === undefined
|
|
757
1165
|
? undefined
|
|
@@ -766,14 +1174,17 @@ export function formatHubHealth(report) {
|
|
|
766
1174
|
? undefined
|
|
767
1175
|
: report.inventoryDrift.status === "indeterminate"
|
|
768
1176
|
? `inventory drift: indeterminate${report.inventoryDrift.note === undefined ? "" : ` -- ${report.inventoryDrift.note}`}`
|
|
769
|
-
: `inventory drift: external-only ${report.inventoryDrift.externalOnly.
|
|
1177
|
+
: `inventory drift: external-only ${report.inventoryDrift.externalOnly.count}, launcher-only ${report.inventoryDrift.launcherOnly.count}, agreeing ${report.inventoryDrift.agreeing.count}`;
|
|
770
1178
|
return [
|
|
771
1179
|
`hub marker: ${report.marker}`,
|
|
772
1180
|
`inventory: ${inventory}`,
|
|
773
|
-
`advisor pin: ${
|
|
1181
|
+
`advisor pin: ${formatEnginePin(report.advisorPin)}`,
|
|
1182
|
+
`integrator pin: ${formatEnginePin(report.integratorPin)}`,
|
|
774
1183
|
`dual pin: ${report.dualPin ? "yes" : "no"}`,
|
|
775
1184
|
`extra @clossys/*: ${extra}`,
|
|
776
1185
|
`pin findings: ${findingLine}`,
|
|
1186
|
+
...enginePinLines,
|
|
1187
|
+
...installLine,
|
|
777
1188
|
`degraded: ${report.degraded ? "yes" : "no"}`,
|
|
778
1189
|
...(migrationLine === undefined ? [] : [migrationLine]),
|
|
779
1190
|
...(linkedHostsLine === undefined ? [] : [linkedHostsLine]),
|
|
@@ -783,15 +1194,20 @@ export function formatHubHealth(report) {
|
|
|
783
1194
|
`health: ${JSON.stringify(report)}`,
|
|
784
1195
|
].join("\n");
|
|
785
1196
|
}
|
|
786
|
-
function withHealth(host, directory, headline,
|
|
787
|
-
const base = reportHubHealth(host, directory,
|
|
788
|
-
const
|
|
1197
|
+
function withHealth(host, directory, headline, liveEngines, skillComposition, liveLauncherVersion, migration, inventoryDrift, enginePinChanges = []) {
|
|
1198
|
+
const base = reportHubHealth(host, directory, liveEngines.advisorVersion, liveLauncherVersion, skillComposition?.retired ?? [], migration, liveEngines.integratorVersion);
|
|
1199
|
+
const preserved = skillComposition?.preserved ?? [];
|
|
1200
|
+
const installNeeded = engineInstallFinding(host, directory, enginePinChanges);
|
|
789
1201
|
const health = {
|
|
790
1202
|
...base,
|
|
1203
|
+
...(enginePinChanges.length === 0
|
|
1204
|
+
? {}
|
|
1205
|
+
: { enginePins: { changed: enginePinChanges, nextStep: enginePinNextStep(host, directory) } }),
|
|
1206
|
+
...(installNeeded === undefined ? {} : { installNeeded }),
|
|
791
1207
|
...(skillComposition === undefined ? {} : { skillComposition }),
|
|
792
1208
|
...(skillComposition?.linkedHosts === undefined ? {} : { linkedHosts: skillComposition.linkedHosts }),
|
|
793
1209
|
...(inventoryDrift === undefined || inventoryDrift.status === "no-external-source" ? {} : { inventoryDrift }),
|
|
794
|
-
degraded: base.degraded ||
|
|
1210
|
+
degraded: base.degraded || preserved.length > 0 || installNeeded !== undefined,
|
|
795
1211
|
};
|
|
796
1212
|
return {
|
|
797
1213
|
state: "satisfied",
|
|
@@ -805,8 +1221,8 @@ function withHealth(host, directory, headline, liveAdvisorVersion, skillComposit
|
|
|
805
1221
|
* a note; marks ids whose check fails as unknown. Never mutates the inventory.
|
|
806
1222
|
*/
|
|
807
1223
|
export function checkInventoryEntries(host, directory) {
|
|
808
|
-
const
|
|
809
|
-
const observation = inspectInventory(
|
|
1224
|
+
const hubOwner = readHub(host, directory)?.owner;
|
|
1225
|
+
const observation = inspectInventory(host.readBytes(join(directory, WORKSPACE_INVENTORY_REL)), hubOwner);
|
|
810
1226
|
if (observation.status !== "populated") {
|
|
811
1227
|
return { entries: [], skipped: true, note: `inventory is ${observation.status}; nothing to validate` };
|
|
812
1228
|
}
|
|
@@ -814,7 +1230,7 @@ export function checkInventoryEntries(host, directory) {
|
|
|
814
1230
|
if (!available) {
|
|
815
1231
|
return { entries: [], skipped: true, note: "`gh` is unavailable; inventory ids were not validated" };
|
|
816
1232
|
}
|
|
817
|
-
const ids = readInventoryRepositories(host, join(directory, WORKSPACE_INVENTORY_REL), "the hub inventory").filter((id) => id !== "");
|
|
1233
|
+
const ids = readInventoryRepositories(host, join(directory, WORKSPACE_INVENTORY_REL), "the hub inventory", hubOwner).filter((id) => id !== "");
|
|
818
1234
|
const batch = 20;
|
|
819
1235
|
const entries = [];
|
|
820
1236
|
for (let index = 0; index < ids.length; index += batch) {
|
|
@@ -840,6 +1256,8 @@ function shouldRefreshConsumerAgents(existing) {
|
|
|
840
1256
|
return false;
|
|
841
1257
|
if (existing === LEGACY_CONSUMER_AGENTS_MD)
|
|
842
1258
|
return true;
|
|
1259
|
+
if (existing === SIBLING_COMPOSING_CONSUMER_AGENTS_MD)
|
|
1260
|
+
return true;
|
|
843
1261
|
if (existing.includes("Run `npx @clossys/launcher` again to resume"))
|
|
844
1262
|
return true;
|
|
845
1263
|
return false;
|
|
@@ -850,111 +1268,163 @@ function writeConsumerAgentsIfNeeded(host, directory) {
|
|
|
850
1268
|
return;
|
|
851
1269
|
writeSkeletonFile(host, directory, "AGENTS.md", CONSUMER_AGENTS_MD);
|
|
852
1270
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1271
|
+
/** What a hub run reports for each inventoried repository other than the hub. It never writes into one. */
|
|
1272
|
+
const SETUP_PULL_REQUEST_NOTE = "a hub run writes nothing here; once this repository is staffed in an approved plan, @clossys-advisor and the voices of the roles staffed there arrive with that plan's setup pull request";
|
|
1273
|
+
const BESIDE_HUB_NOTE = `checkout beside the hub; ${SETUP_PULL_REQUEST_NOTE}`;
|
|
1274
|
+
const CLONE_NOT_BESIDE_HUB_NOTE = `not cloned beside the hub; ${SETUP_PULL_REQUEST_NOTE}`;
|
|
1275
|
+
/**
|
|
1276
|
+
* Splits an inventory id into owner/repository, trusting a caller that
|
|
1277
|
+
* already validated it with `isValidInventoryId` (every id reaching here
|
|
1278
|
+
* comes from a document `validateInventoryDocument` already accepted)
|
|
1279
|
+
* rather than re-deriving that rule -- but still runs it, so a caller that
|
|
1280
|
+
* somehow supplies an unvalidated id gets `null`, never a wrong split.
|
|
1281
|
+
*/
|
|
860
1282
|
function parseInventoryRepositoryId(id, hubOwner) {
|
|
861
|
-
|
|
862
|
-
if (trimmed === "")
|
|
1283
|
+
if (!isValidInventoryId(id))
|
|
863
1284
|
return null;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
return { owner: hubOwner, repository: trimmed };
|
|
1285
|
+
const slash = id.indexOf("/");
|
|
1286
|
+
if (slash === -1)
|
|
1287
|
+
return { owner: hubOwner, repository: id };
|
|
1288
|
+
return { owner: id.slice(0, slash), repository: id.slice(slash + 1) };
|
|
1289
|
+
}
|
|
1290
|
+
/** A checkout's origin as `owner/name`, when it is a github.com repository. */
|
|
1291
|
+
function originRepository(host, directory) {
|
|
1292
|
+
const result = host.run("git", ["remote", "get-url", "origin"], { cwd: directory });
|
|
1293
|
+
const remote = result.status === 0 ? parseGitHubRemote(result.stdout.trim()) : null;
|
|
1294
|
+
return remote === null ? undefined : `${remote.owner}/${remote.repository}`;
|
|
875
1295
|
}
|
|
876
|
-
|
|
1296
|
+
/**
|
|
1297
|
+
* The hub's own repository identity: its origin's `owner/name`, or, when
|
|
1298
|
+
* the checkout has no github.com origin, the repository its hub marker
|
|
1299
|
+
* records. Never its folder path (see identity.ts).
|
|
1300
|
+
*/
|
|
1301
|
+
function hubRepositoryIdentity(host, hubDirectory) {
|
|
1302
|
+
return originRepository(host, hubDirectory) ?? readHub(host, hubDirectory)?.repository;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Classifies every inventoried repository other than the hub. Read-only:
|
|
1306
|
+
* it looks for a checkout beside the hub and reads that checkout's git
|
|
1307
|
+
* origin, and nothing else, so a sibling's working tree, legacy output or
|
|
1308
|
+
* old pins can neither change the result nor be changed by it. The hub run
|
|
1309
|
+
* reports the result; only `--clone-missing` acts on it, and only on
|
|
1310
|
+
* `not-cloned` entries.
|
|
1311
|
+
*/
|
|
1312
|
+
function classifyInventoriedSiblings(host, hubDirectory, hubOwner) {
|
|
877
1313
|
const parent = dirname(resolve(hubDirectory));
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
const targets = [];
|
|
1314
|
+
const hubIdentity = hubRepositoryIdentity(host, hubDirectory);
|
|
1315
|
+
const siblings = [];
|
|
881
1316
|
const inventoryPath = join(hubDirectory, WORKSPACE_INVENTORY_REL);
|
|
882
1317
|
let inventoryIds;
|
|
883
1318
|
try {
|
|
884
|
-
inventoryIds = readInventoryRepositories(host, inventoryPath, "the
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1319
|
+
inventoryIds = readInventoryRepositories(host, inventoryPath, "the stored inventory", hubOwner);
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
// An invalid stored inventory is reported, never silently read for what
|
|
1323
|
+
// it happens to look like -- no clone is attempted from it (#1334).
|
|
1324
|
+
// WORKSPACE_INVENTORY_REL is Launcher's own constant path, not document
|
|
1325
|
+
// content, so it is safe to name directly; there is no per-id position
|
|
1326
|
+
// for a failure at the file level, so `position` stays undefined.
|
|
1327
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1328
|
+
return [{ inventoryId: WORKSPACE_INVENTORY_REL, status: "inventory-invalid", note: reason }];
|
|
1329
|
+
}
|
|
1330
|
+
for (const [position, id] of inventoryIds.entries()) {
|
|
892
1331
|
const parsed = parseInventoryRepositoryId(id, hubOwner);
|
|
893
1332
|
if (parsed === null) {
|
|
894
|
-
|
|
1333
|
+
siblings.push({ inventoryId: id, position, status: "invalid-id", note: "inventory id is not a valid repository slug" });
|
|
895
1334
|
continue;
|
|
896
1335
|
}
|
|
897
|
-
if (
|
|
898
|
-
|
|
1336
|
+
if (!belongsToOwner(id, hubOwner)) {
|
|
1337
|
+
siblings.push({ inventoryId: id, position, status: "other-account", note: "other account; not this roster" });
|
|
899
1338
|
continue;
|
|
900
1339
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (candidateResolved === hubResolved)
|
|
1340
|
+
// The hub itself is recognised by repository identity, not by folder path.
|
|
1341
|
+
if (hubIdentity !== undefined && sameRepository(id, hubIdentity, hubOwner))
|
|
904
1342
|
continue;
|
|
1343
|
+
const candidate = join(parent, parsed.repository);
|
|
905
1344
|
if (!host.exists(candidate) || !host.isDirectory(candidate)) {
|
|
906
|
-
|
|
1345
|
+
siblings.push({ inventoryId: id, position, status: "not-cloned", note: CLONE_NOT_BESIDE_HUB_NOTE });
|
|
907
1346
|
continue;
|
|
908
1347
|
}
|
|
909
1348
|
if (looksLikeFoundry(host, candidate)) {
|
|
910
|
-
|
|
1349
|
+
siblings.push({ inventoryId: id, position, status: "foundry-supplier-tree", note: "foundry supplier tree; skills are not written here" });
|
|
911
1350
|
continue;
|
|
912
1351
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1352
|
+
if (!host.exists(join(candidate, ".git"))) {
|
|
1353
|
+
siblings.push({
|
|
1354
|
+
inventoryId: id,
|
|
1355
|
+
position,
|
|
1356
|
+
status: "not-a-git-checkout",
|
|
1357
|
+
note: "the folder beside the hub with this name is not a git checkout, so it cannot be matched to this inventory id",
|
|
1358
|
+
});
|
|
918
1359
|
continue;
|
|
919
1360
|
}
|
|
920
|
-
|
|
1361
|
+
const remote = host.run("git", ["remote", "get-url", "origin"], { cwd: candidate });
|
|
1362
|
+
if (remote.status !== 0 && /dubious ownership/i.test(remote.stderr)) {
|
|
1363
|
+
siblings.push({
|
|
1364
|
+
inventoryId: id,
|
|
1365
|
+
position,
|
|
1366
|
+
status: "git-refused",
|
|
1367
|
+
note: "git refuses to read this checkout (it reports dubious ownership), so its origin could not be matched to this inventory id",
|
|
1368
|
+
});
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
const parsedOrigin = remote.status === 0 ? parseGitHubRemote(remote.stdout.trim()) : null;
|
|
1372
|
+
const origin = parsedOrigin === null ? undefined : `${parsedOrigin.owner}/${parsedOrigin.repository}`;
|
|
1373
|
+
if (origin === undefined || !sameRepository(origin, id, hubOwner)) {
|
|
1374
|
+
siblings.push({ inventoryId: id, position, status: "origin-mismatch", note: "git origin does not match inventory id" });
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
siblings.push({ inventoryId: id, position, status: "beside-the-hub", note: BESIDE_HUB_NOTE });
|
|
921
1378
|
}
|
|
922
|
-
return
|
|
1379
|
+
return siblings;
|
|
1380
|
+
}
|
|
1381
|
+
/** The position a message names instead of a document-sourced inventory id, matching `inventory-choice.ts`'s `listRemovedPositions`. `undefined` only for the stored-inventory-file-itself failure, which names the file by its own constant path instead. */
|
|
1382
|
+
export function inventoryPositionLabel(inventoryId, position) {
|
|
1383
|
+
return position === undefined ? inventoryId : `repositories[${position}] in the stored inventory`;
|
|
923
1384
|
}
|
|
924
1385
|
/**
|
|
925
1386
|
* Explicit, approved action (#1179, the #1045 pattern): clones every
|
|
926
|
-
* inventoried repository that
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* --clone-missing flag. Reverses the launcher README's own no-clone
|
|
932
|
-
* default for exactly this one approved action.
|
|
1387
|
+
* inventoried repository that classifyInventoriedSiblings found not cloned
|
|
1388
|
+
* beside the hub, and only those -- every other reason (wrong account,
|
|
1389
|
+
* foundry supplier tree, origin mismatch, invalid slug, invalid inventory)
|
|
1390
|
+
* is reported as skipped, never attempted, and a checkout already beside
|
|
1391
|
+
* the hub is left out. Never called from resume's default path; only from
|
|
1392
|
+
* the --clone-missing flag. Reverses the launcher README's own no-clone
|
|
1393
|
+
* default for exactly this one approved action. Cloning is not composing:
|
|
1394
|
+
* a cloned repository receives its team only once it is staffed in an
|
|
1395
|
+
* approved plan, with that plan's setup pull request.
|
|
933
1396
|
*/
|
|
934
1397
|
export function cloneMissingInventoryRepositories(host, hubDirectory, hubOwner) {
|
|
935
|
-
const { skipped } = resolveSisterCloneTargets(host, hubDirectory, hubOwner);
|
|
936
1398
|
const parent = dirname(resolve(hubDirectory));
|
|
937
1399
|
const outcomes = [];
|
|
938
|
-
for (const skip of
|
|
939
|
-
if (skip.
|
|
940
|
-
|
|
1400
|
+
for (const skip of classifyInventoriedSiblings(host, hubDirectory, hubOwner)) {
|
|
1401
|
+
if (skip.status === "beside-the-hub")
|
|
1402
|
+
continue;
|
|
1403
|
+
if (skip.status !== "not-cloned") {
|
|
1404
|
+
outcomes.push({ inventoryId: skip.inventoryId, position: skip.position, result: "skipped-other-reason", note: skip.note });
|
|
941
1405
|
continue;
|
|
942
1406
|
}
|
|
943
1407
|
const parsed = parseInventoryRepositoryId(skip.inventoryId, hubOwner);
|
|
944
1408
|
if (parsed === null) {
|
|
945
|
-
outcomes.push({ inventoryId: skip.inventoryId, result: "failed", note: "inventory id is not a valid repository slug" });
|
|
1409
|
+
outcomes.push({ inventoryId: skip.inventoryId, position: skip.position, result: "failed", note: "inventory id is not a valid repository slug" });
|
|
946
1410
|
continue;
|
|
947
1411
|
}
|
|
1412
|
+
// Cloning itself still uses the raw id and the real path -- that is the whole
|
|
1413
|
+
// point of this action -- but the *reported* note never repeats either: the
|
|
1414
|
+
// folder name comes straight from the inventory (a document), and `gh`'s own
|
|
1415
|
+
// stderr on failure names the repository it could not find or clone. Both are
|
|
1416
|
+
// fixed text instead (#1179); `position` is still how this outcome is named.
|
|
948
1417
|
const siblingPath = join(parent, parsed.repository);
|
|
949
1418
|
const result = host.run("gh", ["repo", "clone", `${hubOwner}/${parsed.repository}`, siblingPath]);
|
|
950
1419
|
if (result.status === 0) {
|
|
951
|
-
outcomes.push({ inventoryId: skip.inventoryId, result: "cloned", note:
|
|
1420
|
+
outcomes.push({ inventoryId: skip.inventoryId, position: skip.position, result: "cloned", note: "cloned beside the hub" });
|
|
952
1421
|
}
|
|
953
1422
|
else {
|
|
954
1423
|
outcomes.push({
|
|
955
1424
|
inventoryId: skip.inventoryId,
|
|
1425
|
+
position: skip.position,
|
|
956
1426
|
result: "failed",
|
|
957
|
-
note: `gh repo clone exited ${result.status ?? "null"}
|
|
1427
|
+
note: `gh repo clone exited ${result.status ?? "null"}`,
|
|
958
1428
|
});
|
|
959
1429
|
}
|
|
960
1430
|
}
|
|
@@ -1021,6 +1491,16 @@ function recordLinkedHosts(host, directory) {
|
|
|
1021
1491
|
writeSkeletonFile(host, directory, HOSTS_REL, serializeHostRecord({ schemaVersion: 1, linkedHosts, recordedAt: host.now() }));
|
|
1022
1492
|
return linkedHosts;
|
|
1023
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Composes the team in the hub, and only the hub: this writes nothing into
|
|
1496
|
+
* any checkout beside the hub. An inventoried product repository receives
|
|
1497
|
+
* its skills, skills manifest, host discovery links and guidance only once
|
|
1498
|
+
* it is staffed in an approved plan, with that plan's setup pull request. Each inventoried
|
|
1499
|
+
* repository other than the hub is reported, read-only, as a sibling, named
|
|
1500
|
+
* by its `inventoryPositionLabel()` position -- never its raw stored
|
|
1501
|
+
* inventory id -- because the whole report is JSON-dumped into the apply
|
|
1502
|
+
* message's `health:` line (see `formatHubHealth`).
|
|
1503
|
+
*/
|
|
1024
1504
|
function composeSkillRoster(host, hubDirectory, hubOwner, hubRepository, options) {
|
|
1025
1505
|
const composeOptions = {
|
|
1026
1506
|
launcherPackageRoot: options.launcherPackageRoot,
|
|
@@ -1032,17 +1512,13 @@ function composeSkillRoster(host, hubDirectory, hubOwner, hubRepository, options
|
|
|
1032
1512
|
writeConsumerAgentsIfNeeded(host, hubDirectory);
|
|
1033
1513
|
writeClossysReadme(host, hubDirectory);
|
|
1034
1514
|
const hubId = hubRosterId(host, hubDirectory, hubOwner, hubRepository);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
writeSisterConsumerAgentsIfNeeded(host, target.directory);
|
|
1041
|
-
rosterTargets.push(target.inventoryId);
|
|
1042
|
-
}
|
|
1043
|
-
return { ...hubSkill, rosterTargets, rosterSkipped: skipped, linkedHosts };
|
|
1515
|
+
const siblings = classifyInventoriedSiblings(host, hubDirectory, hubOwner)
|
|
1516
|
+
// An invalid stored inventory is a hub finding: the health report's inventory line names it.
|
|
1517
|
+
.filter((sibling) => sibling.status !== "inventory-invalid")
|
|
1518
|
+
.map(({ inventoryId, position, note }) => ({ inventoryId: inventoryPositionLabel(inventoryId, position), note }));
|
|
1519
|
+
return { ...hubSkill, rosterTargets: [hubId], siblings, linkedHosts };
|
|
1044
1520
|
}
|
|
1045
|
-
function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner, hubRepository,
|
|
1521
|
+
function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner, hubRepository, liveEngines, skillCatalogueRoot, contractPath, liveLauncherVersion, migration, enginePinChanges = []) {
|
|
1046
1522
|
const skillComposition = composeSkillRoster(host, directory, hubOwner, hubRepository, {
|
|
1047
1523
|
launcherPackageRoot,
|
|
1048
1524
|
...(skillCatalogueRoot === undefined ? {} : { skillCatalogueRoot }),
|
|
@@ -1051,49 +1527,148 @@ function finishHubApply(host, directory, headline, launcherPackageRoot, hubOwner
|
|
|
1051
1527
|
// #1216: when the hub marker declares an external inventory, report drift against
|
|
1052
1528
|
// it on every apply (create's fresh marker never declares one, so this is a no-op there).
|
|
1053
1529
|
const hubDocument = readHub(host, directory);
|
|
1054
|
-
const inventoryDrift = reportInventoryDrift(host, directory, hubDocument?.externalInventory, WORKSPACE_INVENTORY_REL);
|
|
1055
|
-
return withHealth(host, directory, headline,
|
|
1530
|
+
const inventoryDrift = reportInventoryDrift(host, directory, hubDocument?.externalInventory, WORKSPACE_INVENTORY_REL, hubOwner);
|
|
1531
|
+
return withHealth(host, directory, headline, liveEngines, skillComposition, liveLauncherVersion, migration, inventoryDrift, enginePinChanges);
|
|
1056
1532
|
}
|
|
1057
1533
|
/**
|
|
1058
1534
|
* Migrates a legacy `.clossys/` hub marker (and its sibling inventory, when
|
|
1059
1535
|
* present) to `clossys/.state/`, then removes the old directory. Called only
|
|
1060
1536
|
* when `locateHub` found the marker at the legacy path and nowhere else
|
|
1061
|
-
* (`plan.migrateFrom === "legacy"
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1537
|
+
* (`plan.migrateFrom === "legacy"`, or `classifyClonedHub` for a fresh
|
|
1538
|
+
* clone); a hub with markers at both paths is refused by `planWorkspace`
|
|
1539
|
+
* (or, for a clone, by `classifyClonedHub`) before anything is written, so
|
|
1540
|
+
* this never merges two hub states.
|
|
1064
1541
|
*/
|
|
1065
1542
|
function migrateLegacyHubState(host, directory) {
|
|
1066
1543
|
const markerRaw = host.readText(join(directory, LEGACY_WORKSPACE_MARKER_REL));
|
|
1067
1544
|
if (markerRaw === null)
|
|
1068
1545
|
return undefined;
|
|
1069
1546
|
writeSkeletonFile(host, directory, WORKSPACE_MARKER_REL, markerRaw.endsWith("\n") ? markerRaw : `${markerRaw}\n`);
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1547
|
+
// Moved byte for byte, so bytes that are not valid UTF-8 are still refused when read from the new place.
|
|
1548
|
+
const inventoryBytes = host.readBytes(join(directory, LEGACY_WORKSPACE_INVENTORY_REL));
|
|
1549
|
+
if (inventoryBytes !== null)
|
|
1550
|
+
writeSkeletonBytes(host, directory, WORKSPACE_INVENTORY_REL, withFinalNewline(inventoryBytes));
|
|
1074
1551
|
host.remove(join(directory, LEGACY_STATE_DIR_REL));
|
|
1075
1552
|
return { status: "migrated", from: LEGACY_STATE_DIR_REL, to: STATE_DIR_REL };
|
|
1076
1553
|
}
|
|
1077
|
-
/**
|
|
1554
|
+
/** The apply message's line saying what `--repositories` did to the inventory, or nothing without it. */
|
|
1555
|
+
function chosenInventoryNote(chosen) {
|
|
1556
|
+
return chosen === undefined ? "" : `\n${describeChosenInventory(chosen)}`;
|
|
1557
|
+
}
|
|
1558
|
+
function engineVersionsOf(plan) {
|
|
1559
|
+
return {
|
|
1560
|
+
...(plan.advisorVersion === undefined ? {} : { advisorVersion: plan.advisorVersion }),
|
|
1561
|
+
...(plan.integratorVersion === undefined ? {} : { integratorVersion: plan.integratorVersion }),
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Classifies a freshly cloned `{owner}/workspace` (a resume plan with
|
|
1566
|
+
* `clone: true`) by its marker, the way `planWorkspace` classifies a local
|
|
1567
|
+
* checkout, and returns how apply must continue (#1585). It only reads: every
|
|
1568
|
+
* refusal it decides is thrown here, before apply writes anything into the
|
|
1569
|
+
* clone, and the appoint path's own refusals (a `package.json` that is not a
|
|
1570
|
+
* JSON object among them) are thrown by `adoptHubFiles` before its first write.
|
|
1571
|
+
*
|
|
1572
|
+
* - both markers: refused, as `planWorkspace` refuses them locally;
|
|
1573
|
+
* - a marker (current or legacy) for another owner: refused;
|
|
1574
|
+
* - only the legacy marker: resume, migrating it;
|
|
1575
|
+
* - only the current marker: resume;
|
|
1576
|
+
* - no marker: an existing repository with unrelated content is being
|
|
1577
|
+
* appointed, so apply takes the adopt path, which keeps its files. That
|
|
1578
|
+
* path needs both engine versions, refuses the Foundry supplier tree, and
|
|
1579
|
+
* refuses an on-disk inventory that fails its contract (a missing or valid
|
|
1580
|
+
* one is kept as it is; this path writes no inventory), and refuses a
|
|
1581
|
+
* `package.json` that is not a JSON object (checked by `adoptHubFiles`).
|
|
1582
|
+
*/
|
|
1583
|
+
function classifyClonedHub(host, plan) {
|
|
1584
|
+
const located = locateHub(host, plan.directory);
|
|
1585
|
+
if (located.migration === "indeterminate") {
|
|
1586
|
+
throw new Error(`in the cloned ${plan.owner}/${plan.repository}, both ${WORKSPACE_MARKER_REL} and the legacy ${LEGACY_WORKSPACE_MARKER_REL} are present; launcher never merges them silently -- remove one before resuming`);
|
|
1587
|
+
}
|
|
1588
|
+
if (located.document !== undefined) {
|
|
1589
|
+
if (!sameOwner(located.document.owner, plan.owner)) {
|
|
1590
|
+
throw new Error(`the cloned ${plan.owner}/${plan.repository} carries a hub marker owned by "${located.document.owner}", not "${plan.owner}"; refusing to resume a hub for the wrong account`);
|
|
1591
|
+
}
|
|
1592
|
+
return located.migration === "legacy" ? { kind: "resume", migrateFrom: "legacy" } : { kind: "resume" };
|
|
1593
|
+
}
|
|
1594
|
+
if (!plan.advisorVersion) {
|
|
1595
|
+
throw new Error(`cannot read a public ${ADVISOR_PACKAGE} version from the npm registry; appointing the cloned ${plan.owner}/${plan.repository} pins it`);
|
|
1596
|
+
}
|
|
1597
|
+
if (!plan.integratorVersion) {
|
|
1598
|
+
throw new Error(`cannot read a public ${INTEGRATOR_PACKAGE} version from the npm registry; appointing the cloned ${plan.owner}/${plan.repository} pins it`);
|
|
1599
|
+
}
|
|
1600
|
+
if (looksLikeFoundry(host, plan.directory)) {
|
|
1601
|
+
throw new Error(`refusing to appoint the cloned ${plan.owner}/${plan.repository}: it looks like the Foundry supplier tree`);
|
|
1602
|
+
}
|
|
1603
|
+
const inventory = inspectInventory(host.readBytes(join(plan.directory, WORKSPACE_INVENTORY_REL)), plan.owner);
|
|
1604
|
+
if (inventory.status === "invalid") {
|
|
1605
|
+
throw new Error(`the cloned ${plan.owner}/${plan.repository} has an on-disk hub inventory that ${inventory.reason ?? "does not conform to the inventory contract"}; fix or remove ${WORKSPACE_INVENTORY_REL} in that repository, then run launcher from inside the checkout`);
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
kind: "appoint",
|
|
1609
|
+
plan: {
|
|
1610
|
+
action: "adopt",
|
|
1611
|
+
owner: plan.owner,
|
|
1612
|
+
repository: plan.repository,
|
|
1613
|
+
directory: plan.directory,
|
|
1614
|
+
advisorVersion: plan.advisorVersion,
|
|
1615
|
+
integratorVersion: plan.integratorVersion,
|
|
1616
|
+
},
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Applies a create, resume, or adopt plan through the host. Resume refreshes
|
|
1621
|
+
* composed skills and stale AGENTS.md guidance. A resume plan that clones
|
|
1622
|
+
* (`clone: true`, planned from an empty directory) is classified only after
|
|
1623
|
+
* the clone, by `classifyClonedHub`: a marked clone resumes (migrating a
|
|
1624
|
+
* legacy marker), an unmarked one is appointed through the adopt path, and
|
|
1625
|
+
* every refusal Launcher decides happens before anything is written into the clone (#1585).
|
|
1626
|
+
* Every path writes only into the hub checkout, never into an inventoried
|
|
1627
|
+
* repository beside it.
|
|
1628
|
+
*/
|
|
1078
1629
|
export function applyWorkspacePlan(host, plan, skeletonRoot, options = {}) {
|
|
1079
1630
|
const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
|
|
1080
1631
|
const skillCatalogueRoot = options.skillCatalogueRoot;
|
|
1081
1632
|
const contractPath = options.contractPath;
|
|
1082
1633
|
const liveLauncherVersion = options.liveLauncherVersion;
|
|
1083
1634
|
if (plan.action === "resume") {
|
|
1635
|
+
let migrateFrom = plan.migrateFrom;
|
|
1084
1636
|
if (plan.clone) {
|
|
1085
1637
|
requireZero(host.run("gh", ["repo", "clone", `${plan.owner}/${plan.repository}`, plan.directory]), "gh repo clone");
|
|
1638
|
+
const cloned = classifyClonedHub(host, plan);
|
|
1639
|
+
if (cloned.kind === "appoint")
|
|
1640
|
+
return applyAdoptPlan(host, cloned.plan, skeletonRoot, options);
|
|
1641
|
+
migrateFrom = cloned.migrateFrom;
|
|
1086
1642
|
}
|
|
1087
|
-
const migration =
|
|
1088
|
-
|
|
1643
|
+
const migration = migrateFrom === "legacy" ? migrateLegacyHubState(host, plan.directory) : undefined;
|
|
1644
|
+
// --repositories (#1179): write the chosen inventory before composing, so
|
|
1645
|
+
// this same run's health report lists the repositories just chosen.
|
|
1646
|
+
if (plan.chosenInventory?.kind === "write") {
|
|
1647
|
+
writeSkeletonFile(host, plan.directory, WORKSPACE_INVENTORY_REL, revalidatedDocument(plan.chosenInventory.document, plan.owner));
|
|
1648
|
+
}
|
|
1649
|
+
// Resume raises the hub's engine pins to live: a frozen Advisor is
|
|
1650
|
+
// bumped and Integrator is added, for each engine whose live version
|
|
1651
|
+
// observeWorkspace could read. The health report says what changed and
|
|
1652
|
+
// that the install, and a commit with the lockfile, come next.
|
|
1653
|
+
const enginePinChanges = mergeHubEnginePins(host, plan.directory, skeletonRoot, engineVersionsOf(plan), plan.owner, plan.repository, "resume");
|
|
1654
|
+
return finishHubApply(host, plan.directory, `resumed ${plan.owner}/${plan.repository} as the account hub\nOpen this folder in your coding agent. Advisor stays read-only until you approve a next action.${chosenInventoryNote(plan.chosenInventory)}`, launcherPackageRoot, plan.owner, plan.repository, engineVersionsOf(plan), skillCatalogueRoot, contractPath, liveLauncherVersion, migration, enginePinChanges);
|
|
1089
1655
|
}
|
|
1090
1656
|
if (plan.action === "create") {
|
|
1091
1657
|
copySkeleton(host, skeletonRoot, plan);
|
|
1092
1658
|
requireZero(host.run("gh", ["repo", "create", `${plan.owner}/${plan.repository}`, "--private", "--source", plan.directory, "--remote", "origin", "--push"], { cwd: plan.directory }), "gh repo create");
|
|
1093
|
-
return finishHubApply(host, plan.directory, `created ${plan.owner}/${plan.repository} as the account hub\nOpen this folder in your coding agent. Advisor stays read-only until you approve a next action.`, launcherPackageRoot, plan.owner, plan.repository, plan
|
|
1659
|
+
return finishHubApply(host, plan.directory, `created ${plan.owner}/${plan.repository} as the account hub\nOpen this folder in your coding agent. Advisor stays read-only until you approve a next action.`, launcherPackageRoot, plan.owner, plan.repository, engineVersionsOf(plan), skillCatalogueRoot, contractPath, liveLauncherVersion);
|
|
1094
1660
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1661
|
+
return applyAdoptPlan(host, plan, skeletonRoot, options);
|
|
1662
|
+
}
|
|
1663
|
+
/** Appoints an existing checkout as the hub, keeping its files: the adopt plan's apply, also reached from an unmarked clone (#1585). */
|
|
1664
|
+
function applyAdoptPlan(host, plan, skeletonRoot, options) {
|
|
1665
|
+
const launcherPackageRoot = options.launcherPackageRoot ?? resolve(skeletonRoot, "..");
|
|
1666
|
+
const { skillCatalogueRoot, contractPath, liveLauncherVersion } = options;
|
|
1667
|
+
const enginePinChanges = adoptHubFiles(host, skeletonRoot, plan);
|
|
1668
|
+
const inventoryReplacedNote = plan.replacesInvalidInventory === true
|
|
1669
|
+
? " The on-disk inventory failed schema validation; --inventory replaced it."
|
|
1670
|
+
: "";
|
|
1671
|
+
return finishHubApply(host, plan.directory, `appointed ${plan.owner}/${plan.repository} as the account hub\nExisting project files were kept. This hub inventories engagement; it does not install the catalogue into the repo.${inventoryReplacedNote}${chosenInventoryNote(plan.chosenInventory)}`, launcherPackageRoot, plan.owner, plan.repository, engineVersionsOf(plan), skillCatalogueRoot, contractPath, liveLauncherVersion, undefined, enginePinChanges);
|
|
1097
1672
|
}
|
|
1098
1673
|
export function launcherPackageRootFromModule(moduleUrl) {
|
|
1099
1674
|
return resolve(dirname(fileURLToPath(moduleUrl)), "..");
|