@mutmutco/codex-plugin 4.5.88 → 4.5.90
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/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +877 -147
- package/cli/package.json +1 -1
- package/package.json +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -965,6 +965,8 @@ var init_house_map = __esm({
|
|
|
965
965
|
train: "devops",
|
|
966
966
|
suite: "devops",
|
|
967
967
|
// #7264: cross-repo stage and served-version visibility
|
|
968
|
+
packages: "devops",
|
|
969
|
+
// #7833: consumer adoption of a verified package export
|
|
968
970
|
"wave land": "devops",
|
|
969
971
|
// the write-side serial merge train is shipping, not board observation
|
|
970
972
|
bootstrap: "devops",
|
|
@@ -4745,7 +4747,7 @@ function useColor() {
|
|
|
4745
4747
|
var program = new Command();
|
|
4746
4748
|
|
|
4747
4749
|
// src/command-composition.ts
|
|
4748
|
-
var
|
|
4750
|
+
var import_node_fs54 = require("node:fs");
|
|
4749
4751
|
init_clean_exit();
|
|
4750
4752
|
init_cli_shared();
|
|
4751
4753
|
|
|
@@ -5647,8 +5649,8 @@ function resolveVerbAliasShim(argv) {
|
|
|
5647
5649
|
}
|
|
5648
5650
|
|
|
5649
5651
|
// src/command-composition.ts
|
|
5650
|
-
var
|
|
5651
|
-
var
|
|
5652
|
+
var import_node_os24 = require("node:os");
|
|
5653
|
+
var import_node_path53 = require("node:path");
|
|
5652
5654
|
|
|
5653
5655
|
// src/board-read.ts
|
|
5654
5656
|
var import_node_fs13 = require("node:fs");
|
|
@@ -14524,6 +14526,24 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
|
|
|
14524
14526
|
await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
|
|
14525
14527
|
return { action: "created", enforcement };
|
|
14526
14528
|
}
|
|
14529
|
+
async function gateContextEnforced(repo, rest) {
|
|
14530
|
+
try {
|
|
14531
|
+
const list = await rest(`repos/${repo}/rulesets?includes_parents=true`);
|
|
14532
|
+
for (const entry of list) {
|
|
14533
|
+
if (entry.id == null) continue;
|
|
14534
|
+
const rs = await rest(`repos/${repo}/rulesets/${entry.id}`);
|
|
14535
|
+
if (rs.target !== "branch" || rs.enforcement !== "active") continue;
|
|
14536
|
+
const hit = (rs.rules ?? []).some((r) => r.type === "required_status_checks" && (r.parameters?.required_status_checks ?? []).some((c) => c.context === PRODUCT_GATE_CONTEXT));
|
|
14537
|
+
if (hit) return true;
|
|
14538
|
+
}
|
|
14539
|
+
return false;
|
|
14540
|
+
} catch {
|
|
14541
|
+
return false;
|
|
14542
|
+
}
|
|
14543
|
+
}
|
|
14544
|
+
async function propagateMayAutoMerge(repo, repoClass, rest) {
|
|
14545
|
+
return repoClass === "content" || gateContextEnforced(repo, rest);
|
|
14546
|
+
}
|
|
14527
14547
|
|
|
14528
14548
|
// src/bootstrap-apply.ts
|
|
14529
14549
|
var import_node_crypto5 = require("node:crypto");
|
|
@@ -17225,10 +17245,10 @@ var rollout_plan_default = {
|
|
|
17225
17245
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
17226
17246
|
},
|
|
17227
17247
|
baseline: {
|
|
17228
|
-
version: "4.5.
|
|
17229
|
-
tag: "v4.5.
|
|
17230
|
-
commit: "
|
|
17231
|
-
npm: "@mutmutco/cli@4.5.
|
|
17248
|
+
version: "4.5.90",
|
|
17249
|
+
tag: "v4.5.90",
|
|
17250
|
+
commit: "f096038f4a17",
|
|
17251
|
+
npm: "@mutmutco/cli@4.5.90"
|
|
17232
17252
|
},
|
|
17233
17253
|
exitCriterion: "fleet-n-of-n",
|
|
17234
17254
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17245,14 +17265,14 @@ var rollout_plan_default = {
|
|
|
17245
17265
|
repo: "mutmutco/mmi-hub",
|
|
17246
17266
|
role: "canary",
|
|
17247
17267
|
schedule: "train",
|
|
17248
|
-
v3Target: "v4.5.
|
|
17268
|
+
v3Target: "v4.5.90"
|
|
17249
17269
|
}
|
|
17250
17270
|
],
|
|
17251
17271
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
17252
17272
|
rollback: {
|
|
17253
17273
|
independent: true,
|
|
17254
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17255
|
-
v3Target: "v4.5.
|
|
17274
|
+
mechanism: "npm dist-tag latest -> 4.5.90 and redeploy the Hub Lambda from tag v4.5.90 (f096038f4a17); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17275
|
+
v3Target: "v4.5.90 (@mutmutco/cli@4.5.90, tag commit f096038f4a17 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17256
17276
|
}
|
|
17257
17277
|
},
|
|
17258
17278
|
{
|
|
@@ -28972,7 +28992,7 @@ var PRIMARY_GROUPS = [
|
|
|
28972
28992
|
["Setup and support", ["bootstrap", "secrets", "docs", "tests", "spawn", "dist"]],
|
|
28973
28993
|
["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
|
|
28974
28994
|
];
|
|
28975
|
-
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
|
|
28995
|
+
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin", "packages"]);
|
|
28976
28996
|
var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup"]);
|
|
28977
28997
|
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
|
|
28978
28998
|
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
@@ -29020,6 +29040,8 @@ var COMMAND_OWNERSHIP = {
|
|
|
29020
29040
|
train: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
29021
29041
|
suite: { module_owner: "cli/src/suite-commands.ts", consumer: "release-operator" },
|
|
29022
29042
|
// #7264
|
|
29043
|
+
packages: { module_owner: "cli/src/packages-adopt-export.ts", consumer: "agent-workflow" },
|
|
29044
|
+
// #7833
|
|
29023
29045
|
bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
|
|
29024
29046
|
secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
|
|
29025
29047
|
docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
|
|
@@ -30595,16 +30617,6 @@ function loadPortRegistry(path2) {
|
|
|
30595
30617
|
}
|
|
30596
30618
|
return out;
|
|
30597
30619
|
}
|
|
30598
|
-
function ensurePortRange(repo, path2) {
|
|
30599
|
-
const registry2 = loadPortRegistry(path2);
|
|
30600
|
-
const existing = registry2[repo];
|
|
30601
|
-
if (existing) return existing;
|
|
30602
|
-
const range = nextPortBlock(registry2);
|
|
30603
|
-
const raw = (0, import_node_fs25.existsSync)(path2) ? JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")) : {};
|
|
30604
|
-
raw[repo] = range;
|
|
30605
|
-
(0, import_node_fs25.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
30606
|
-
return range;
|
|
30607
|
-
}
|
|
30608
30620
|
function portCursorSeed(registry2) {
|
|
30609
30621
|
return nextPortBlock(registry2)[0];
|
|
30610
30622
|
}
|
|
@@ -30645,7 +30657,7 @@ function resolvePortRangeInfra(cwd, packageDir) {
|
|
|
30645
30657
|
}
|
|
30646
30658
|
return null;
|
|
30647
30659
|
}
|
|
30648
|
-
async function ensurePortRangeAtomic(repo, path2, allocate
|
|
30660
|
+
async function ensurePortRangeAtomic(repo, path2, allocate) {
|
|
30649
30661
|
const registry2 = loadPortRegistry(path2);
|
|
30650
30662
|
const existing = existingPortRange(repo, registry2);
|
|
30651
30663
|
if (existing) return { range: existing, source: "existing" };
|
|
@@ -30654,8 +30666,9 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
30654
30666
|
const range = await allocate(seed);
|
|
30655
30667
|
return { range, source: "ddb" };
|
|
30656
30668
|
} catch (e) {
|
|
30657
|
-
|
|
30658
|
-
|
|
30669
|
+
const why = e.message;
|
|
30670
|
+
const missing = /@aws-sdk\/[\w-]+/.exec(why)?.[0];
|
|
30671
|
+
throw new Error(`DDB port allocator failed for ${repo} \u2014 refusing to fall back to the frozen ${path2} (it would hand out a block the cursor already gave away). ` + (missing && /cannot find|not found|ERR_MODULE_NOT_FOUND/i.test(why) ? `Missing dependency ${missing}: run \`npm ci\` in ${(0, import_node_path25.dirname)(path2)}, then retry.` : "Check AWS credentials and that the registry table is reachable, then retry.") + ` Cause: ${why}`);
|
|
30659
30672
|
}
|
|
30660
30673
|
}
|
|
30661
30674
|
|
|
@@ -30683,7 +30696,13 @@ async function assignPersistedPortRange(repo, slug, reg, opts) {
|
|
|
30683
30696
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
30684
30697
|
return parsed.range;
|
|
30685
30698
|
};
|
|
30686
|
-
|
|
30699
|
+
let result;
|
|
30700
|
+
try {
|
|
30701
|
+
result = await ensurePortRangeAtomic(repo, path2, allocate);
|
|
30702
|
+
} catch (e) {
|
|
30703
|
+
return { ok: false, error: e.message };
|
|
30704
|
+
}
|
|
30705
|
+
const { range: [start, end], source } = result;
|
|
30687
30706
|
const write = await upsertProject(slug, { portRange: { start, end } }, reg);
|
|
30688
30707
|
if (!write.ok && source === "ddb") {
|
|
30689
30708
|
return {
|
|
@@ -34144,7 +34163,7 @@ async function prepareTargetedSeedBranch(repo, plan, target, client, gh2) {
|
|
|
34144
34163
|
}
|
|
34145
34164
|
await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
34146
34165
|
}
|
|
34147
|
-
async function
|
|
34166
|
+
async function verifyTargetedSeedPr(repo, number, plan, target, expected, client) {
|
|
34148
34167
|
const head = await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
34149
34168
|
const pr = await client.rest("GET", `repos/${repo}/pulls/${number}`);
|
|
34150
34169
|
if (pr?.state !== "open" || pr.head?.ref !== plan.branch || pr.head?.sha !== head || pr.head.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.base?.ref !== plan.baseBranch || pr.base.sha !== plan.baseSha || pr.base.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.changed_files !== 1) throw new Error("bootstrap apply: scoped seed PR identity or diff changed");
|
|
@@ -34159,7 +34178,7 @@ async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected,
|
|
|
34159
34178
|
if (file?.content !== expected) {
|
|
34160
34179
|
throw new Error("bootstrap apply: scoped seed content differs from this invocation");
|
|
34161
34180
|
}
|
|
34162
|
-
|
|
34181
|
+
return head;
|
|
34163
34182
|
}
|
|
34164
34183
|
|
|
34165
34184
|
// src/bootstrap-commands.ts
|
|
@@ -34843,8 +34862,6 @@ function registerBootstrapCommands(program3) {
|
|
|
34843
34862
|
let seedPrUrl;
|
|
34844
34863
|
const scopedHead = o.execute && scopedPlan?.branch && configGateAction?.action !== "skip" ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
34845
34864
|
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && configGateAction?.action !== "skip" && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
|
|
34846
|
-
await gh2(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
|
|
34847
|
-
});
|
|
34848
34865
|
const openPrs = await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
34849
34866
|
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
34850
34867
|
if (prDecision.action === "reuse") {
|
|
@@ -34870,22 +34887,13 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34870
34887
|
]), "bootstrap apply pr create");
|
|
34871
34888
|
seedPrUrl = created.url;
|
|
34872
34889
|
}
|
|
34873
|
-
|
|
34890
|
+
const number = Number(seedPrUrl.split("/").pop());
|
|
34874
34891
|
if (scopedPlan) {
|
|
34875
34892
|
if (scopedContent === void 0) throw new Error("bootstrap apply: scoped seed content is unresolved");
|
|
34876
|
-
const number = Number(seedPrUrl.split("/").pop());
|
|
34877
34893
|
if (!Number.isInteger(number) || number <= 0) throw new Error("bootstrap apply: scoped seed PR number is unreadable");
|
|
34878
|
-
await
|
|
34879
|
-
}
|
|
34880
|
-
|
|
34881
|
-
if (/already/i.test(message2)) return;
|
|
34882
|
-
if (/clean status|enablePullRequestAutoMerge/i.test(message2)) {
|
|
34883
|
-
autoMergeEnabled = false;
|
|
34884
|
-
return;
|
|
34885
|
-
}
|
|
34886
|
-
throw e;
|
|
34887
|
-
});
|
|
34888
|
-
applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli devops pr land <n> --repo ${repo})`);
|
|
34894
|
+
await verifyTargetedSeedPr(repo, number, scopedPlan, onlyTarget, scopedContent, controlClient);
|
|
34895
|
+
}
|
|
34896
|
+
applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; left open, never merged by apply \u2014 land it once its gate is green: mmi-cli devops pr land ${number} --repo ${repo})`);
|
|
34889
34897
|
}
|
|
34890
34898
|
if (o.execute && !onlyTarget) {
|
|
34891
34899
|
const desiredBranches = branchesForTrack(effectiveTrack);
|
|
@@ -34932,6 +34940,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34932
34940
|
applied.push(`has_wiki=false (failed: ${e.message})`);
|
|
34933
34941
|
}
|
|
34934
34942
|
}
|
|
34943
|
+
const deferredFailures = [];
|
|
34935
34944
|
if (o.execute && !onlyTarget && o.class === "deployable") {
|
|
34936
34945
|
try {
|
|
34937
34946
|
await gh2(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]);
|
|
@@ -34947,9 +34956,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34947
34956
|
const client = controlClient;
|
|
34948
34957
|
const gateFacts = await readBootstrapGitHubFacts(repo, effectiveTrack, client);
|
|
34949
34958
|
const rulesetBlockers = bootstrapControlWriteBlockers(gateFacts, true);
|
|
34950
|
-
if (rulesetBlockers.length) {
|
|
34951
|
-
return failGraceful(`bootstrap apply: product ruleset facts became unreadable before PUT; refusing write: ${rulesetBlockers.join("; ")}`);
|
|
34952
|
-
}
|
|
34959
|
+
if (rulesetBlockers.length) throw new Error(`facts became unreadable before PUT; refusing write: ${rulesetBlockers.join("; ")}`);
|
|
34953
34960
|
const activation = await activateProductRuleset(
|
|
34954
34961
|
repo,
|
|
34955
34962
|
stripRulesetComment(rulesetContent),
|
|
@@ -34960,7 +34967,8 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34960
34967
|
`product ruleset: ${activation.action} (${activation.enforcement}${activation.detail ? `; ${activation.detail}` : ""})` + (activation.enforcement === "active" ? "" : ` \u2014 ${PRODUCT_GATE_PATH2} has not emitted a proven-green context on ${baseBranch}, so it is NOT enforcing. Re-run bootstrap apply after the gate passes; ci reconcile --apply remains the supported fallback.`)
|
|
34961
34968
|
);
|
|
34962
34969
|
} catch (e) {
|
|
34963
|
-
|
|
34970
|
+
deferredFailures.push(`product ruleset not written (${e.message}) \u2014 re-run bootstrap apply once ${PRODUCT_GATE_PATH2} has a completed run`);
|
|
34971
|
+
applied.push(`product ruleset (REFUSED: ${e.message})`);
|
|
34964
34972
|
}
|
|
34965
34973
|
}
|
|
34966
34974
|
}
|
|
@@ -35047,6 +35055,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
35047
35055
|
);
|
|
35048
35056
|
} else {
|
|
35049
35057
|
applied.push(`portRange (failed: ${assigned.error})`);
|
|
35058
|
+
deferredFailures.push(`portRange not assigned: ${assigned.error}`);
|
|
35050
35059
|
}
|
|
35051
35060
|
}
|
|
35052
35061
|
} else {
|
|
@@ -35124,6 +35133,8 @@ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSp
|
|
|
35124
35133
|
LIVE apply to ${repo}:
|
|
35125
35134
|
${applied.join("\n ")}`);
|
|
35126
35135
|
}
|
|
35136
|
+
if (deferredFailures.length) return failGraceful(`bootstrap apply: incomplete \u2014 every other step ran:
|
|
35137
|
+
${deferredFailures.join("\n ")}`);
|
|
35127
35138
|
});
|
|
35128
35139
|
bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an org-owned whole file or declared Hub-managed block)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
|
|
35129
35140
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
@@ -35371,8 +35382,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
35371
35382
|
prUrl = created.url;
|
|
35372
35383
|
}
|
|
35373
35384
|
if (rec.wave !== 0) {
|
|
35374
|
-
|
|
35385
|
+
const enforced = await propagateMayAutoMerge(rec.repo, classOf(rec.repo), async (path2) => JSON.parse((await gh2(["api", path2])).stdout || "[]"));
|
|
35386
|
+
if (enforced) await gh2(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
|
|
35375
35387
|
});
|
|
35388
|
+
else console.error(`bootstrap propagate: ${rec.repo} does not enforce the gate \u2014 auto-merge not armed; land it with: mmi-cli devops pr land ${prUrl.split("/").pop()} --repo ${rec.repo}`);
|
|
35376
35389
|
}
|
|
35377
35390
|
rec.prUrl = prUrl;
|
|
35378
35391
|
}
|
|
@@ -37054,9 +37067,9 @@ A hotfix branch is cut from origin/main and carries ONLY the fixes --from names,
|
|
|
37054
37067
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
37055
37068
|
if (sources.length === 0) return { failures: [], skipped: [] };
|
|
37056
37069
|
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("node:fs/promises");
|
|
37057
|
-
const { tmpdir:
|
|
37058
|
-
const { join:
|
|
37059
|
-
const dir = await mkdtemp2(
|
|
37070
|
+
const { tmpdir: tmpdir12 } = await import("node:os");
|
|
37071
|
+
const { join: join47 } = await import("node:path");
|
|
37072
|
+
const dir = await mkdtemp2(join47(tmpdir12(), "mmi-hotfix-preflight-"));
|
|
37060
37073
|
const wt = { run: (file, args) => deps.run(file, file === "git" ? ["-C", dir, ...args] : args) };
|
|
37061
37074
|
const failures = [];
|
|
37062
37075
|
const skipped = [];
|
|
@@ -47591,16 +47604,732 @@ function suiteStatusDeps(github, registry2) {
|
|
|
47591
47604
|
};
|
|
47592
47605
|
}
|
|
47593
47606
|
|
|
47607
|
+
// src/packages-adopt-export.ts
|
|
47608
|
+
var import_node_child_process18 = require("node:child_process");
|
|
47609
|
+
var import_node_fs45 = require("node:fs");
|
|
47610
|
+
var import_node_os21 = require("node:os");
|
|
47611
|
+
var import_node_path44 = require("node:path");
|
|
47612
|
+
var import_node_util6 = require("node:util");
|
|
47613
|
+
init_cli_shared();
|
|
47614
|
+
|
|
47615
|
+
// ../scripts/package_export.py
|
|
47616
|
+
var package_export_default = `#!/usr/bin/env python3
|
|
47617
|
+
"""Export and offline verification of exact original package registry archives (#7805)."""
|
|
47618
|
+
import base64
|
|
47619
|
+
import binascii
|
|
47620
|
+
from datetime import datetime, timedelta, timezone
|
|
47621
|
+
import gzip
|
|
47622
|
+
import hashlib
|
|
47623
|
+
import hmac
|
|
47624
|
+
import io
|
|
47625
|
+
import json
|
|
47626
|
+
import os
|
|
47627
|
+
import pathlib
|
|
47628
|
+
import re
|
|
47629
|
+
import stat
|
|
47630
|
+
import struct
|
|
47631
|
+
import sys
|
|
47632
|
+
import tarfile
|
|
47633
|
+
import zipfile
|
|
47634
|
+
import zlib
|
|
47635
|
+
import urllib.error
|
|
47636
|
+
import urllib.request
|
|
47637
|
+
from urllib.parse import urlsplit
|
|
47638
|
+
|
|
47639
|
+
PACKAGES = ("@mutmutco/mmwork-ds", "@mutmutco/mmwork-screens")
|
|
47640
|
+
MAX_METADATA_BYTES = 2 * 1024 * 1024
|
|
47641
|
+
MAX_TARBALL_BYTES = 64 * 1024 * 1024
|
|
47642
|
+
MAX_ARTIFACT_BYTES = 160 * 1024 * 1024
|
|
47643
|
+
MAX_ZIP_DIRECTORY_BYTES = 8192
|
|
47644
|
+
MAX_UNPACKED_BYTES = 256 * 1024 * 1024
|
|
47645
|
+
MAX_MEMBERS = 10000 # Yielded entries; tarfile consumes PAX/GNU extension headers internally.
|
|
47646
|
+
MAX_PATH_LENGTH = 1024
|
|
47647
|
+
MAX_PATH_DEPTH = 32
|
|
47648
|
+
MAX_COMPONENT_LENGTH = 255
|
|
47649
|
+
WINDOWS_DEVICE = re.compile(r"con|conin\\$|conout\\$|prn|aux|nul|com[1-9]|lpt[1-9]", re.IGNORECASE)
|
|
47650
|
+
EXACT_VERSION = re.compile(
|
|
47651
|
+
r"(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)"
|
|
47652
|
+
r"(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"
|
|
47653
|
+
r"(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?"
|
|
47654
|
+
)
|
|
47655
|
+
|
|
47656
|
+
|
|
47657
|
+
def reject_constant(_):
|
|
47658
|
+
raise ValueError("nonstandard-json")
|
|
47659
|
+
|
|
47660
|
+
|
|
47661
|
+
def unique_object(pairs):
|
|
47662
|
+
result = {}
|
|
47663
|
+
for key, value in pairs:
|
|
47664
|
+
if key in result:
|
|
47665
|
+
raise ValueError("registry-metadata")
|
|
47666
|
+
result[key] = value
|
|
47667
|
+
return result
|
|
47668
|
+
|
|
47669
|
+
|
|
47670
|
+
def verify_artifact_digest(provider_digest, zip_bytes):
|
|
47671
|
+
"""Bind whole bytes to an independently trusted provider digest, not an embedded claim.
|
|
47672
|
+
|
|
47673
|
+
This does not authenticate the digest's source or validate ZIP members.
|
|
47674
|
+
"""
|
|
47675
|
+
if (type(zip_bytes) is not bytes or not 0 < len(zip_bytes) <= MAX_ARTIFACT_BYTES
|
|
47676
|
+
or type(provider_digest) is not str
|
|
47677
|
+
or not re.fullmatch(r"sha256:[0-9a-f]{64}", provider_digest)):
|
|
47678
|
+
raise ValueError("artifact-integrity")
|
|
47679
|
+
digest = hashlib.sha256(zip_bytes).hexdigest()
|
|
47680
|
+
if not hmac.compare_digest(provider_digest[7:], digest):
|
|
47681
|
+
raise ValueError("artifact-integrity")
|
|
47682
|
+
return {"artifactSha256": digest, "artifactBytes": len(zip_bytes)}
|
|
47683
|
+
|
|
47684
|
+
|
|
47685
|
+
def verify_artifact_receipt(provider_bytes, zip_bytes, expected, now):
|
|
47686
|
+
"""Check separately supplied provider metadata against trusted external selectors.
|
|
47687
|
+
|
|
47688
|
+
The caller must obtain metadata and selectors independently of the artifact.
|
|
47689
|
+
This cannot authenticate their origin or establish workflow/requester authority.
|
|
47690
|
+
Optional node_id, url, archive_download_url and updated_at are ignored, not validated.
|
|
47691
|
+
"""
|
|
47692
|
+
keys = {"artifactId", "artifactName", "repositoryId", "runId", "headSha", "headBranch", "digest"}
|
|
47693
|
+
try:
|
|
47694
|
+
if type(expected) is not dict:
|
|
47695
|
+
raise ValueError()
|
|
47696
|
+
expected = expected.copy()
|
|
47697
|
+
if set(expected) != keys:
|
|
47698
|
+
raise ValueError()
|
|
47699
|
+
if any(type(expected[key]) is not int or not 0 < expected[key] < (1 << 53)
|
|
47700
|
+
for key in ("artifactId", "repositoryId", "runId")):
|
|
47701
|
+
raise ValueError()
|
|
47702
|
+
if (type(expected["headSha"]) is not str or not re.fullmatch(r"[0-9a-f]{40}", expected["headSha"])
|
|
47703
|
+
or any(type(expected[key]) is not str or not 0 < len(expected[key]) <= 128
|
|
47704
|
+
or any(ord(char) < 33 or ord(char) > 126 for char in expected[key])
|
|
47705
|
+
for key in ("artifactName", "headBranch"))):
|
|
47706
|
+
raise ValueError()
|
|
47707
|
+
if type(provider_bytes) is not bytes or not 0 < len(provider_bytes) <= MAX_METADATA_BYTES:
|
|
47708
|
+
raise ValueError()
|
|
47709
|
+
provider = json.loads(provider_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
47710
|
+
parse_constant=reject_constant)
|
|
47711
|
+
required = {"id", "name", "size_in_bytes", "digest", "expired", "created_at", "expires_at", "workflow_run"}
|
|
47712
|
+
if (type(provider) is not dict or not required <= set(provider)
|
|
47713
|
+
or set(provider) - required - {"node_id", "url", "archive_download_url", "updated_at"}):
|
|
47714
|
+
raise ValueError()
|
|
47715
|
+
run = provider["workflow_run"]
|
|
47716
|
+
if (type(run) is not dict or set(run) != {"id", "repository_id", "head_repository_id", "head_sha", "head_branch"}
|
|
47717
|
+
or provider["expired"] is not False):
|
|
47718
|
+
raise ValueError()
|
|
47719
|
+
if type(now) is not datetime or now.utcoffset() != timedelta(0):
|
|
47720
|
+
raise ValueError()
|
|
47721
|
+
dates = []
|
|
47722
|
+
for key in ("created_at", "expires_at"):
|
|
47723
|
+
value = provider[key]
|
|
47724
|
+
if type(value) is not str or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z", value):
|
|
47725
|
+
raise ValueError()
|
|
47726
|
+
dates.append(datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc))
|
|
47727
|
+
created, expires = dates
|
|
47728
|
+
if not created <= now < expires or not timedelta(0) < expires - created <= timedelta(days=1):
|
|
47729
|
+
raise ValueError()
|
|
47730
|
+
for actual, wanted in [(provider["id"], expected["artifactId"]),
|
|
47731
|
+
(run["id"], expected["runId"]),
|
|
47732
|
+
(run["repository_id"], expected["repositoryId"]),
|
|
47733
|
+
(run["head_repository_id"], expected["repositoryId"])]:
|
|
47734
|
+
if type(actual) is not int or actual != wanted:
|
|
47735
|
+
raise ValueError()
|
|
47736
|
+
for actual, wanted in [(provider["name"], expected["artifactName"]),
|
|
47737
|
+
(provider["digest"], expected["digest"]),
|
|
47738
|
+
(run["head_sha"], expected["headSha"]),
|
|
47739
|
+
(run["head_branch"], expected["headBranch"])]:
|
|
47740
|
+
if type(actual) is not str or actual != wanted:
|
|
47741
|
+
raise ValueError()
|
|
47742
|
+
receipt = verify_artifact_digest(expected["digest"], zip_bytes)
|
|
47743
|
+
if type(provider["size_in_bytes"]) is not int or provider["size_in_bytes"] != len(zip_bytes):
|
|
47744
|
+
raise ValueError()
|
|
47745
|
+
except (ValueError, RecursionError):
|
|
47746
|
+
raise ValueError("artifact-receipt") from None
|
|
47747
|
+
return {**expected, **receipt}
|
|
47748
|
+
|
|
47749
|
+
|
|
47750
|
+
def inspect_artifact_zip(zip_bytes):
|
|
47751
|
+
"""Read five proposed export members in memory without extracting or executing.
|
|
47752
|
+
|
|
47753
|
+
Authenticate whole artifact bytes before this stage. Returned member bytes still
|
|
47754
|
+
require registry integrity checks; the manifest is untrusted, not an authority.
|
|
47755
|
+
Byte/output limits are not a total process-memory budget, including codec remainders.
|
|
47756
|
+
"""
|
|
47757
|
+
limits = {"manifest.json": MAX_METADATA_BYTES,
|
|
47758
|
+
"mmwork-ds.metadata.json": MAX_METADATA_BYTES, "mmwork-ds.tgz": MAX_TARBALL_BYTES,
|
|
47759
|
+
"mmwork-screens.metadata.json": MAX_METADATA_BYTES, "mmwork-screens.tgz": MAX_TARBALL_BYTES}
|
|
47760
|
+
try:
|
|
47761
|
+
if type(zip_bytes) is not bytes or not 0 < len(zip_bytes) <= MAX_ARTIFACT_BYTES:
|
|
47762
|
+
raise ValueError()
|
|
47763
|
+
stream = io.BytesIO(zip_bytes)
|
|
47764
|
+
# ZipFile allocates every central-directory entry before application checks.
|
|
47765
|
+
# Use stdlib's bounded EOCD reader and struct definitions for a strict profile audit.
|
|
47766
|
+
end = zipfile._EndRecData(stream)
|
|
47767
|
+
if (end is None or end[zipfile._ECD_SIGNATURE] != zipfile.stringEndArchive
|
|
47768
|
+
or end[zipfile._ECD_DISK_NUMBER] != 0
|
|
47769
|
+
or end[zipfile._ECD_DISK_START] != 0
|
|
47770
|
+
or end[zipfile._ECD_ENTRIES_THIS_DISK] != len(limits)
|
|
47771
|
+
or end[zipfile._ECD_ENTRIES_TOTAL] != len(limits)
|
|
47772
|
+
or not 0 < end[zipfile._ECD_SIZE] <= MAX_ZIP_DIRECTORY_BYTES
|
|
47773
|
+
or end[zipfile._ECD_COMMENT_SIZE] != 0
|
|
47774
|
+
or end[zipfile._ECD_LOCATION] + zipfile.sizeEndCentDir != len(zip_bytes)
|
|
47775
|
+
or end[zipfile._ECD_OFFSET] + end[zipfile._ECD_SIZE] != end[zipfile._ECD_LOCATION]):
|
|
47776
|
+
raise ValueError()
|
|
47777
|
+
directory = memoryview(zip_bytes)[end[zipfile._ECD_OFFSET]:end[zipfile._ECD_LOCATION]]
|
|
47778
|
+
allowed = {name.encode("ascii") for name in limits}
|
|
47779
|
+
seen = set()
|
|
47780
|
+
cursor = 0
|
|
47781
|
+
# ZipFile accepts short variable-field reads, so audit declared boundaries first.
|
|
47782
|
+
while cursor < len(directory):
|
|
47783
|
+
fields = struct.unpack_from(zipfile.structCentralDir, directory, cursor)
|
|
47784
|
+
start = cursor + zipfile.sizeCentralDir
|
|
47785
|
+
stop = start + fields[zipfile._CD_FILENAME_LENGTH]
|
|
47786
|
+
if (fields[zipfile._CD_SIGNATURE] != zipfile.stringCentralDir
|
|
47787
|
+
or fields[zipfile._CD_EXTRA_FIELD_LENGTH] != 0 or fields[zipfile._CD_COMMENT_LENGTH] != 0
|
|
47788
|
+
or stop > len(directory)):
|
|
47789
|
+
raise ValueError()
|
|
47790
|
+
name = bytes(directory[start:stop])
|
|
47791
|
+
if name not in allowed or name in seen:
|
|
47792
|
+
raise ValueError()
|
|
47793
|
+
seen.add(name)
|
|
47794
|
+
cursor = stop
|
|
47795
|
+
if cursor != len(directory) or seen != allowed:
|
|
47796
|
+
raise ValueError()
|
|
47797
|
+
with zipfile.ZipFile(stream, "r") as bundle:
|
|
47798
|
+
entries = bundle.infolist()
|
|
47799
|
+
if (bundle.start_dir != end[zipfile._ECD_OFFSET]
|
|
47800
|
+
or len(entries) != len(limits) or {entry.orig_filename for entry in entries} != set(limits)
|
|
47801
|
+
or min(entry.header_offset for entry in entries) != 0):
|
|
47802
|
+
raise ValueError()
|
|
47803
|
+
result = {}
|
|
47804
|
+
entries.sort(key=lambda entry: entry.header_offset)
|
|
47805
|
+
for index, entry in enumerate(entries):
|
|
47806
|
+
if (entry.filename != entry.orig_filename or entry.is_dir()
|
|
47807
|
+
or stat.S_IFMT(entry.external_attr >> 16) not in (0, stat.S_IFREG)
|
|
47808
|
+
or entry.external_attr & 0x10 or entry.volume != 0
|
|
47809
|
+
or entry.flag_bits & ~(0x8 | 0x800)
|
|
47810
|
+
or entry.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED)
|
|
47811
|
+
or entry.comment or entry.extra
|
|
47812
|
+
or not 0 < entry.file_size <= limits[entry.filename]
|
|
47813
|
+
or not 0 < entry.compress_size <= MAX_ARTIFACT_BYTES):
|
|
47814
|
+
raise ValueError()
|
|
47815
|
+
header = entry.FileHeader(zip64=False)
|
|
47816
|
+
start = entry.header_offset + len(header)
|
|
47817
|
+
data_end = start + entry.compress_size
|
|
47818
|
+
next_offset = entries[index + 1].header_offset if index + 1 < len(entries) else bundle.start_dir
|
|
47819
|
+
if zip_bytes[entry.header_offset:start] != header or data_end > next_offset:
|
|
47820
|
+
raise ValueError()
|
|
47821
|
+
span = next_offset - data_end
|
|
47822
|
+
if span not in ((12, 16) if entry.flag_bits & 0x8 else (0,)):
|
|
47823
|
+
raise ValueError()
|
|
47824
|
+
descriptor = zip_bytes[data_end:next_offset]
|
|
47825
|
+
if entry.flag_bits & 0x8:
|
|
47826
|
+
fields = struct.pack("<III", entry.CRC, entry.compress_size, entry.file_size)
|
|
47827
|
+
if descriptor not in (fields, b"PK\\x07\\x08" + fields):
|
|
47828
|
+
raise ValueError()
|
|
47829
|
+
elif descriptor:
|
|
47830
|
+
raise ValueError()
|
|
47831
|
+
compressed = memoryview(zip_bytes)[start:data_end]
|
|
47832
|
+
if entry.compress_type == zipfile.ZIP_STORED:
|
|
47833
|
+
if entry.compress_size != entry.file_size:
|
|
47834
|
+
raise ValueError()
|
|
47835
|
+
payload = bytes(compressed)
|
|
47836
|
+
else:
|
|
47837
|
+
codec = zlib.decompressobj(wbits=-15)
|
|
47838
|
+
payload = codec.decompress(compressed, entry.file_size + 1)
|
|
47839
|
+
# ZipExtFile clips to the declared size and may ignore trailing data.
|
|
47840
|
+
if not codec.eof or codec.unused_data or codec.unconsumed_tail:
|
|
47841
|
+
raise ValueError()
|
|
47842
|
+
if len(payload) != entry.file_size or zlib.crc32(payload) != entry.CRC:
|
|
47843
|
+
raise ValueError()
|
|
47844
|
+
result[entry.filename] = payload
|
|
47845
|
+
return result
|
|
47846
|
+
except (ValueError, OSError, EOFError, NotImplementedError, zipfile.BadZipFile, zlib.error, struct.error):
|
|
47847
|
+
raise ValueError("artifact-members") from None
|
|
47848
|
+
|
|
47849
|
+
|
|
47850
|
+
def inspect_archive(name, version, tarball_bytes):
|
|
47851
|
+
seen = set()
|
|
47852
|
+
files = set()
|
|
47853
|
+
directories = set()
|
|
47854
|
+
package = None
|
|
47855
|
+
end = 0
|
|
47856
|
+
try:
|
|
47857
|
+
with gzip.GzipFile(fileobj=io.BytesIO(tarball_bytes)) as compressed:
|
|
47858
|
+
unpacked = compressed.read(MAX_UNPACKED_BYTES + 1)
|
|
47859
|
+
if len(unpacked) > MAX_UNPACKED_BYTES:
|
|
47860
|
+
raise ValueError()
|
|
47861
|
+
with tarfile.open(fileobj=io.BytesIO(unpacked), mode="r:") as archive:
|
|
47862
|
+
for index, entry in enumerate(archive):
|
|
47863
|
+
if (index >= MAX_MEMBERS or entry.size < 0 or entry.size > MAX_UNPACKED_BYTES
|
|
47864
|
+
or (entry.isdir() and entry.size != 0)):
|
|
47865
|
+
raise ValueError()
|
|
47866
|
+
end = entry.offset_data + ((entry.size + 511) // 512) * 512
|
|
47867
|
+
# Bound names before splitting or allocating retained ancestor prefixes.
|
|
47868
|
+
if len(entry.name) > MAX_PATH_LENGTH:
|
|
47869
|
+
raise ValueError()
|
|
47870
|
+
path = entry.name.rstrip("/") if entry.isdir() else entry.name
|
|
47871
|
+
if path.count("/") >= MAX_PATH_DEPTH:
|
|
47872
|
+
raise ValueError()
|
|
47873
|
+
parts = path.split("/")
|
|
47874
|
+
if (not (entry.isreg() or entry.isdir()) or entry.issparse()
|
|
47875
|
+
or parts[0] != "package" or "\\\\" in path or ":" in path
|
|
47876
|
+
or any(ord(char) < 32 or ord(char) > 126 for char in path)
|
|
47877
|
+
or any(len(part) > MAX_COMPONENT_LENGTH or part in ("", ".", "..")
|
|
47878
|
+
or part.rstrip(". ") != part for part in parts)
|
|
47879
|
+
or path.lower() in seen):
|
|
47880
|
+
raise ValueError()
|
|
47881
|
+
seen.add(path.lower())
|
|
47882
|
+
folded = path.lower()
|
|
47883
|
+
parents = ["/".join(parts[:i]).lower() for i in range(1, len(parts))]
|
|
47884
|
+
if any(parent in files for parent in parents) or (entry.isreg() and folded in directories):
|
|
47885
|
+
raise ValueError()
|
|
47886
|
+
directories.update(parents)
|
|
47887
|
+
if entry.isdir():
|
|
47888
|
+
directories.add(folded)
|
|
47889
|
+
else:
|
|
47890
|
+
files.add(folded)
|
|
47891
|
+
for part in parts:
|
|
47892
|
+
lower = part.lower()
|
|
47893
|
+
if (WINDOWS_DEVICE.fullmatch(part.split(".", 1)[0].rstrip(" "))
|
|
47894
|
+
or any(char in part for char in '<>"|?*')
|
|
47895
|
+
or lower in (".npmrc", ".netrc", ".pypirc", ".git", ".gitconfig", ".git-credentials", ".yarnrc", ".yarnrc.yml")
|
|
47896
|
+
or lower.startswith(".env") or lower.endswith((".env", ".pem", ".key", ".p12", ".pfx", ".log"))):
|
|
47897
|
+
raise ValueError()
|
|
47898
|
+
if path == "package/package.json":
|
|
47899
|
+
if not entry.isreg() or entry.size > MAX_METADATA_BYTES:
|
|
47900
|
+
raise ValueError()
|
|
47901
|
+
with archive.extractfile(entry) as file:
|
|
47902
|
+
package = json.loads(file.read().decode("utf-8"), object_pairs_hook=unique_object,
|
|
47903
|
+
parse_constant=reject_constant)
|
|
47904
|
+
if len(unpacked) - end < 1024 or any(unpacked[end:]):
|
|
47905
|
+
raise ValueError()
|
|
47906
|
+
if type(package) is not dict or package.get("name") != name or package.get("version") != version:
|
|
47907
|
+
raise ValueError()
|
|
47908
|
+
except (ValueError, tarfile.TarError, OSError, EOFError, RecursionError, zlib.error):
|
|
47909
|
+
raise ValueError("archive-members") from None
|
|
47910
|
+
|
|
47911
|
+
|
|
47912
|
+
def check_exact_version(version):
|
|
47913
|
+
match = EXACT_VERSION.fullmatch(version) if type(version) is str and len(version) <= 128 else None
|
|
47914
|
+
if not match or any(
|
|
47915
|
+
part.isdigit() and len(part) > 1 and part.startswith("0")
|
|
47916
|
+
for part in (match[4] or "").split(".")
|
|
47917
|
+
):
|
|
47918
|
+
raise ValueError("package-identity")
|
|
47919
|
+
|
|
47920
|
+
|
|
47921
|
+
def check_tarball_url(url):
|
|
47922
|
+
# Accept literal paths only: never reinterpret percent escapes or empty delimiters.
|
|
47923
|
+
if (type(url) is not str or any(ord(char) < 33 or ord(char) > 126 for char in url)
|
|
47924
|
+
or any(char in url for char in "\\\\%?#")):
|
|
47925
|
+
raise ValueError("registry-url")
|
|
47926
|
+
try:
|
|
47927
|
+
location = urlsplit(url)
|
|
47928
|
+
except ValueError:
|
|
47929
|
+
raise ValueError("registry-url") from None
|
|
47930
|
+
if (location.scheme != "https" or location.netloc != "npm.pkg.github.com"
|
|
47931
|
+
or not location.path.startswith("/download/")
|
|
47932
|
+
or any(part in ("", ".", "..") for part in location.path.split("/")[1:])):
|
|
47933
|
+
raise ValueError("registry-url")
|
|
47934
|
+
|
|
47935
|
+
|
|
47936
|
+
def verify_registry_archive(name, version, metadata_bytes, tarball_bytes):
|
|
47937
|
+
if (type(metadata_bytes) is not bytes or not 0 < len(metadata_bytes) <= MAX_METADATA_BYTES
|
|
47938
|
+
or type(tarball_bytes) is not bytes or not 0 < len(tarball_bytes) <= MAX_TARBALL_BYTES):
|
|
47939
|
+
raise ValueError("byte-limit")
|
|
47940
|
+
if type(name) is not str or name not in PACKAGES:
|
|
47941
|
+
raise ValueError("package-identity")
|
|
47942
|
+
check_exact_version(version)
|
|
47943
|
+
try:
|
|
47944
|
+
metadata = json.loads(metadata_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
47945
|
+
parse_constant=reject_constant)
|
|
47946
|
+
if type(metadata) is not dict or type(metadata.get("dist")) is not dict:
|
|
47947
|
+
raise ValueError()
|
|
47948
|
+
full_integrity = metadata["dist"].get("integrity")
|
|
47949
|
+
if type(full_integrity) is not str or not full_integrity.startswith("sha512-"):
|
|
47950
|
+
raise ValueError()
|
|
47951
|
+
registry_digest = base64.b64decode(full_integrity[7:], validate=True)
|
|
47952
|
+
if (len(registry_digest) != hashlib.sha512().digest_size
|
|
47953
|
+
or base64.b64encode(registry_digest).decode() != full_integrity[7:]):
|
|
47954
|
+
raise ValueError()
|
|
47955
|
+
except (ValueError, binascii.Error, RecursionError):
|
|
47956
|
+
raise ValueError("registry-metadata") from None
|
|
47957
|
+
if metadata.get("name") != name or metadata.get("version") != version:
|
|
47958
|
+
raise ValueError("package-identity")
|
|
47959
|
+
check_tarball_url(metadata["dist"].get("tarball"))
|
|
47960
|
+
if not hmac.compare_digest(registry_digest, hashlib.sha512(tarball_bytes).digest()):
|
|
47961
|
+
raise ValueError("integrity-mismatch")
|
|
47962
|
+
inspect_archive(name, version, tarball_bytes)
|
|
47963
|
+
return {
|
|
47964
|
+
"name": name,
|
|
47965
|
+
"version": version,
|
|
47966
|
+
"integrity": metadata["dist"]["integrity"],
|
|
47967
|
+
"metadataSha256": hashlib.sha256(metadata_bytes).hexdigest(),
|
|
47968
|
+
"tarballSha256": hashlib.sha256(tarball_bytes).hexdigest(),
|
|
47969
|
+
"metadataBytes": len(metadata_bytes),
|
|
47970
|
+
"tarballBytes": len(tarball_bytes),
|
|
47971
|
+
}
|
|
47972
|
+
|
|
47973
|
+
|
|
47974
|
+
# --- Export (runs only inside .github/workflows/package-export.yml) ---------------------------
|
|
47975
|
+
TRUSTED_REPOSITORY = "mutmutco/MMI-Hub"
|
|
47976
|
+
TRUSTED_WORKFLOW = ".github/workflows/package-export.yml"
|
|
47977
|
+
TRUSTED_REF = "refs/heads/development"
|
|
47978
|
+
REGISTRY = "https://npm.pkg.github.com"
|
|
47979
|
+
MAX_PACKUMENT_BYTES = 16 * 1024 * 1024
|
|
47980
|
+
VERSION_INPUTS = {"@mutmutco/mmwork-ds": "DS_VERSION", "@mutmutco/mmwork-screens": "SCREENS_VERSION"}
|
|
47981
|
+
|
|
47982
|
+
|
|
47983
|
+
def dispatch_identity(env):
|
|
47984
|
+
"""Refuse any run that is not this workflow, dispatched on the trusted ref of the trusted repo."""
|
|
47985
|
+
try:
|
|
47986
|
+
identity = {key: env[name] for key, name in [
|
|
47987
|
+
("repository", "GITHUB_REPOSITORY"), ("repositoryId", "GITHUB_REPOSITORY_ID"),
|
|
47988
|
+
("workflowRef", "GITHUB_WORKFLOW_REF"), ("ref", "GITHUB_REF"), ("headSha", "GITHUB_SHA"),
|
|
47989
|
+
("runId", "GITHUB_RUN_ID"), ("runAttempt", "GITHUB_RUN_ATTEMPT"),
|
|
47990
|
+
("event", "GITHUB_EVENT_NAME"), ("actor", "GITHUB_ACTOR"),
|
|
47991
|
+
("triggeringActor", "GITHUB_TRIGGERING_ACTOR")]}
|
|
47992
|
+
except KeyError:
|
|
47993
|
+
raise ValueError("dispatch-identity") from None
|
|
47994
|
+
if (identity["repository"] != TRUSTED_REPOSITORY or identity["ref"] != TRUSTED_REF
|
|
47995
|
+
or identity["event"] != "workflow_dispatch"
|
|
47996
|
+
or identity["workflowRef"] != f"{TRUSTED_REPOSITORY}/{TRUSTED_WORKFLOW}@{TRUSTED_REF}"
|
|
47997
|
+
or not re.fullmatch(r"[0-9a-f]{40}", identity["headSha"])
|
|
47998
|
+
or not all(re.fullmatch(r"[1-9][0-9]{0,15}", identity[key])
|
|
47999
|
+
for key in ("repositoryId", "runId", "runAttempt"))
|
|
48000
|
+
or not all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]{0,38}(\\[bot\\])?", identity[key])
|
|
48001
|
+
for key in ("actor", "triggeringActor"))):
|
|
48002
|
+
raise ValueError("dispatch-identity")
|
|
48003
|
+
return identity
|
|
48004
|
+
|
|
48005
|
+
|
|
48006
|
+
class HttpsOnlyRedirect(urllib.request.HTTPRedirectHandler):
|
|
48007
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
48008
|
+
if urlsplit(newurl).scheme != "https":
|
|
48009
|
+
raise ValueError("registry-url")
|
|
48010
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
48011
|
+
|
|
48012
|
+
|
|
48013
|
+
def default_opener():
|
|
48014
|
+
# No proxy handler: the token goes only to the registry host named in the request.
|
|
48015
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}), HttpsOnlyRedirect())
|
|
48016
|
+
|
|
48017
|
+
|
|
48018
|
+
def fetch(opener, url, token, limit):
|
|
48019
|
+
request = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
48020
|
+
# Unredirected: the registry's redirect to blob storage never carries the token.
|
|
48021
|
+
request.add_unredirected_header("Authorization", f"Bearer {token}")
|
|
48022
|
+
with opener.open(request, timeout=60) as response:
|
|
48023
|
+
body = response.read(limit + 1)
|
|
48024
|
+
if len(body) > limit or not body:
|
|
48025
|
+
raise ValueError("byte-limit")
|
|
48026
|
+
return body
|
|
48027
|
+
|
|
48028
|
+
|
|
48029
|
+
def export_package(opener, token, name, version):
|
|
48030
|
+
check_exact_version(version)
|
|
48031
|
+
try:
|
|
48032
|
+
packument_bytes = fetch(opener, f"{REGISTRY}/{name.replace('/', '%2f')}", token, MAX_PACKUMENT_BYTES)
|
|
48033
|
+
except urllib.error.HTTPError as error:
|
|
48034
|
+
# Name the package and the registry's own short reason (body only; no request headers).
|
|
48035
|
+
reason = error.read(300).decode("utf-8", "replace").replace(token, "***")
|
|
48036
|
+
raise ValueError(f"registry {name}: HTTP {error.code} {reason}") from None
|
|
48037
|
+
try:
|
|
48038
|
+
packument = json.loads(packument_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
48039
|
+
parse_constant=reject_constant)
|
|
48040
|
+
if (type(packument) is not dict or packument.get("name") != name
|
|
48041
|
+
or type(packument.get("versions")) is not dict):
|
|
48042
|
+
raise ValueError()
|
|
48043
|
+
document = packument["versions"][version]
|
|
48044
|
+
if type(document) is not dict or type(document.get("dist")) is not dict:
|
|
48045
|
+
raise ValueError()
|
|
48046
|
+
except (ValueError, KeyError, RecursionError):
|
|
48047
|
+
raise ValueError("registry-metadata") from None
|
|
48048
|
+
check_tarball_url(document["dist"].get("tarball"))
|
|
48049
|
+
metadata_bytes = json.dumps(document, sort_keys=True, separators=(",", ":")).encode()
|
|
48050
|
+
tarball_bytes = fetch(opener, document["dist"]["tarball"], token, MAX_TARBALL_BYTES)
|
|
48051
|
+
receipt = verify_registry_archive(name, version, metadata_bytes, tarball_bytes)
|
|
48052
|
+
receipt["packumentSha256"] = hashlib.sha256(packument_bytes).hexdigest()
|
|
48053
|
+
return metadata_bytes, tarball_bytes, receipt
|
|
48054
|
+
|
|
48055
|
+
|
|
48056
|
+
def export(env, out_dir, opener=None):
|
|
48057
|
+
"""Write exactly the five artifact members into a new directory. Nothing else is written."""
|
|
48058
|
+
identity = dispatch_identity(env)
|
|
48059
|
+
versions = {name: env.get(variable) for name, variable in VERSION_INPUTS.items()}
|
|
48060
|
+
for version in versions.values():
|
|
48061
|
+
check_exact_version(version)
|
|
48062
|
+
token = env.get("GITHUB_PACKAGES_TOKEN")
|
|
48063
|
+
if type(token) is not str or not token.strip():
|
|
48064
|
+
raise ValueError("registry-token")
|
|
48065
|
+
opener = opener or default_opener()
|
|
48066
|
+
files = {}
|
|
48067
|
+
receipts = []
|
|
48068
|
+
for name in PACKAGES:
|
|
48069
|
+
metadata_bytes, tarball_bytes, receipt = export_package(opener, token, name, versions[name])
|
|
48070
|
+
short = name.split("/")[1]
|
|
48071
|
+
files[f"{short}.metadata.json"] = metadata_bytes
|
|
48072
|
+
files[f"{short}.tgz"] = tarball_bytes
|
|
48073
|
+
receipts.append(receipt)
|
|
48074
|
+
manifest = {"schema": 1, "registry": REGISTRY, "source": identity, "packages": receipts}
|
|
48075
|
+
files["manifest.json"] = json.dumps(manifest, sort_keys=True, indent=2).encode()
|
|
48076
|
+
if any(token.encode() in payload for payload in files.values()):
|
|
48077
|
+
raise ValueError("registry-token")
|
|
48078
|
+
out = pathlib.Path(out_dir)
|
|
48079
|
+
out.mkdir(parents=True, exist_ok=False)
|
|
48080
|
+
for member, payload in files.items():
|
|
48081
|
+
(out / member).write_bytes(payload)
|
|
48082
|
+
return manifest
|
|
48083
|
+
|
|
48084
|
+
|
|
48085
|
+
def verify_export(provider_bytes, zip_bytes, expected, versions, now):
|
|
48086
|
+
"""Consumer side: bind the artifact to external selectors, then re-verify every package offline.
|
|
48087
|
+
|
|
48088
|
+
versions maps each allowlisted package to the exact version the consumer asked for. The
|
|
48089
|
+
manifest inside the artifact is checked against recomputed receipts, never trusted.
|
|
48090
|
+
"""
|
|
48091
|
+
receipt = verify_artifact_receipt(provider_bytes, zip_bytes, expected, now)
|
|
48092
|
+
members = inspect_artifact_zip(zip_bytes)
|
|
48093
|
+
if type(versions) is not dict or set(versions) != set(PACKAGES):
|
|
48094
|
+
raise ValueError("package-identity")
|
|
48095
|
+
packages = []
|
|
48096
|
+
for name in PACKAGES:
|
|
48097
|
+
short = name.split("/")[1]
|
|
48098
|
+
packages.append(verify_registry_archive(name, versions[name], members[f"{short}.metadata.json"],
|
|
48099
|
+
members[f"{short}.tgz"]))
|
|
48100
|
+
try:
|
|
48101
|
+
manifest = json.loads(members["manifest.json"].decode("utf-8"), object_pairs_hook=unique_object,
|
|
48102
|
+
parse_constant=reject_constant)
|
|
48103
|
+
source = manifest["source"]
|
|
48104
|
+
claimed = manifest["packages"]
|
|
48105
|
+
if (manifest.get("registry") != REGISTRY or type(claimed) is not list or len(claimed) != len(packages)
|
|
48106
|
+
or any(type(entry) is not dict or {key: entry.get(key) for key in package} != package
|
|
48107
|
+
for entry, package in zip(claimed, packages))
|
|
48108
|
+
or source.get("repository") != TRUSTED_REPOSITORY
|
|
48109
|
+
or source.get("workflowRef") != f"{TRUSTED_REPOSITORY}/{TRUSTED_WORKFLOW}@{TRUSTED_REF}"
|
|
48110
|
+
or source.get("headSha") != expected["headSha"]
|
|
48111
|
+
or source.get("runId") != str(expected["runId"])
|
|
48112
|
+
or source.get("repositoryId") != str(expected["repositoryId"])):
|
|
48113
|
+
raise ValueError()
|
|
48114
|
+
except (ValueError, KeyError, TypeError, AttributeError, RecursionError):
|
|
48115
|
+
raise ValueError("artifact-manifest") from None
|
|
48116
|
+
return {"artifact": receipt, "packages": packages}
|
|
48117
|
+
|
|
48118
|
+
|
|
48119
|
+
def verify_dir(work_dir, ds_version, screens_version):
|
|
48120
|
+
"""Consumer CLI entry (#7833): verify_export over files a caller fetched into work_dir.
|
|
48121
|
+
|
|
48122
|
+
Reads provider.json, artifact.zip and expected.json; writes only the four verified package
|
|
48123
|
+
members into work_dir/verified (a new directory). The caller acts only on those bytes.
|
|
48124
|
+
"""
|
|
48125
|
+
work = pathlib.Path(work_dir)
|
|
48126
|
+
result = verify_export((work / "provider.json").read_bytes(), (work / "artifact.zip").read_bytes(),
|
|
48127
|
+
json.loads((work / "expected.json").read_text("utf-8")),
|
|
48128
|
+
{PACKAGES[0]: ds_version, PACKAGES[1]: screens_version}, datetime.now(timezone.utc))
|
|
48129
|
+
members = inspect_artifact_zip((work / "artifact.zip").read_bytes())
|
|
48130
|
+
out = work / "verified"
|
|
48131
|
+
out.mkdir(exist_ok=False)
|
|
48132
|
+
for member, payload in members.items():
|
|
48133
|
+
if member != "manifest.json":
|
|
48134
|
+
(out / member).write_bytes(payload)
|
|
48135
|
+
return result
|
|
48136
|
+
|
|
48137
|
+
|
|
48138
|
+
if __name__ == "__main__":
|
|
48139
|
+
if len(sys.argv) == 5 and sys.argv[1] == "verify":
|
|
48140
|
+
try:
|
|
48141
|
+
print(json.dumps(verify_dir(*sys.argv[2:]), sort_keys=True, indent=2))
|
|
48142
|
+
except (ValueError, OSError) as error:
|
|
48143
|
+
sys.exit(f"package-export verify refused: {error}")
|
|
48144
|
+
sys.exit(0)
|
|
48145
|
+
if len(sys.argv) != 3 or sys.argv[1] != "export":
|
|
48146
|
+
sys.exit("usage: package_export.py export <new-output-dir> | verify <work-dir> <ds-version> <screens-version>")
|
|
48147
|
+
try:
|
|
48148
|
+
result = export(os.environ, sys.argv[2])
|
|
48149
|
+
except (ValueError, OSError, urllib.error.URLError) as error:
|
|
48150
|
+
# Error text carries no request headers: HTTPError renders only the status line.
|
|
48151
|
+
sys.exit(f"package-export refused: {error}")
|
|
48152
|
+
print(json.dumps(result, sort_keys=True, indent=2))
|
|
48153
|
+
`;
|
|
48154
|
+
|
|
48155
|
+
// src/packages-adopt-export.ts
|
|
48156
|
+
var EXPORT_REPO = "mutmutco/MMI-Hub";
|
|
48157
|
+
var EXPORT_WORKFLOW = ".github/workflows/package-export.yml";
|
|
48158
|
+
var PACKAGES = [
|
|
48159
|
+
{ name: "@mutmutco/mmwork-ds", short: "mmwork-ds" },
|
|
48160
|
+
{ name: "@mutmutco/mmwork-screens", short: "mmwork-screens" }
|
|
48161
|
+
];
|
|
48162
|
+
var SPEC_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
48163
|
+
var SHAPE_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies", "peerDependenciesMeta", "bin", "os", "cpu", "hasInstallScript"];
|
|
48164
|
+
var stable = (value) => JSON.stringify(value, (_key, inner) => inner && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => a.localeCompare(b))) : inner);
|
|
48165
|
+
function lockShape(meta, field) {
|
|
48166
|
+
if (field === "hasInstallScript") {
|
|
48167
|
+
const scripts = meta.scripts ?? {};
|
|
48168
|
+
return meta.hasInstallScript === true || Boolean(scripts.preinstall || scripts.install || scripts.postinstall) || void 0;
|
|
48169
|
+
}
|
|
48170
|
+
if (field === "bin" && meta.bin && typeof meta.bin === "object") {
|
|
48171
|
+
return Object.fromEntries(Object.entries(meta.bin).map(([k, v]) => [k, String(v).replace(/^(\.\/)+/, "")]));
|
|
48172
|
+
}
|
|
48173
|
+
const value = meta[field];
|
|
48174
|
+
return value && typeof value === "object" && Object.keys(value).length === 0 ? void 0 : value;
|
|
48175
|
+
}
|
|
48176
|
+
function formatLike(original, value) {
|
|
48177
|
+
const indent = /\n([ \t]+)"/.exec(original)?.[1] ?? " ";
|
|
48178
|
+
const text = JSON.stringify(value, null, indent) + "\n";
|
|
48179
|
+
return original.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
|
|
48180
|
+
}
|
|
48181
|
+
function planAdoption(packageText, lockText, metadata) {
|
|
48182
|
+
const pkg = JSON.parse(packageText);
|
|
48183
|
+
const lock = JSON.parse(lockText);
|
|
48184
|
+
const root = lock.packages?.[""];
|
|
48185
|
+
if (lock.lockfileVersion < 2 || !root) throw new Error("package-lock.json must be lockfileVersion 2 or 3");
|
|
48186
|
+
const result = { adopted: [], skipped: [] };
|
|
48187
|
+
for (const { name } of PACKAGES) {
|
|
48188
|
+
const section = SPEC_SECTIONS.find((key) => pkg[key]?.[name] !== void 0);
|
|
48189
|
+
if (!section) {
|
|
48190
|
+
result.skipped.push(name);
|
|
48191
|
+
continue;
|
|
48192
|
+
}
|
|
48193
|
+
const meta = metadata[name];
|
|
48194
|
+
const entry = lock.packages[`node_modules/${name}`];
|
|
48195
|
+
if (!entry) throw new Error(`${name} is in package.json but not in package-lock.json; run a normal registry install first`);
|
|
48196
|
+
const nested = Object.keys(lock.packages).filter((key) => key.endsWith(`/node_modules/${name}`));
|
|
48197
|
+
if (nested.length) throw new Error(`${name} has nested lock copies (${nested.join(", ")}); adopt-export edits only the top-level entry`);
|
|
48198
|
+
const changed = SHAPE_FIELDS.filter((field) => stable(field === "hasInstallScript" ? entry[field] || void 0 : entry[field]) !== stable(lockShape(meta, field)));
|
|
48199
|
+
if (changed.length) {
|
|
48200
|
+
throw new Error(`${name} ${entry.version} -> ${meta.version} changes ${changed.join(", ")}; that needs the registry, so adopt it through a normal install in CI instead`);
|
|
48201
|
+
}
|
|
48202
|
+
result.adopted.push({ name, from: entry.version, to: meta.version, resolved: meta.dist.tarball, integrity: meta.dist.integrity });
|
|
48203
|
+
entry.version = meta.version;
|
|
48204
|
+
entry.resolved = meta.dist.tarball;
|
|
48205
|
+
entry.integrity = meta.dist.integrity;
|
|
48206
|
+
for (const field of ["license", "engines"]) {
|
|
48207
|
+
if (meta[field] === void 0) delete entry[field];
|
|
48208
|
+
else entry[field] = meta[field];
|
|
48209
|
+
}
|
|
48210
|
+
pkg[section][name] = meta.version;
|
|
48211
|
+
root[section] = { ...root[section], [name]: meta.version };
|
|
48212
|
+
}
|
|
48213
|
+
return { packageText: formatLike(packageText, pkg), lockText: formatLike(lockText, lock), result };
|
|
48214
|
+
}
|
|
48215
|
+
function expectedFromRun(run, opts) {
|
|
48216
|
+
if (run.id !== opts.run || run.repository?.full_name !== EXPORT_REPO || run.path !== EXPORT_WORKFLOW) {
|
|
48217
|
+
throw new Error(`run ${opts.run} is not a ${EXPORT_REPO} ${EXPORT_WORKFLOW} run`);
|
|
48218
|
+
}
|
|
48219
|
+
return {
|
|
48220
|
+
artifactId: opts.artifact,
|
|
48221
|
+
artifactName: `mmi-package-export-${run.id}-${run.run_attempt}`,
|
|
48222
|
+
repositoryId: run.repository.id,
|
|
48223
|
+
runId: run.id,
|
|
48224
|
+
headSha: run.head_sha,
|
|
48225
|
+
headBranch: run.head_branch,
|
|
48226
|
+
digest: opts.digest
|
|
48227
|
+
};
|
|
48228
|
+
}
|
|
48229
|
+
async function adoptExport(opts, deps) {
|
|
48230
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(opts.digest)) throw new Error("--digest must be sha256:<64 hex>, from the export run summary");
|
|
48231
|
+
const packagePath = (0, import_node_path44.join)(opts.dir, "package.json");
|
|
48232
|
+
const lockPath = (0, import_node_path44.join)(opts.dir, "package-lock.json");
|
|
48233
|
+
const packageText = (0, import_node_fs45.readFileSync)(packagePath, "utf8");
|
|
48234
|
+
const lockText = (0, import_node_fs45.readFileSync)(lockPath, "utf8");
|
|
48235
|
+
const work = (0, import_node_fs45.mkdtempSync)((0, import_node_path44.join)((0, import_node_os21.tmpdir)(), "mmi-adopt-export-"));
|
|
48236
|
+
try {
|
|
48237
|
+
const run = JSON.parse((await deps.ghApi(`repos/${EXPORT_REPO}/actions/runs/${opts.run}`)).toString("utf8"));
|
|
48238
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
|
|
48239
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
|
|
48240
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
|
|
48241
|
+
const script = (0, import_node_path44.join)(work, "package_export.py");
|
|
48242
|
+
(0, import_node_fs45.writeFileSync)(script, package_export_default);
|
|
48243
|
+
await deps.python(script, ["verify", work, opts.ds, opts.screens]);
|
|
48244
|
+
const verified = (0, import_node_path44.join)(work, "verified");
|
|
48245
|
+
const metadata = Object.fromEntries(PACKAGES.map(({ name, short: short3 }) => [name, JSON.parse((0, import_node_fs45.readFileSync)((0, import_node_path44.join)(verified, `${short3}.metadata.json`), "utf8"))]));
|
|
48246
|
+
const plan = planAdoption(packageText, lockText, metadata);
|
|
48247
|
+
for (const { name } of plan.result.adopted) {
|
|
48248
|
+
const short3 = PACKAGES.find((p) => p.name === name).short;
|
|
48249
|
+
await deps.npm(["cache", "add", (0, import_node_path44.join)(verified, `${short3}.tgz`)], opts.dir);
|
|
48250
|
+
}
|
|
48251
|
+
(0, import_node_fs45.writeFileSync)(packagePath, plan.packageText);
|
|
48252
|
+
(0, import_node_fs45.writeFileSync)(lockPath, plan.lockText);
|
|
48253
|
+
try {
|
|
48254
|
+
await deps.npm(["ci", "--offline", "--ignore-scripts", "--no-audit", "--no-fund"], opts.dir);
|
|
48255
|
+
} catch (error) {
|
|
48256
|
+
(0, import_node_fs45.writeFileSync)(packagePath, packageText);
|
|
48257
|
+
(0, import_node_fs45.writeFileSync)(lockPath, lockText);
|
|
48258
|
+
throw new Error(`npm ci --offline failed, package.json and package-lock.json restored: ${error.message}`);
|
|
48259
|
+
}
|
|
48260
|
+
return plan.result;
|
|
48261
|
+
} finally {
|
|
48262
|
+
(0, import_node_fs45.rmSync)(work, { recursive: true, force: true });
|
|
48263
|
+
}
|
|
48264
|
+
}
|
|
48265
|
+
var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process18.execFile);
|
|
48266
|
+
var MAX_OUTPUT = 200 * 1024 * 1024;
|
|
48267
|
+
async function findPython() {
|
|
48268
|
+
for (const bin of ["python3", "python"]) {
|
|
48269
|
+
try {
|
|
48270
|
+
await execFileBuffer(bin, ["-I", "-S", "-B", "-c", "import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)"], { windowsHide: true, timeout: 1e4 });
|
|
48271
|
+
return bin;
|
|
48272
|
+
} catch {
|
|
48273
|
+
}
|
|
48274
|
+
}
|
|
48275
|
+
throw new Error("Python 3.11+ is required to run the Hub package-export verifier");
|
|
48276
|
+
}
|
|
48277
|
+
function adoptExportProdDeps() {
|
|
48278
|
+
const failText = (error) => String(error?.stderr || error?.message || error).trim();
|
|
48279
|
+
return {
|
|
48280
|
+
ghApi: async (path2) => {
|
|
48281
|
+
try {
|
|
48282
|
+
return (await execFileBuffer("gh", ["api", "--method", "GET", path2], { encoding: "buffer", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 3e5 })).stdout;
|
|
48283
|
+
} catch (error) {
|
|
48284
|
+
throw new Error(`gh api ${path2} failed: ${failText(error)}`);
|
|
48285
|
+
}
|
|
48286
|
+
},
|
|
48287
|
+
python: async (script, args) => {
|
|
48288
|
+
try {
|
|
48289
|
+
return (await execFileBuffer(await findPython(), ["-I", "-S", "-B", script, ...args], { encoding: "utf8", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 3e5 })).stdout;
|
|
48290
|
+
} catch (error) {
|
|
48291
|
+
throw new Error(failText(error));
|
|
48292
|
+
}
|
|
48293
|
+
},
|
|
48294
|
+
npm: async (args, cwd) => {
|
|
48295
|
+
const [file, argv] = process.platform === "win32" ? ["cmd.exe", ["/c", "npm", ...args]] : ["npm", args];
|
|
48296
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "npm_config_allow_scripts"));
|
|
48297
|
+
try {
|
|
48298
|
+
return (await execFileBuffer(file, argv, { cwd, env, encoding: "utf8", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 6e5 })).stdout;
|
|
48299
|
+
} catch (error) {
|
|
48300
|
+
throw new Error(`npm ${args[0]} failed: ${failText(error)}`);
|
|
48301
|
+
}
|
|
48302
|
+
}
|
|
48303
|
+
};
|
|
48304
|
+
}
|
|
48305
|
+
function registerPackagesCommands(program3) {
|
|
48306
|
+
const packages = program3.command("packages").description("consumer adoption of verified package exports (#7833)");
|
|
48307
|
+
packages.command("adopt-export").description("verify a package-export artifact and install its exact kit pair offline, keeping registry URLs and SRI in the lock").requiredOption("--run <id>", "package-export workflow run id").requiredOption("--artifact <id>", "artifact id from the run summary").requiredOption("--digest <sha256:hex>", "artifact digest from the run summary").requiredOption("--ds <version>", "exact @mutmutco/mmwork-ds version").requiredOption("--screens <version>", "exact @mutmutco/mmwork-screens version").option("--dir <path>", "npm project holding package.json and package-lock.json", ".").action(async (opts) => {
|
|
48308
|
+
const id = (value, flag) => {
|
|
48309
|
+
if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
|
|
48310
|
+
return Number(value);
|
|
48311
|
+
};
|
|
48312
|
+
try {
|
|
48313
|
+
const result = await adoptExport({ ...opts, run: id(opts.run, "--run"), artifact: id(opts.artifact, "--artifact") }, adoptExportProdDeps());
|
|
48314
|
+
for (const a of result.adopted) console.log(`adopted ${a.name} ${a.from} -> ${a.to} (${a.resolved}, ${a.integrity})`);
|
|
48315
|
+
for (const name of result.skipped) console.log(`skipped ${name}: not a dependency in this package.json`);
|
|
48316
|
+
console.log("npm ci --offline --ignore-scripts passed; commit package.json and package-lock.json");
|
|
48317
|
+
} catch (error) {
|
|
48318
|
+
fail(`packages adopt-export refused: ${error.message}`);
|
|
48319
|
+
}
|
|
48320
|
+
});
|
|
48321
|
+
}
|
|
48322
|
+
|
|
47594
48323
|
// src/discovery-commands.ts
|
|
47595
48324
|
init_cli_shared();
|
|
47596
48325
|
|
|
47597
48326
|
// src/wave-status.ts
|
|
47598
|
-
var
|
|
48327
|
+
var import_node_fs46 = require("node:fs");
|
|
47599
48328
|
function readStageSummary(worktreePath) {
|
|
47600
48329
|
const statePath = stageStatePath(worktreePath);
|
|
47601
|
-
if (!(0,
|
|
48330
|
+
if (!(0, import_node_fs46.existsSync)(statePath)) return void 0;
|
|
47602
48331
|
try {
|
|
47603
|
-
const state = JSON.parse((0,
|
|
48332
|
+
const state = JSON.parse((0, import_node_fs46.readFileSync)(statePath, "utf8"));
|
|
47604
48333
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
47605
48334
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
47606
48335
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -47657,9 +48386,9 @@ async function collectWaveStatus(deps) {
|
|
|
47657
48386
|
// src/discovery-commands.ts
|
|
47658
48387
|
init_github_client();
|
|
47659
48388
|
init_cli_shared();
|
|
47660
|
-
var
|
|
47661
|
-
var
|
|
47662
|
-
var
|
|
48389
|
+
var import_node_fs47 = require("node:fs");
|
|
48390
|
+
var import_node_os22 = require("node:os");
|
|
48391
|
+
var import_node_path45 = require("node:path");
|
|
47663
48392
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
47664
48393
|
async function collectStatus() {
|
|
47665
48394
|
const repo = await resolveRepo();
|
|
@@ -47848,10 +48577,10 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
47848
48577
|
else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
|
|
47849
48578
|
else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
|
|
47850
48579
|
}
|
|
47851
|
-
const home = (0,
|
|
48580
|
+
const home = (0, import_node_os22.homedir)();
|
|
47852
48581
|
const plugin = onboardPluginGate({
|
|
47853
|
-
readKnown: () => readFileSyncSafe((0,
|
|
47854
|
-
readSettings: () => readFileSyncSafe((0,
|
|
48582
|
+
readKnown: () => readFileSyncSafe((0, import_node_path45.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs47.readFileSync),
|
|
48583
|
+
readSettings: () => readFileSyncSafe((0, import_node_path45.join)(home, ".claude", "settings.json"), import_node_fs47.readFileSync)
|
|
47855
48584
|
});
|
|
47856
48585
|
return {
|
|
47857
48586
|
guide: "Read the MMI org-guide skill before project work; load only the relevant topic. Use mmi-help for a short introduction.",
|
|
@@ -50955,8 +51684,8 @@ function registerSessionReport(program3) {
|
|
|
50955
51684
|
}
|
|
50956
51685
|
|
|
50957
51686
|
// src/stage-commands.ts
|
|
50958
|
-
var
|
|
50959
|
-
var
|
|
51687
|
+
var import_node_fs48 = require("node:fs");
|
|
51688
|
+
var import_node_path46 = require("node:path");
|
|
50960
51689
|
init_cli_shared();
|
|
50961
51690
|
init_clean_exit();
|
|
50962
51691
|
|
|
@@ -51202,8 +51931,8 @@ function registerStageCommands(program3) {
|
|
|
51202
51931
|
return {
|
|
51203
51932
|
resolution: decideStage({
|
|
51204
51933
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
51205
|
-
hasCompose: (0,
|
|
51206
|
-
hasEnvExample: (0,
|
|
51934
|
+
hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
|
|
51935
|
+
hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
|
|
51207
51936
|
repo
|
|
51208
51937
|
}),
|
|
51209
51938
|
project: project2,
|
|
@@ -51285,7 +52014,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
51285
52014
|
if (!res.derived) return void 0;
|
|
51286
52015
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
51287
52016
|
}
|
|
51288
|
-
program3.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (
|
|
52017
|
+
program3.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (fails loud when DDB is unreachable)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
51289
52018
|
const cfg = await loadConfig();
|
|
51290
52019
|
const reg = registryClientDeps(cfg);
|
|
51291
52020
|
const slug = slugOf(repo);
|
|
@@ -51488,19 +52217,19 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
51488
52217
|
|
|
51489
52218
|
// src/tenant-artifact.ts
|
|
51490
52219
|
var import_node_crypto16 = require("node:crypto");
|
|
51491
|
-
var
|
|
52220
|
+
var import_node_fs49 = require("node:fs");
|
|
51492
52221
|
var import_promises11 = require("node:fs/promises");
|
|
51493
|
-
var
|
|
52222
|
+
var import_node_path47 = require("node:path");
|
|
51494
52223
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
51495
52224
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
51496
52225
|
async function sha256File(path2) {
|
|
51497
52226
|
const hash = (0, import_node_crypto16.createHash)("sha256");
|
|
51498
|
-
for await (const chunk of (0,
|
|
52227
|
+
for await (const chunk of (0, import_node_fs49.createReadStream)(path2)) hash.update(chunk);
|
|
51499
52228
|
return hash.digest("hex");
|
|
51500
52229
|
}
|
|
51501
52230
|
async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
51502
52231
|
if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
|
|
51503
|
-
const path2 = (0,
|
|
52232
|
+
const path2 = (0, import_node_path47.resolve)(inputPath);
|
|
51504
52233
|
const info = await (0, import_promises11.stat)(path2);
|
|
51505
52234
|
if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
|
|
51506
52235
|
if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES} bytes`);
|
|
@@ -51518,7 +52247,7 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
|
51518
52247
|
return [key, value];
|
|
51519
52248
|
}));
|
|
51520
52249
|
headers["content-length"] = String(info.size);
|
|
51521
|
-
const stream = (0,
|
|
52250
|
+
const stream = (0, import_node_fs49.createReadStream)(path2);
|
|
51522
52251
|
let uploaded;
|
|
51523
52252
|
try {
|
|
51524
52253
|
uploaded = await fetch(body.uploadUrl, {
|
|
@@ -51657,7 +52386,7 @@ function renderVerifySecrets(body) {
|
|
|
51657
52386
|
}
|
|
51658
52387
|
|
|
51659
52388
|
// src/command-register-developer.ts
|
|
51660
|
-
var
|
|
52389
|
+
var import_node_fs53 = require("node:fs");
|
|
51661
52390
|
init_clean_exit();
|
|
51662
52391
|
init_cli_shared();
|
|
51663
52392
|
init_error_codes();
|
|
@@ -51690,7 +52419,7 @@ async function resolveWhoami(deps) {
|
|
|
51690
52419
|
}
|
|
51691
52420
|
|
|
51692
52421
|
// src/command-register-developer.ts
|
|
51693
|
-
var
|
|
52422
|
+
var import_node_path52 = require("node:path");
|
|
51694
52423
|
init_house_map();
|
|
51695
52424
|
init_hub_url();
|
|
51696
52425
|
|
|
@@ -51723,21 +52452,21 @@ async function executeWaveLand(plan, deps) {
|
|
|
51723
52452
|
}
|
|
51724
52453
|
|
|
51725
52454
|
// src/dist-drift.ts
|
|
51726
|
-
var
|
|
52455
|
+
var import_node_child_process19 = require("node:child_process");
|
|
51727
52456
|
var import_node_crypto18 = require("node:crypto");
|
|
51728
|
-
var
|
|
51729
|
-
var
|
|
51730
|
-
var
|
|
52457
|
+
var import_node_fs51 = require("node:fs");
|
|
52458
|
+
var import_node_os23 = require("node:os");
|
|
52459
|
+
var import_node_path49 = require("node:path");
|
|
51731
52460
|
|
|
51732
52461
|
// ../scripts/distribution-digest.mjs
|
|
51733
52462
|
var import_node_crypto17 = require("node:crypto");
|
|
51734
|
-
var
|
|
51735
|
-
var
|
|
52463
|
+
var import_node_fs50 = require("node:fs");
|
|
52464
|
+
var import_node_path48 = require("node:path");
|
|
51736
52465
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
51737
52466
|
function repoPath(root, declaredPath, label) {
|
|
51738
|
-
const absoluteRoot = (0,
|
|
51739
|
-
const target = (0,
|
|
51740
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
52467
|
+
const absoluteRoot = (0, import_node_path48.resolve)(root);
|
|
52468
|
+
const target = (0, import_node_path48.resolve)(root, declaredPath);
|
|
52469
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path48.sep}`)) {
|
|
51741
52470
|
throw new Error(`${label} ${declaredPath} escapes the repository root`);
|
|
51742
52471
|
}
|
|
51743
52472
|
return target;
|
|
@@ -51745,7 +52474,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
51745
52474
|
function digestFiles(files) {
|
|
51746
52475
|
const hash = (0, import_node_crypto17.createHash)("sha256");
|
|
51747
52476
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
51748
|
-
const content = file.stat.isSymbolicLink() ? Buffer.from((0,
|
|
52477
|
+
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs50.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs50.readFileSync)(file.absolute);
|
|
51749
52478
|
hash.update(file.relative, "utf8");
|
|
51750
52479
|
hash.update("\0");
|
|
51751
52480
|
hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
|
|
@@ -51760,8 +52489,8 @@ function digestFiles(files) {
|
|
|
51760
52489
|
function digestPackedFiles(packageRoot, packedFiles) {
|
|
51761
52490
|
return digestFiles(packedFiles.map((path2) => {
|
|
51762
52491
|
const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
|
|
51763
|
-
if (!(0,
|
|
51764
|
-
return { absolute, relative: slash(path2), stat: (0,
|
|
52492
|
+
if (!(0, import_node_fs50.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
|
|
52493
|
+
return { absolute, relative: slash(path2), stat: (0, import_node_fs50.lstatSync)(absolute) };
|
|
51765
52494
|
}));
|
|
51766
52495
|
}
|
|
51767
52496
|
|
|
@@ -51845,13 +52574,13 @@ function renderDistDriftReceipt(receipt) {
|
|
|
51845
52574
|
return lines2;
|
|
51846
52575
|
}
|
|
51847
52576
|
function readOrNull(path2) {
|
|
51848
|
-
return (0,
|
|
52577
|
+
return (0, import_node_fs51.existsSync)(path2) ? (0, import_node_fs51.readFileSync)(path2) : null;
|
|
51849
52578
|
}
|
|
51850
52579
|
function walkFiles(root) {
|
|
51851
52580
|
const files = [];
|
|
51852
52581
|
const walk2 = (directory) => {
|
|
51853
|
-
for (const entry of (0,
|
|
51854
|
-
const child2 = (0,
|
|
52582
|
+
for (const entry of (0, import_node_fs51.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
52583
|
+
const child2 = (0, import_node_path49.join)(directory, entry.name);
|
|
51855
52584
|
if (entry.isDirectory()) walk2(child2);
|
|
51856
52585
|
else files.push(child2);
|
|
51857
52586
|
}
|
|
@@ -51861,14 +52590,14 @@ function walkFiles(root) {
|
|
|
51861
52590
|
}
|
|
51862
52591
|
function bomPathFor(root) {
|
|
51863
52592
|
try {
|
|
51864
|
-
const registry2 = JSON.parse((0,
|
|
51865
|
-
return (0,
|
|
52593
|
+
const registry2 = JSON.parse((0, import_node_fs51.readFileSync)((0, import_node_path49.join)(root, "surfaces.json"), "utf8"));
|
|
52594
|
+
return (0, import_node_path49.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
|
|
51866
52595
|
} catch {
|
|
51867
|
-
return (0,
|
|
52596
|
+
return (0, import_node_path49.join)(root, "distribution-bom.json");
|
|
51868
52597
|
}
|
|
51869
52598
|
}
|
|
51870
52599
|
function rebuildTo(packageRoot, outDir) {
|
|
51871
|
-
(0,
|
|
52600
|
+
(0, import_node_child_process19.execFileSync)(process.execPath, ["build.mjs"], {
|
|
51872
52601
|
cwd: packageRoot,
|
|
51873
52602
|
env: { ...process.env, MMI_DIST_OUTDIR: outDir },
|
|
51874
52603
|
windowsHide: true,
|
|
@@ -51877,35 +52606,35 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
51877
52606
|
});
|
|
51878
52607
|
}
|
|
51879
52608
|
function runDistStatus(root) {
|
|
51880
|
-
const stage = (0,
|
|
52609
|
+
const stage = (0, import_node_fs51.mkdtempSync)((0, import_node_path49.join)((0, import_node_os23.tmpdir)(), "mmi-dist-drift-"));
|
|
51881
52610
|
let overlayCount = 0;
|
|
51882
52611
|
try {
|
|
51883
|
-
const cliOut = (0,
|
|
51884
|
-
const hubOut = (0,
|
|
51885
|
-
rebuildTo((0,
|
|
51886
|
-
rebuildTo((0,
|
|
52612
|
+
const cliOut = (0, import_node_path49.join)(stage, "cli-dist");
|
|
52613
|
+
const hubOut = (0, import_node_path49.join)(stage, "hub-dist");
|
|
52614
|
+
rebuildTo((0, import_node_path49.join)(root, "cli"), cliOut);
|
|
52615
|
+
rebuildTo((0, import_node_path49.join)(root, "updater"), hubOut);
|
|
51887
52616
|
const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
|
|
51888
52617
|
const rebuilt = (path2) => {
|
|
51889
52618
|
const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
|
|
51890
|
-
return spec ? readOrNull((0,
|
|
52619
|
+
return spec ? readOrNull((0, import_node_path49.join)(outDirFor(spec.packageDir), spec.output)) : null;
|
|
51891
52620
|
};
|
|
51892
|
-
const committed = (path2) => readOrNull((0,
|
|
51893
|
-
const tree = (path2) => readOrNull((0,
|
|
51894
|
-
const distRoot = (0,
|
|
51895
|
-
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0,
|
|
51896
|
-
const bom = JSON.parse((0,
|
|
52621
|
+
const committed = (path2) => readOrNull((0, import_node_path49.join)(root, path2));
|
|
52622
|
+
const tree = (path2) => readOrNull((0, import_node_path49.join)(root, path2));
|
|
52623
|
+
const distRoot = (0, import_node_path49.join)(root, "cli", "dist");
|
|
52624
|
+
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path49.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
|
|
52625
|
+
const bom = JSON.parse((0, import_node_fs51.readFileSync)(bomPathFor(root), "utf8"));
|
|
51897
52626
|
const digest = (entries) => {
|
|
51898
|
-
const overlay = (0,
|
|
52627
|
+
const overlay = (0, import_node_path49.join)(stage, `overlay-${overlayCount++}`);
|
|
51899
52628
|
for (const entry of entries) {
|
|
51900
|
-
const target = (0,
|
|
51901
|
-
(0,
|
|
51902
|
-
(0,
|
|
52629
|
+
const target = (0, import_node_path49.join)(overlay, entry.path);
|
|
52630
|
+
(0, import_node_fs51.mkdirSync)((0, import_node_path49.dirname)(target), { recursive: true });
|
|
52631
|
+
(0, import_node_fs51.writeFileSync)(target, entry.bytes);
|
|
51903
52632
|
}
|
|
51904
52633
|
return digestPackedFiles(overlay, entries.map((entry) => entry.path));
|
|
51905
52634
|
};
|
|
51906
52635
|
return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
|
|
51907
52636
|
} finally {
|
|
51908
|
-
(0,
|
|
52637
|
+
(0, import_node_fs51.rmSync)(stage, { recursive: true, force: true });
|
|
51909
52638
|
}
|
|
51910
52639
|
}
|
|
51911
52640
|
|
|
@@ -51984,7 +52713,7 @@ init_hub_auth();
|
|
|
51984
52713
|
|
|
51985
52714
|
// src/schedules-lift-command.ts
|
|
51986
52715
|
var import_promises12 = require("node:fs/promises");
|
|
51987
|
-
var
|
|
52716
|
+
var import_node_path50 = require("node:path");
|
|
51988
52717
|
init_clean_exit();
|
|
51989
52718
|
init_cli_shared();
|
|
51990
52719
|
var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
|
|
@@ -52013,7 +52742,7 @@ async function readWorkflowFiles(dir) {
|
|
|
52013
52742
|
const files = [];
|
|
52014
52743
|
for (const name of names.sort()) {
|
|
52015
52744
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
52016
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0,
|
|
52745
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path50.join)(dir, name), "utf8") });
|
|
52017
52746
|
}
|
|
52018
52747
|
return files;
|
|
52019
52748
|
}
|
|
@@ -52095,9 +52824,9 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
|
52095
52824
|
}
|
|
52096
52825
|
|
|
52097
52826
|
// src/spawn-policy-core.ts
|
|
52098
|
-
var
|
|
52099
|
-
var
|
|
52100
|
-
var
|
|
52827
|
+
var import_node_child_process20 = require("node:child_process");
|
|
52828
|
+
var import_node_fs52 = require("node:fs");
|
|
52829
|
+
var import_node_path51 = require("node:path");
|
|
52101
52830
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
52102
52831
|
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
52103
52832
|
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
@@ -52166,7 +52895,7 @@ function findViolationsInSource(raw) {
|
|
|
52166
52895
|
return found;
|
|
52167
52896
|
}
|
|
52168
52897
|
function policedFiles(root) {
|
|
52169
|
-
const r = (0,
|
|
52898
|
+
const r = (0, import_node_child_process20.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
52170
52899
|
cwd: root,
|
|
52171
52900
|
encoding: "utf8",
|
|
52172
52901
|
windowsHide: true,
|
|
@@ -52183,7 +52912,7 @@ function runSpawnPolicy(root) {
|
|
|
52183
52912
|
for (const file of files) {
|
|
52184
52913
|
let raw;
|
|
52185
52914
|
try {
|
|
52186
|
-
raw = (0,
|
|
52915
|
+
raw = (0, import_node_fs52.readFileSync)((0, import_node_path51.join)(root, file), "utf8");
|
|
52187
52916
|
} catch {
|
|
52188
52917
|
continue;
|
|
52189
52918
|
}
|
|
@@ -52203,19 +52932,19 @@ function runSpawnPolicy(root) {
|
|
|
52203
52932
|
function registerDeveloperCommands(program3) {
|
|
52204
52933
|
const rules = program3.command("rules").description("org-managed .gitignore delivery");
|
|
52205
52934
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
52206
|
-
const path2 = (0,
|
|
52207
|
-
const current = (0,
|
|
52935
|
+
const path2 = (0, import_node_path52.join)(process.cwd(), ".gitignore");
|
|
52936
|
+
const current = (0, import_node_fs53.existsSync)(path2) ? (0, import_node_fs53.readFileSync)(path2, "utf8") : null;
|
|
52208
52937
|
const plan = planManagedGitignore(current);
|
|
52209
52938
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
52210
52939
|
if (opts.json) {
|
|
52211
|
-
if (opts.write && plan.changed) (0,
|
|
52940
|
+
if (opts.write && plan.changed) (0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
|
|
52212
52941
|
console.log(JSON.stringify(plan, null, 2));
|
|
52213
52942
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
52214
52943
|
return;
|
|
52215
52944
|
}
|
|
52216
52945
|
if (opts.write) {
|
|
52217
52946
|
if (plan.changed) {
|
|
52218
|
-
(0,
|
|
52947
|
+
(0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
|
|
52219
52948
|
console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
|
|
52220
52949
|
} else {
|
|
52221
52950
|
console.log("mmi-cli devops org rules gitignore: up to date");
|
|
@@ -52478,12 +53207,12 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
52478
53207
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
52479
53208
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
52480
53209
|
function envHealLockPath(home) {
|
|
52481
|
-
return (0,
|
|
53210
|
+
return (0, import_node_path53.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
52482
53211
|
}
|
|
52483
53212
|
async function withEnvHealLock(what, run) {
|
|
52484
53213
|
try {
|
|
52485
53214
|
return await withFileLock(
|
|
52486
|
-
envHealLockPath((0,
|
|
53215
|
+
envHealLockPath((0, import_node_os24.homedir)()),
|
|
52487
53216
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
52488
53217
|
run
|
|
52489
53218
|
);
|
|
@@ -52580,7 +53309,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52580
53309
|
const configRoot = surfaceConfigRoot(surface);
|
|
52581
53310
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
52582
53311
|
const plan = buildPluginCachePlan(
|
|
52583
|
-
(0,
|
|
53312
|
+
(0, import_node_os24.homedir)(),
|
|
52584
53313
|
running,
|
|
52585
53314
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
52586
53315
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -52608,14 +53337,14 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52608
53337
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
52609
53338
|
const installed = installedActivePluginVersion(surface);
|
|
52610
53339
|
const plan = buildPluginCachePlan(
|
|
52611
|
-
(0,
|
|
53340
|
+
(0, import_node_os24.homedir)(),
|
|
52612
53341
|
running,
|
|
52613
53342
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
52614
53343
|
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
52615
53344
|
);
|
|
52616
53345
|
const result = applyPluginCachePlan(
|
|
52617
53346
|
plan,
|
|
52618
|
-
(p) => (0,
|
|
53347
|
+
(p) => (0, import_node_fs54.rmSync)(p, { recursive: true }),
|
|
52619
53348
|
stagingApplyFsGuard(configRoot)
|
|
52620
53349
|
);
|
|
52621
53350
|
return {
|
|
@@ -52650,7 +53379,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52650
53379
|
// has no generated routing index would
|
|
52651
53380
|
// get a permanent — demanding an artifact it never asked for.
|
|
52652
53381
|
docsIndexState: (root) => {
|
|
52653
|
-
if (!(0,
|
|
53382
|
+
if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
52654
53383
|
const real = createDocsIndexDeps(root);
|
|
52655
53384
|
let docs;
|
|
52656
53385
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -52659,7 +53388,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52659
53388
|
},
|
|
52660
53389
|
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
52661
53390
|
healDocsIndex: (root) => {
|
|
52662
|
-
if (!(0,
|
|
53391
|
+
if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
52663
53392
|
const real = createDocsIndexDeps(root);
|
|
52664
53393
|
let docs;
|
|
52665
53394
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -53367,7 +54096,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
53367
54096
|
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
53368
54097
|
if (o.secretsFile) {
|
|
53369
54098
|
try {
|
|
53370
|
-
vars.push(`secrets=${(0,
|
|
54099
|
+
vars.push(`secrets=${(0, import_node_fs54.readFileSync)(o.secretsFile, "utf8")}`);
|
|
53371
54100
|
} catch (e) {
|
|
53372
54101
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
53373
54102
|
}
|
|
@@ -53755,16 +54484,16 @@ function ciAuditDeps2() {
|
|
|
53755
54484
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
53756
54485
|
readSeedFile: (path2) => {
|
|
53757
54486
|
if (!root) return null;
|
|
53758
|
-
const fullPath = (0,
|
|
53759
|
-
return (0,
|
|
54487
|
+
const fullPath = (0, import_node_path53.join)(root, path2);
|
|
54488
|
+
return (0, import_node_fs54.existsSync)(fullPath) ? (0, import_node_fs54.readFileSync)(fullPath, "utf8") : null;
|
|
53760
54489
|
}
|
|
53761
54490
|
};
|
|
53762
54491
|
}
|
|
53763
54492
|
function hubRoot2() {
|
|
53764
|
-
const fromPkg = (0,
|
|
54493
|
+
const fromPkg = (0, import_node_path53.join)(__dirname, "..", "..");
|
|
53765
54494
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
53766
|
-
if ((0,
|
|
53767
|
-
if ((0,
|
|
54495
|
+
if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(fromPkg, marker))) return fromPkg;
|
|
54496
|
+
if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(process.cwd(), marker))) return process.cwd();
|
|
53768
54497
|
return null;
|
|
53769
54498
|
}
|
|
53770
54499
|
registerQueryCommands(program2);
|
|
@@ -53773,6 +54502,7 @@ registerTrainCommandsAndInventory(program2);
|
|
|
53773
54502
|
registerFleetLockstepEnforce(program2);
|
|
53774
54503
|
registerDeployCommands(program2);
|
|
53775
54504
|
registerSuiteCommands(program2);
|
|
54505
|
+
registerPackagesCommands(program2);
|
|
53776
54506
|
registerDiscoveryCommands(program2);
|
|
53777
54507
|
registerExplainCommand(program2);
|
|
53778
54508
|
registerPrLifecycleCommands(program2);
|
|
@@ -53888,10 +54618,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
53888
54618
|
targets = resolution.targets;
|
|
53889
54619
|
}
|
|
53890
54620
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
53891
|
-
const fileMatrix = (0,
|
|
54621
|
+
const fileMatrix = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
53892
54622
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
53893
54623
|
const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
53894
|
-
const sanctioned = (0,
|
|
54624
|
+
const sanctioned = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
53895
54625
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
53896
54626
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
53897
54627
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -53922,16 +54652,16 @@ function directoryBytes(path2) {
|
|
|
53922
54652
|
let total = 0;
|
|
53923
54653
|
let entries;
|
|
53924
54654
|
try {
|
|
53925
|
-
entries = (0,
|
|
54655
|
+
entries = (0, import_node_fs54.readdirSync)(path2, { withFileTypes: true });
|
|
53926
54656
|
} catch {
|
|
53927
54657
|
return 0;
|
|
53928
54658
|
}
|
|
53929
54659
|
for (const entry of entries) {
|
|
53930
|
-
const child2 = (0,
|
|
54660
|
+
const child2 = (0, import_node_path53.join)(path2, entry.name);
|
|
53931
54661
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
53932
54662
|
else {
|
|
53933
54663
|
try {
|
|
53934
|
-
total += (0,
|
|
54664
|
+
total += (0, import_node_fs54.statSync)(child2).size;
|
|
53935
54665
|
} catch {
|
|
53936
54666
|
}
|
|
53937
54667
|
}
|
|
@@ -53939,25 +54669,25 @@ function directoryBytes(path2) {
|
|
|
53939
54669
|
return total;
|
|
53940
54670
|
}
|
|
53941
54671
|
function listDirEntries(dir) {
|
|
53942
|
-
return (0,
|
|
54672
|
+
return (0, import_node_fs54.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
53943
54673
|
}
|
|
53944
54674
|
function readInstalledPluginRefs(configRoot) {
|
|
53945
54675
|
const p = installedPluginsPathForConfig(configRoot);
|
|
53946
|
-
if (!(0,
|
|
54676
|
+
if (!(0, import_node_fs54.existsSync)(p)) return [];
|
|
53947
54677
|
try {
|
|
53948
|
-
return installedPluginPaths((0,
|
|
54678
|
+
return installedPluginPaths((0, import_node_fs54.readFileSync)(p, "utf8"));
|
|
53949
54679
|
} catch {
|
|
53950
54680
|
return null;
|
|
53951
54681
|
}
|
|
53952
54682
|
}
|
|
53953
54683
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
53954
54684
|
return {
|
|
53955
|
-
exists: (p) => (0,
|
|
53956
|
-
listVersionDirs: (root) => (0,
|
|
54685
|
+
exists: (p) => (0, import_node_fs54.existsSync)(p),
|
|
54686
|
+
listVersionDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
53957
54687
|
dirBytes,
|
|
53958
|
-
listStagingDirs: (root) => (0,
|
|
54688
|
+
listStagingDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
53959
54689
|
try {
|
|
53960
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
54690
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path53.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs54.statSync)(p).mtimeMs) };
|
|
53961
54691
|
} catch {
|
|
53962
54692
|
return { name: d.name, mtimeMs: Date.now() };
|
|
53963
54693
|
}
|
|
@@ -53971,10 +54701,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
53971
54701
|
return {
|
|
53972
54702
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
53973
54703
|
mtimeMs: (name) => {
|
|
53974
|
-
const p = (0,
|
|
53975
|
-
if (!(0,
|
|
54704
|
+
const p = (0, import_node_path53.join)(stagingRoot, name);
|
|
54705
|
+
if (!(0, import_node_fs54.existsSync)(p)) return null;
|
|
53976
54706
|
try {
|
|
53977
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
54707
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs54.statSync)(q).mtimeMs);
|
|
53978
54708
|
} catch {
|
|
53979
54709
|
return null;
|
|
53980
54710
|
}
|
|
@@ -53994,13 +54724,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
53994
54724
|
return;
|
|
53995
54725
|
}
|
|
53996
54726
|
const plan = buildPluginCachePlan(
|
|
53997
|
-
(0,
|
|
54727
|
+
(0, import_node_os24.homedir)(),
|
|
53998
54728
|
running,
|
|
53999
54729
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
54000
54730
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
54001
54731
|
);
|
|
54002
54732
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
54003
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
54733
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs54.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
54004
54734
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
54005
54735
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
54006
54736
|
else console.log(renderPluginCachePlan(plan, result));
|