@mutmutco/codex-plugin 4.5.89 → 4.5.91
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 +899 -153
- 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");
|
|
@@ -5792,7 +5794,14 @@ function decidePrMergeCleanupGate(input) {
|
|
|
5792
5794
|
message: `could not read the PR's state after the merge attempt${input.stateError ? `: ${input.stateError}` : ""} \u2014 cleanup skipped because the merge is unproven`
|
|
5793
5795
|
};
|
|
5794
5796
|
}
|
|
5795
|
-
if (input.autoRequested) return { action: "enqueued" };
|
|
5797
|
+
if (input.autoRequested && state === "OPEN" && (input.autoMergeRequest ?? "").trim()) return { action: "enqueued" };
|
|
5798
|
+
if (input.autoRequested && state === "OPEN") {
|
|
5799
|
+
return {
|
|
5800
|
+
action: "refuse",
|
|
5801
|
+
reason: "not-merged",
|
|
5802
|
+
message: "the PR is OPEN and GitHub shows no queued auto-merge (or it could not be read) \u2014 the merge is neither done nor proven enqueued, so the branch and worktree were left untouched"
|
|
5803
|
+
};
|
|
5804
|
+
}
|
|
5796
5805
|
return {
|
|
5797
5806
|
action: "refuse",
|
|
5798
5807
|
reason: "not-merged",
|
|
@@ -14524,6 +14533,24 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
|
|
|
14524
14533
|
await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
|
|
14525
14534
|
return { action: "created", enforcement };
|
|
14526
14535
|
}
|
|
14536
|
+
async function gateContextEnforced(repo, rest) {
|
|
14537
|
+
try {
|
|
14538
|
+
const list = await rest(`repos/${repo}/rulesets?includes_parents=true`);
|
|
14539
|
+
for (const entry of list) {
|
|
14540
|
+
if (entry.id == null) continue;
|
|
14541
|
+
const rs = await rest(`repos/${repo}/rulesets/${entry.id}`);
|
|
14542
|
+
if (rs.target !== "branch" || rs.enforcement !== "active") continue;
|
|
14543
|
+
const hit = (rs.rules ?? []).some((r) => r.type === "required_status_checks" && (r.parameters?.required_status_checks ?? []).some((c) => c.context === PRODUCT_GATE_CONTEXT));
|
|
14544
|
+
if (hit) return true;
|
|
14545
|
+
}
|
|
14546
|
+
return false;
|
|
14547
|
+
} catch {
|
|
14548
|
+
return false;
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14551
|
+
async function propagateMayAutoMerge(repo, repoClass, rest) {
|
|
14552
|
+
return repoClass === "content" || gateContextEnforced(repo, rest);
|
|
14553
|
+
}
|
|
14527
14554
|
|
|
14528
14555
|
// src/bootstrap-apply.ts
|
|
14529
14556
|
var import_node_crypto5 = require("node:crypto");
|
|
@@ -17225,10 +17252,10 @@ var rollout_plan_default = {
|
|
|
17225
17252
|
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
17253
|
},
|
|
17227
17254
|
baseline: {
|
|
17228
|
-
version: "4.5.
|
|
17229
|
-
tag: "v4.5.
|
|
17230
|
-
commit: "
|
|
17231
|
-
npm: "@mutmutco/cli@4.5.
|
|
17255
|
+
version: "4.5.91",
|
|
17256
|
+
tag: "v4.5.91",
|
|
17257
|
+
commit: "086f604f627d",
|
|
17258
|
+
npm: "@mutmutco/cli@4.5.91"
|
|
17232
17259
|
},
|
|
17233
17260
|
exitCriterion: "fleet-n-of-n",
|
|
17234
17261
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17245,14 +17272,14 @@ var rollout_plan_default = {
|
|
|
17245
17272
|
repo: "mutmutco/mmi-hub",
|
|
17246
17273
|
role: "canary",
|
|
17247
17274
|
schedule: "train",
|
|
17248
|
-
v3Target: "v4.5.
|
|
17275
|
+
v3Target: "v4.5.91"
|
|
17249
17276
|
}
|
|
17250
17277
|
],
|
|
17251
17278
|
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
17279
|
rollback: {
|
|
17253
17280
|
independent: true,
|
|
17254
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17255
|
-
v3Target: "v4.5.
|
|
17281
|
+
mechanism: "npm dist-tag latest -> 4.5.91 and redeploy the Hub Lambda from tag v4.5.91 (086f604f627d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17282
|
+
v3Target: "v4.5.91 (@mutmutco/cli@4.5.91, tag commit 086f604f627d \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17256
17283
|
}
|
|
17257
17284
|
},
|
|
17258
17285
|
{
|
|
@@ -28972,7 +28999,7 @@ var PRIMARY_GROUPS = [
|
|
|
28972
28999
|
["Setup and support", ["bootstrap", "secrets", "docs", "tests", "spawn", "dist"]],
|
|
28973
29000
|
["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
|
|
28974
29001
|
];
|
|
28975
|
-
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
|
|
29002
|
+
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin", "packages"]);
|
|
28976
29003
|
var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup"]);
|
|
28977
29004
|
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
|
|
28978
29005
|
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
@@ -29020,6 +29047,8 @@ var COMMAND_OWNERSHIP = {
|
|
|
29020
29047
|
train: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
29021
29048
|
suite: { module_owner: "cli/src/suite-commands.ts", consumer: "release-operator" },
|
|
29022
29049
|
// #7264
|
|
29050
|
+
packages: { module_owner: "cli/src/packages-adopt-export.ts", consumer: "agent-workflow" },
|
|
29051
|
+
// #7833
|
|
29023
29052
|
bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
|
|
29024
29053
|
secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
|
|
29025
29054
|
docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
|
|
@@ -30595,16 +30624,6 @@ function loadPortRegistry(path2) {
|
|
|
30595
30624
|
}
|
|
30596
30625
|
return out;
|
|
30597
30626
|
}
|
|
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
30627
|
function portCursorSeed(registry2) {
|
|
30609
30628
|
return nextPortBlock(registry2)[0];
|
|
30610
30629
|
}
|
|
@@ -30645,7 +30664,7 @@ function resolvePortRangeInfra(cwd, packageDir) {
|
|
|
30645
30664
|
}
|
|
30646
30665
|
return null;
|
|
30647
30666
|
}
|
|
30648
|
-
async function ensurePortRangeAtomic(repo, path2, allocate
|
|
30667
|
+
async function ensurePortRangeAtomic(repo, path2, allocate) {
|
|
30649
30668
|
const registry2 = loadPortRegistry(path2);
|
|
30650
30669
|
const existing = existingPortRange(repo, registry2);
|
|
30651
30670
|
if (existing) return { range: existing, source: "existing" };
|
|
@@ -30654,8 +30673,9 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
30654
30673
|
const range = await allocate(seed);
|
|
30655
30674
|
return { range, source: "ddb" };
|
|
30656
30675
|
} catch (e) {
|
|
30657
|
-
|
|
30658
|
-
|
|
30676
|
+
const why = e.message;
|
|
30677
|
+
const missing = /@aws-sdk\/[\w-]+/.exec(why)?.[0];
|
|
30678
|
+
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
30679
|
}
|
|
30660
30680
|
}
|
|
30661
30681
|
|
|
@@ -30683,7 +30703,13 @@ async function assignPersistedPortRange(repo, slug, reg, opts) {
|
|
|
30683
30703
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
30684
30704
|
return parsed.range;
|
|
30685
30705
|
};
|
|
30686
|
-
|
|
30706
|
+
let result;
|
|
30707
|
+
try {
|
|
30708
|
+
result = await ensurePortRangeAtomic(repo, path2, allocate);
|
|
30709
|
+
} catch (e) {
|
|
30710
|
+
return { ok: false, error: e.message };
|
|
30711
|
+
}
|
|
30712
|
+
const { range: [start, end], source } = result;
|
|
30687
30713
|
const write = await upsertProject(slug, { portRange: { start, end } }, reg);
|
|
30688
30714
|
if (!write.ok && source === "ddb") {
|
|
30689
30715
|
return {
|
|
@@ -34144,7 +34170,7 @@ async function prepareTargetedSeedBranch(repo, plan, target, client, gh2) {
|
|
|
34144
34170
|
}
|
|
34145
34171
|
await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
34146
34172
|
}
|
|
34147
|
-
async function
|
|
34173
|
+
async function verifyTargetedSeedPr(repo, number, plan, target, expected, client) {
|
|
34148
34174
|
const head = await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
34149
34175
|
const pr = await client.rest("GET", `repos/${repo}/pulls/${number}`);
|
|
34150
34176
|
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 +34185,7 @@ async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected,
|
|
|
34159
34185
|
if (file?.content !== expected) {
|
|
34160
34186
|
throw new Error("bootstrap apply: scoped seed content differs from this invocation");
|
|
34161
34187
|
}
|
|
34162
|
-
|
|
34188
|
+
return head;
|
|
34163
34189
|
}
|
|
34164
34190
|
|
|
34165
34191
|
// src/bootstrap-commands.ts
|
|
@@ -34843,8 +34869,6 @@ function registerBootstrapCommands(program3) {
|
|
|
34843
34869
|
let seedPrUrl;
|
|
34844
34870
|
const scopedHead = o.execute && scopedPlan?.branch && configGateAction?.action !== "skip" ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
34845
34871
|
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
34872
|
const openPrs = await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
34849
34873
|
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
34850
34874
|
if (prDecision.action === "reuse") {
|
|
@@ -34870,22 +34894,13 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34870
34894
|
]), "bootstrap apply pr create");
|
|
34871
34895
|
seedPrUrl = created.url;
|
|
34872
34896
|
}
|
|
34873
|
-
|
|
34897
|
+
const number = Number(seedPrUrl.split("/").pop());
|
|
34874
34898
|
if (scopedPlan) {
|
|
34875
34899
|
if (scopedContent === void 0) throw new Error("bootstrap apply: scoped seed content is unresolved");
|
|
34876
|
-
const number = Number(seedPrUrl.split("/").pop());
|
|
34877
34900
|
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})`);
|
|
34901
|
+
await verifyTargetedSeedPr(repo, number, scopedPlan, onlyTarget, scopedContent, controlClient);
|
|
34902
|
+
}
|
|
34903
|
+
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
34904
|
}
|
|
34890
34905
|
if (o.execute && !onlyTarget) {
|
|
34891
34906
|
const desiredBranches = branchesForTrack(effectiveTrack);
|
|
@@ -34932,6 +34947,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34932
34947
|
applied.push(`has_wiki=false (failed: ${e.message})`);
|
|
34933
34948
|
}
|
|
34934
34949
|
}
|
|
34950
|
+
const deferredFailures = [];
|
|
34935
34951
|
if (o.execute && !onlyTarget && o.class === "deployable") {
|
|
34936
34952
|
try {
|
|
34937
34953
|
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 +34963,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34947
34963
|
const client = controlClient;
|
|
34948
34964
|
const gateFacts = await readBootstrapGitHubFacts(repo, effectiveTrack, client);
|
|
34949
34965
|
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
|
-
}
|
|
34966
|
+
if (rulesetBlockers.length) throw new Error(`facts became unreadable before PUT; refusing write: ${rulesetBlockers.join("; ")}`);
|
|
34953
34967
|
const activation = await activateProductRuleset(
|
|
34954
34968
|
repo,
|
|
34955
34969
|
stripRulesetComment(rulesetContent),
|
|
@@ -34960,7 +34974,8 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34960
34974
|
`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
34975
|
);
|
|
34962
34976
|
} catch (e) {
|
|
34963
|
-
|
|
34977
|
+
deferredFailures.push(`product ruleset not written (${e.message}) \u2014 re-run bootstrap apply once ${PRODUCT_GATE_PATH2} has a completed run`);
|
|
34978
|
+
applied.push(`product ruleset (REFUSED: ${e.message})`);
|
|
34964
34979
|
}
|
|
34965
34980
|
}
|
|
34966
34981
|
}
|
|
@@ -35047,6 +35062,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
35047
35062
|
);
|
|
35048
35063
|
} else {
|
|
35049
35064
|
applied.push(`portRange (failed: ${assigned.error})`);
|
|
35065
|
+
deferredFailures.push(`portRange not assigned: ${assigned.error}`);
|
|
35050
35066
|
}
|
|
35051
35067
|
}
|
|
35052
35068
|
} else {
|
|
@@ -35124,6 +35140,8 @@ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSp
|
|
|
35124
35140
|
LIVE apply to ${repo}:
|
|
35125
35141
|
${applied.join("\n ")}`);
|
|
35126
35142
|
}
|
|
35143
|
+
if (deferredFailures.length) return failGraceful(`bootstrap apply: incomplete \u2014 every other step ran:
|
|
35144
|
+
${deferredFailures.join("\n ")}`);
|
|
35127
35145
|
});
|
|
35128
35146
|
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
35147
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
@@ -35371,8 +35389,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
35371
35389
|
prUrl = created.url;
|
|
35372
35390
|
}
|
|
35373
35391
|
if (rec.wave !== 0) {
|
|
35374
|
-
|
|
35392
|
+
const enforced = await propagateMayAutoMerge(rec.repo, classOf(rec.repo), async (path2) => JSON.parse((await gh2(["api", path2])).stdout || "[]"));
|
|
35393
|
+
if (enforced) await gh2(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
|
|
35375
35394
|
});
|
|
35395
|
+
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
35396
|
}
|
|
35377
35397
|
rec.prUrl = prUrl;
|
|
35378
35398
|
}
|
|
@@ -37054,9 +37074,9 @@ A hotfix branch is cut from origin/main and carries ONLY the fixes --from names,
|
|
|
37054
37074
|
async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
37055
37075
|
if (sources.length === 0) return { failures: [], skipped: [] };
|
|
37056
37076
|
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("node:fs/promises");
|
|
37057
|
-
const { tmpdir:
|
|
37058
|
-
const { join:
|
|
37059
|
-
const dir = await mkdtemp2(
|
|
37077
|
+
const { tmpdir: tmpdir12 } = await import("node:os");
|
|
37078
|
+
const { join: join47 } = await import("node:path");
|
|
37079
|
+
const dir = await mkdtemp2(join47(tmpdir12(), "mmi-hotfix-preflight-"));
|
|
37060
37080
|
const wt = { run: (file, args) => deps.run(file, file === "git" ? ["-C", dir, ...args] : args) };
|
|
37061
37081
|
const failures = [];
|
|
37062
37082
|
const skipped = [];
|
|
@@ -39548,6 +39568,11 @@ function basePolicyBlocksImmediateMerge(message2) {
|
|
|
39548
39568
|
return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message2);
|
|
39549
39569
|
}
|
|
39550
39570
|
var PR_MERGE_ENQUEUED_EXIT_CODE = 2;
|
|
39571
|
+
function prMergeGateExitCode(gate, explicitAuto) {
|
|
39572
|
+
if (gate.action === "cleanup") return void 0;
|
|
39573
|
+
if (gate.action === "enqueued") return explicitAuto ? void 0 : PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
39574
|
+
return 1;
|
|
39575
|
+
}
|
|
39551
39576
|
function ghPrMergeLocalBranchDeleteWarning(message2) {
|
|
39552
39577
|
return /used by worktree|cannot delete branch/i.test(message2);
|
|
39553
39578
|
}
|
|
@@ -45745,19 +45770,23 @@ ${list}`);
|
|
|
45745
45770
|
return snapshot.merged ? "MERGED" : snapshot.state.toUpperCase();
|
|
45746
45771
|
})
|
|
45747
45772
|
);
|
|
45773
|
+
const autoRequested = Boolean(o.auto || upgradedToAuto);
|
|
45774
|
+
const autoMergeRequest = autoRequested && stateRead.ok && stateRead.state.trim().toUpperCase() === "OPEN" ? (await execFileP("gh", ["pr", "view", number, ...repoArgs, "--json", "autoMergeRequest", "--jq", '.autoMergeRequest // ""'], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() : void 0;
|
|
45748
45775
|
const gate = decidePrMergeCleanupGate({
|
|
45749
45776
|
state: stateRead.ok ? stateRead.state : void 0,
|
|
45750
45777
|
stateError: stateRead.ok ? void 0 : stateRead.error,
|
|
45751
|
-
autoRequested
|
|
45778
|
+
autoRequested,
|
|
45779
|
+
autoMergeRequest
|
|
45752
45780
|
});
|
|
45781
|
+
const gateExitCode = prMergeGateExitCode(gate, Boolean(o.auto));
|
|
45753
45782
|
if (gate.action === "enqueued") {
|
|
45754
45783
|
const state = stateRead.ok ? stateRead.state : "";
|
|
45755
45784
|
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
45756
45785
|
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
45757
45786
|
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
45758
|
-
if (
|
|
45759
|
-
console.warn(`pr merge: exiting ${
|
|
45760
|
-
process.exitCode =
|
|
45787
|
+
if (gateExitCode !== void 0) {
|
|
45788
|
+
console.warn(`pr merge: exiting ${gateExitCode} so a chained command does not treat this as a completed merge.`);
|
|
45789
|
+
process.exitCode = gateExitCode;
|
|
45761
45790
|
}
|
|
45762
45791
|
return;
|
|
45763
45792
|
}
|
|
@@ -45772,7 +45801,7 @@ ${list}`);
|
|
|
45772
45801
|
...branchUpdated ? { branchUpdated } : {}
|
|
45773
45802
|
}));
|
|
45774
45803
|
console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
|
|
45775
|
-
process.exitCode =
|
|
45804
|
+
process.exitCode = gateExitCode;
|
|
45776
45805
|
return;
|
|
45777
45806
|
}
|
|
45778
45807
|
const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
|
|
@@ -47591,16 +47620,732 @@ function suiteStatusDeps(github, registry2) {
|
|
|
47591
47620
|
};
|
|
47592
47621
|
}
|
|
47593
47622
|
|
|
47623
|
+
// src/packages-adopt-export.ts
|
|
47624
|
+
var import_node_child_process18 = require("node:child_process");
|
|
47625
|
+
var import_node_fs45 = require("node:fs");
|
|
47626
|
+
var import_node_os21 = require("node:os");
|
|
47627
|
+
var import_node_path44 = require("node:path");
|
|
47628
|
+
var import_node_util6 = require("node:util");
|
|
47629
|
+
init_cli_shared();
|
|
47630
|
+
|
|
47631
|
+
// ../scripts/package_export.py
|
|
47632
|
+
var package_export_default = `#!/usr/bin/env python3
|
|
47633
|
+
"""Export and offline verification of exact original package registry archives (#7805)."""
|
|
47634
|
+
import base64
|
|
47635
|
+
import binascii
|
|
47636
|
+
from datetime import datetime, timedelta, timezone
|
|
47637
|
+
import gzip
|
|
47638
|
+
import hashlib
|
|
47639
|
+
import hmac
|
|
47640
|
+
import io
|
|
47641
|
+
import json
|
|
47642
|
+
import os
|
|
47643
|
+
import pathlib
|
|
47644
|
+
import re
|
|
47645
|
+
import stat
|
|
47646
|
+
import struct
|
|
47647
|
+
import sys
|
|
47648
|
+
import tarfile
|
|
47649
|
+
import zipfile
|
|
47650
|
+
import zlib
|
|
47651
|
+
import urllib.error
|
|
47652
|
+
import urllib.request
|
|
47653
|
+
from urllib.parse import urlsplit
|
|
47654
|
+
|
|
47655
|
+
PACKAGES = ("@mutmutco/mmwork-ds", "@mutmutco/mmwork-screens")
|
|
47656
|
+
MAX_METADATA_BYTES = 2 * 1024 * 1024
|
|
47657
|
+
MAX_TARBALL_BYTES = 64 * 1024 * 1024
|
|
47658
|
+
MAX_ARTIFACT_BYTES = 160 * 1024 * 1024
|
|
47659
|
+
MAX_ZIP_DIRECTORY_BYTES = 8192
|
|
47660
|
+
MAX_UNPACKED_BYTES = 256 * 1024 * 1024
|
|
47661
|
+
MAX_MEMBERS = 10000 # Yielded entries; tarfile consumes PAX/GNU extension headers internally.
|
|
47662
|
+
MAX_PATH_LENGTH = 1024
|
|
47663
|
+
MAX_PATH_DEPTH = 32
|
|
47664
|
+
MAX_COMPONENT_LENGTH = 255
|
|
47665
|
+
WINDOWS_DEVICE = re.compile(r"con|conin\\$|conout\\$|prn|aux|nul|com[1-9]|lpt[1-9]", re.IGNORECASE)
|
|
47666
|
+
EXACT_VERSION = re.compile(
|
|
47667
|
+
r"(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)"
|
|
47668
|
+
r"(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"
|
|
47669
|
+
r"(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?"
|
|
47670
|
+
)
|
|
47671
|
+
|
|
47672
|
+
|
|
47673
|
+
def reject_constant(_):
|
|
47674
|
+
raise ValueError("nonstandard-json")
|
|
47675
|
+
|
|
47676
|
+
|
|
47677
|
+
def unique_object(pairs):
|
|
47678
|
+
result = {}
|
|
47679
|
+
for key, value in pairs:
|
|
47680
|
+
if key in result:
|
|
47681
|
+
raise ValueError("registry-metadata")
|
|
47682
|
+
result[key] = value
|
|
47683
|
+
return result
|
|
47684
|
+
|
|
47685
|
+
|
|
47686
|
+
def verify_artifact_digest(provider_digest, zip_bytes):
|
|
47687
|
+
"""Bind whole bytes to an independently trusted provider digest, not an embedded claim.
|
|
47688
|
+
|
|
47689
|
+
This does not authenticate the digest's source or validate ZIP members.
|
|
47690
|
+
"""
|
|
47691
|
+
if (type(zip_bytes) is not bytes or not 0 < len(zip_bytes) <= MAX_ARTIFACT_BYTES
|
|
47692
|
+
or type(provider_digest) is not str
|
|
47693
|
+
or not re.fullmatch(r"sha256:[0-9a-f]{64}", provider_digest)):
|
|
47694
|
+
raise ValueError("artifact-integrity")
|
|
47695
|
+
digest = hashlib.sha256(zip_bytes).hexdigest()
|
|
47696
|
+
if not hmac.compare_digest(provider_digest[7:], digest):
|
|
47697
|
+
raise ValueError("artifact-integrity")
|
|
47698
|
+
return {"artifactSha256": digest, "artifactBytes": len(zip_bytes)}
|
|
47699
|
+
|
|
47700
|
+
|
|
47701
|
+
def verify_artifact_receipt(provider_bytes, zip_bytes, expected, now):
|
|
47702
|
+
"""Check separately supplied provider metadata against trusted external selectors.
|
|
47703
|
+
|
|
47704
|
+
The caller must obtain metadata and selectors independently of the artifact.
|
|
47705
|
+
This cannot authenticate their origin or establish workflow/requester authority.
|
|
47706
|
+
Optional node_id, url, archive_download_url and updated_at are ignored, not validated.
|
|
47707
|
+
"""
|
|
47708
|
+
keys = {"artifactId", "artifactName", "repositoryId", "runId", "headSha", "headBranch", "digest"}
|
|
47709
|
+
try:
|
|
47710
|
+
if type(expected) is not dict:
|
|
47711
|
+
raise ValueError()
|
|
47712
|
+
expected = expected.copy()
|
|
47713
|
+
if set(expected) != keys:
|
|
47714
|
+
raise ValueError()
|
|
47715
|
+
if any(type(expected[key]) is not int or not 0 < expected[key] < (1 << 53)
|
|
47716
|
+
for key in ("artifactId", "repositoryId", "runId")):
|
|
47717
|
+
raise ValueError()
|
|
47718
|
+
if (type(expected["headSha"]) is not str or not re.fullmatch(r"[0-9a-f]{40}", expected["headSha"])
|
|
47719
|
+
or any(type(expected[key]) is not str or not 0 < len(expected[key]) <= 128
|
|
47720
|
+
or any(ord(char) < 33 or ord(char) > 126 for char in expected[key])
|
|
47721
|
+
for key in ("artifactName", "headBranch"))):
|
|
47722
|
+
raise ValueError()
|
|
47723
|
+
if type(provider_bytes) is not bytes or not 0 < len(provider_bytes) <= MAX_METADATA_BYTES:
|
|
47724
|
+
raise ValueError()
|
|
47725
|
+
provider = json.loads(provider_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
47726
|
+
parse_constant=reject_constant)
|
|
47727
|
+
required = {"id", "name", "size_in_bytes", "digest", "expired", "created_at", "expires_at", "workflow_run"}
|
|
47728
|
+
if (type(provider) is not dict or not required <= set(provider)
|
|
47729
|
+
or set(provider) - required - {"node_id", "url", "archive_download_url", "updated_at"}):
|
|
47730
|
+
raise ValueError()
|
|
47731
|
+
run = provider["workflow_run"]
|
|
47732
|
+
if (type(run) is not dict or set(run) != {"id", "repository_id", "head_repository_id", "head_sha", "head_branch"}
|
|
47733
|
+
or provider["expired"] is not False):
|
|
47734
|
+
raise ValueError()
|
|
47735
|
+
if type(now) is not datetime or now.utcoffset() != timedelta(0):
|
|
47736
|
+
raise ValueError()
|
|
47737
|
+
dates = []
|
|
47738
|
+
for key in ("created_at", "expires_at"):
|
|
47739
|
+
value = provider[key]
|
|
47740
|
+
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):
|
|
47741
|
+
raise ValueError()
|
|
47742
|
+
dates.append(datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc))
|
|
47743
|
+
created, expires = dates
|
|
47744
|
+
if not created <= now < expires or not timedelta(0) < expires - created <= timedelta(days=1):
|
|
47745
|
+
raise ValueError()
|
|
47746
|
+
for actual, wanted in [(provider["id"], expected["artifactId"]),
|
|
47747
|
+
(run["id"], expected["runId"]),
|
|
47748
|
+
(run["repository_id"], expected["repositoryId"]),
|
|
47749
|
+
(run["head_repository_id"], expected["repositoryId"])]:
|
|
47750
|
+
if type(actual) is not int or actual != wanted:
|
|
47751
|
+
raise ValueError()
|
|
47752
|
+
for actual, wanted in [(provider["name"], expected["artifactName"]),
|
|
47753
|
+
(provider["digest"], expected["digest"]),
|
|
47754
|
+
(run["head_sha"], expected["headSha"]),
|
|
47755
|
+
(run["head_branch"], expected["headBranch"])]:
|
|
47756
|
+
if type(actual) is not str or actual != wanted:
|
|
47757
|
+
raise ValueError()
|
|
47758
|
+
receipt = verify_artifact_digest(expected["digest"], zip_bytes)
|
|
47759
|
+
if type(provider["size_in_bytes"]) is not int or provider["size_in_bytes"] != len(zip_bytes):
|
|
47760
|
+
raise ValueError()
|
|
47761
|
+
except (ValueError, RecursionError):
|
|
47762
|
+
raise ValueError("artifact-receipt") from None
|
|
47763
|
+
return {**expected, **receipt}
|
|
47764
|
+
|
|
47765
|
+
|
|
47766
|
+
def inspect_artifact_zip(zip_bytes):
|
|
47767
|
+
"""Read five proposed export members in memory without extracting or executing.
|
|
47768
|
+
|
|
47769
|
+
Authenticate whole artifact bytes before this stage. Returned member bytes still
|
|
47770
|
+
require registry integrity checks; the manifest is untrusted, not an authority.
|
|
47771
|
+
Byte/output limits are not a total process-memory budget, including codec remainders.
|
|
47772
|
+
"""
|
|
47773
|
+
limits = {"manifest.json": MAX_METADATA_BYTES,
|
|
47774
|
+
"mmwork-ds.metadata.json": MAX_METADATA_BYTES, "mmwork-ds.tgz": MAX_TARBALL_BYTES,
|
|
47775
|
+
"mmwork-screens.metadata.json": MAX_METADATA_BYTES, "mmwork-screens.tgz": MAX_TARBALL_BYTES}
|
|
47776
|
+
try:
|
|
47777
|
+
if type(zip_bytes) is not bytes or not 0 < len(zip_bytes) <= MAX_ARTIFACT_BYTES:
|
|
47778
|
+
raise ValueError()
|
|
47779
|
+
stream = io.BytesIO(zip_bytes)
|
|
47780
|
+
# ZipFile allocates every central-directory entry before application checks.
|
|
47781
|
+
# Use stdlib's bounded EOCD reader and struct definitions for a strict profile audit.
|
|
47782
|
+
end = zipfile._EndRecData(stream)
|
|
47783
|
+
if (end is None or end[zipfile._ECD_SIGNATURE] != zipfile.stringEndArchive
|
|
47784
|
+
or end[zipfile._ECD_DISK_NUMBER] != 0
|
|
47785
|
+
or end[zipfile._ECD_DISK_START] != 0
|
|
47786
|
+
or end[zipfile._ECD_ENTRIES_THIS_DISK] != len(limits)
|
|
47787
|
+
or end[zipfile._ECD_ENTRIES_TOTAL] != len(limits)
|
|
47788
|
+
or not 0 < end[zipfile._ECD_SIZE] <= MAX_ZIP_DIRECTORY_BYTES
|
|
47789
|
+
or end[zipfile._ECD_COMMENT_SIZE] != 0
|
|
47790
|
+
or end[zipfile._ECD_LOCATION] + zipfile.sizeEndCentDir != len(zip_bytes)
|
|
47791
|
+
or end[zipfile._ECD_OFFSET] + end[zipfile._ECD_SIZE] != end[zipfile._ECD_LOCATION]):
|
|
47792
|
+
raise ValueError()
|
|
47793
|
+
directory = memoryview(zip_bytes)[end[zipfile._ECD_OFFSET]:end[zipfile._ECD_LOCATION]]
|
|
47794
|
+
allowed = {name.encode("ascii") for name in limits}
|
|
47795
|
+
seen = set()
|
|
47796
|
+
cursor = 0
|
|
47797
|
+
# ZipFile accepts short variable-field reads, so audit declared boundaries first.
|
|
47798
|
+
while cursor < len(directory):
|
|
47799
|
+
fields = struct.unpack_from(zipfile.structCentralDir, directory, cursor)
|
|
47800
|
+
start = cursor + zipfile.sizeCentralDir
|
|
47801
|
+
stop = start + fields[zipfile._CD_FILENAME_LENGTH]
|
|
47802
|
+
if (fields[zipfile._CD_SIGNATURE] != zipfile.stringCentralDir
|
|
47803
|
+
or fields[zipfile._CD_EXTRA_FIELD_LENGTH] != 0 or fields[zipfile._CD_COMMENT_LENGTH] != 0
|
|
47804
|
+
or stop > len(directory)):
|
|
47805
|
+
raise ValueError()
|
|
47806
|
+
name = bytes(directory[start:stop])
|
|
47807
|
+
if name not in allowed or name in seen:
|
|
47808
|
+
raise ValueError()
|
|
47809
|
+
seen.add(name)
|
|
47810
|
+
cursor = stop
|
|
47811
|
+
if cursor != len(directory) or seen != allowed:
|
|
47812
|
+
raise ValueError()
|
|
47813
|
+
with zipfile.ZipFile(stream, "r") as bundle:
|
|
47814
|
+
entries = bundle.infolist()
|
|
47815
|
+
if (bundle.start_dir != end[zipfile._ECD_OFFSET]
|
|
47816
|
+
or len(entries) != len(limits) or {entry.orig_filename for entry in entries} != set(limits)
|
|
47817
|
+
or min(entry.header_offset for entry in entries) != 0):
|
|
47818
|
+
raise ValueError()
|
|
47819
|
+
result = {}
|
|
47820
|
+
entries.sort(key=lambda entry: entry.header_offset)
|
|
47821
|
+
for index, entry in enumerate(entries):
|
|
47822
|
+
if (entry.filename != entry.orig_filename or entry.is_dir()
|
|
47823
|
+
or stat.S_IFMT(entry.external_attr >> 16) not in (0, stat.S_IFREG)
|
|
47824
|
+
or entry.external_attr & 0x10 or entry.volume != 0
|
|
47825
|
+
or entry.flag_bits & ~(0x8 | 0x800)
|
|
47826
|
+
or entry.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED)
|
|
47827
|
+
or entry.comment or entry.extra
|
|
47828
|
+
or not 0 < entry.file_size <= limits[entry.filename]
|
|
47829
|
+
or not 0 < entry.compress_size <= MAX_ARTIFACT_BYTES):
|
|
47830
|
+
raise ValueError()
|
|
47831
|
+
header = entry.FileHeader(zip64=False)
|
|
47832
|
+
start = entry.header_offset + len(header)
|
|
47833
|
+
data_end = start + entry.compress_size
|
|
47834
|
+
next_offset = entries[index + 1].header_offset if index + 1 < len(entries) else bundle.start_dir
|
|
47835
|
+
if zip_bytes[entry.header_offset:start] != header or data_end > next_offset:
|
|
47836
|
+
raise ValueError()
|
|
47837
|
+
span = next_offset - data_end
|
|
47838
|
+
if span not in ((12, 16) if entry.flag_bits & 0x8 else (0,)):
|
|
47839
|
+
raise ValueError()
|
|
47840
|
+
descriptor = zip_bytes[data_end:next_offset]
|
|
47841
|
+
if entry.flag_bits & 0x8:
|
|
47842
|
+
fields = struct.pack("<III", entry.CRC, entry.compress_size, entry.file_size)
|
|
47843
|
+
if descriptor not in (fields, b"PK\\x07\\x08" + fields):
|
|
47844
|
+
raise ValueError()
|
|
47845
|
+
elif descriptor:
|
|
47846
|
+
raise ValueError()
|
|
47847
|
+
compressed = memoryview(zip_bytes)[start:data_end]
|
|
47848
|
+
if entry.compress_type == zipfile.ZIP_STORED:
|
|
47849
|
+
if entry.compress_size != entry.file_size:
|
|
47850
|
+
raise ValueError()
|
|
47851
|
+
payload = bytes(compressed)
|
|
47852
|
+
else:
|
|
47853
|
+
codec = zlib.decompressobj(wbits=-15)
|
|
47854
|
+
payload = codec.decompress(compressed, entry.file_size + 1)
|
|
47855
|
+
# ZipExtFile clips to the declared size and may ignore trailing data.
|
|
47856
|
+
if not codec.eof or codec.unused_data or codec.unconsumed_tail:
|
|
47857
|
+
raise ValueError()
|
|
47858
|
+
if len(payload) != entry.file_size or zlib.crc32(payload) != entry.CRC:
|
|
47859
|
+
raise ValueError()
|
|
47860
|
+
result[entry.filename] = payload
|
|
47861
|
+
return result
|
|
47862
|
+
except (ValueError, OSError, EOFError, NotImplementedError, zipfile.BadZipFile, zlib.error, struct.error):
|
|
47863
|
+
raise ValueError("artifact-members") from None
|
|
47864
|
+
|
|
47865
|
+
|
|
47866
|
+
def inspect_archive(name, version, tarball_bytes):
|
|
47867
|
+
seen = set()
|
|
47868
|
+
files = set()
|
|
47869
|
+
directories = set()
|
|
47870
|
+
package = None
|
|
47871
|
+
end = 0
|
|
47872
|
+
try:
|
|
47873
|
+
with gzip.GzipFile(fileobj=io.BytesIO(tarball_bytes)) as compressed:
|
|
47874
|
+
unpacked = compressed.read(MAX_UNPACKED_BYTES + 1)
|
|
47875
|
+
if len(unpacked) > MAX_UNPACKED_BYTES:
|
|
47876
|
+
raise ValueError()
|
|
47877
|
+
with tarfile.open(fileobj=io.BytesIO(unpacked), mode="r:") as archive:
|
|
47878
|
+
for index, entry in enumerate(archive):
|
|
47879
|
+
if (index >= MAX_MEMBERS or entry.size < 0 or entry.size > MAX_UNPACKED_BYTES
|
|
47880
|
+
or (entry.isdir() and entry.size != 0)):
|
|
47881
|
+
raise ValueError()
|
|
47882
|
+
end = entry.offset_data + ((entry.size + 511) // 512) * 512
|
|
47883
|
+
# Bound names before splitting or allocating retained ancestor prefixes.
|
|
47884
|
+
if len(entry.name) > MAX_PATH_LENGTH:
|
|
47885
|
+
raise ValueError()
|
|
47886
|
+
path = entry.name.rstrip("/") if entry.isdir() else entry.name
|
|
47887
|
+
if path.count("/") >= MAX_PATH_DEPTH:
|
|
47888
|
+
raise ValueError()
|
|
47889
|
+
parts = path.split("/")
|
|
47890
|
+
if (not (entry.isreg() or entry.isdir()) or entry.issparse()
|
|
47891
|
+
or parts[0] != "package" or "\\\\" in path or ":" in path
|
|
47892
|
+
or any(ord(char) < 32 or ord(char) > 126 for char in path)
|
|
47893
|
+
or any(len(part) > MAX_COMPONENT_LENGTH or part in ("", ".", "..")
|
|
47894
|
+
or part.rstrip(". ") != part for part in parts)
|
|
47895
|
+
or path.lower() in seen):
|
|
47896
|
+
raise ValueError()
|
|
47897
|
+
seen.add(path.lower())
|
|
47898
|
+
folded = path.lower()
|
|
47899
|
+
parents = ["/".join(parts[:i]).lower() for i in range(1, len(parts))]
|
|
47900
|
+
if any(parent in files for parent in parents) or (entry.isreg() and folded in directories):
|
|
47901
|
+
raise ValueError()
|
|
47902
|
+
directories.update(parents)
|
|
47903
|
+
if entry.isdir():
|
|
47904
|
+
directories.add(folded)
|
|
47905
|
+
else:
|
|
47906
|
+
files.add(folded)
|
|
47907
|
+
for part in parts:
|
|
47908
|
+
lower = part.lower()
|
|
47909
|
+
if (WINDOWS_DEVICE.fullmatch(part.split(".", 1)[0].rstrip(" "))
|
|
47910
|
+
or any(char in part for char in '<>"|?*')
|
|
47911
|
+
or lower in (".npmrc", ".netrc", ".pypirc", ".git", ".gitconfig", ".git-credentials", ".yarnrc", ".yarnrc.yml")
|
|
47912
|
+
or lower.startswith(".env") or lower.endswith((".env", ".pem", ".key", ".p12", ".pfx", ".log"))):
|
|
47913
|
+
raise ValueError()
|
|
47914
|
+
if path == "package/package.json":
|
|
47915
|
+
if not entry.isreg() or entry.size > MAX_METADATA_BYTES:
|
|
47916
|
+
raise ValueError()
|
|
47917
|
+
with archive.extractfile(entry) as file:
|
|
47918
|
+
package = json.loads(file.read().decode("utf-8"), object_pairs_hook=unique_object,
|
|
47919
|
+
parse_constant=reject_constant)
|
|
47920
|
+
if len(unpacked) - end < 1024 or any(unpacked[end:]):
|
|
47921
|
+
raise ValueError()
|
|
47922
|
+
if type(package) is not dict or package.get("name") != name or package.get("version") != version:
|
|
47923
|
+
raise ValueError()
|
|
47924
|
+
except (ValueError, tarfile.TarError, OSError, EOFError, RecursionError, zlib.error):
|
|
47925
|
+
raise ValueError("archive-members") from None
|
|
47926
|
+
|
|
47927
|
+
|
|
47928
|
+
def check_exact_version(version):
|
|
47929
|
+
match = EXACT_VERSION.fullmatch(version) if type(version) is str and len(version) <= 128 else None
|
|
47930
|
+
if not match or any(
|
|
47931
|
+
part.isdigit() and len(part) > 1 and part.startswith("0")
|
|
47932
|
+
for part in (match[4] or "").split(".")
|
|
47933
|
+
):
|
|
47934
|
+
raise ValueError("package-identity")
|
|
47935
|
+
|
|
47936
|
+
|
|
47937
|
+
def check_tarball_url(url):
|
|
47938
|
+
# Accept literal paths only: never reinterpret percent escapes or empty delimiters.
|
|
47939
|
+
if (type(url) is not str or any(ord(char) < 33 or ord(char) > 126 for char in url)
|
|
47940
|
+
or any(char in url for char in "\\\\%?#")):
|
|
47941
|
+
raise ValueError("registry-url")
|
|
47942
|
+
try:
|
|
47943
|
+
location = urlsplit(url)
|
|
47944
|
+
except ValueError:
|
|
47945
|
+
raise ValueError("registry-url") from None
|
|
47946
|
+
if (location.scheme != "https" or location.netloc != "npm.pkg.github.com"
|
|
47947
|
+
or not location.path.startswith("/download/")
|
|
47948
|
+
or any(part in ("", ".", "..") for part in location.path.split("/")[1:])):
|
|
47949
|
+
raise ValueError("registry-url")
|
|
47950
|
+
|
|
47951
|
+
|
|
47952
|
+
def verify_registry_archive(name, version, metadata_bytes, tarball_bytes):
|
|
47953
|
+
if (type(metadata_bytes) is not bytes or not 0 < len(metadata_bytes) <= MAX_METADATA_BYTES
|
|
47954
|
+
or type(tarball_bytes) is not bytes or not 0 < len(tarball_bytes) <= MAX_TARBALL_BYTES):
|
|
47955
|
+
raise ValueError("byte-limit")
|
|
47956
|
+
if type(name) is not str or name not in PACKAGES:
|
|
47957
|
+
raise ValueError("package-identity")
|
|
47958
|
+
check_exact_version(version)
|
|
47959
|
+
try:
|
|
47960
|
+
metadata = json.loads(metadata_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
47961
|
+
parse_constant=reject_constant)
|
|
47962
|
+
if type(metadata) is not dict or type(metadata.get("dist")) is not dict:
|
|
47963
|
+
raise ValueError()
|
|
47964
|
+
full_integrity = metadata["dist"].get("integrity")
|
|
47965
|
+
if type(full_integrity) is not str or not full_integrity.startswith("sha512-"):
|
|
47966
|
+
raise ValueError()
|
|
47967
|
+
registry_digest = base64.b64decode(full_integrity[7:], validate=True)
|
|
47968
|
+
if (len(registry_digest) != hashlib.sha512().digest_size
|
|
47969
|
+
or base64.b64encode(registry_digest).decode() != full_integrity[7:]):
|
|
47970
|
+
raise ValueError()
|
|
47971
|
+
except (ValueError, binascii.Error, RecursionError):
|
|
47972
|
+
raise ValueError("registry-metadata") from None
|
|
47973
|
+
if metadata.get("name") != name or metadata.get("version") != version:
|
|
47974
|
+
raise ValueError("package-identity")
|
|
47975
|
+
check_tarball_url(metadata["dist"].get("tarball"))
|
|
47976
|
+
if not hmac.compare_digest(registry_digest, hashlib.sha512(tarball_bytes).digest()):
|
|
47977
|
+
raise ValueError("integrity-mismatch")
|
|
47978
|
+
inspect_archive(name, version, tarball_bytes)
|
|
47979
|
+
return {
|
|
47980
|
+
"name": name,
|
|
47981
|
+
"version": version,
|
|
47982
|
+
"integrity": metadata["dist"]["integrity"],
|
|
47983
|
+
"metadataSha256": hashlib.sha256(metadata_bytes).hexdigest(),
|
|
47984
|
+
"tarballSha256": hashlib.sha256(tarball_bytes).hexdigest(),
|
|
47985
|
+
"metadataBytes": len(metadata_bytes),
|
|
47986
|
+
"tarballBytes": len(tarball_bytes),
|
|
47987
|
+
}
|
|
47988
|
+
|
|
47989
|
+
|
|
47990
|
+
# --- Export (runs only inside .github/workflows/package-export.yml) ---------------------------
|
|
47991
|
+
TRUSTED_REPOSITORY = "mutmutco/MMI-Hub"
|
|
47992
|
+
TRUSTED_WORKFLOW = ".github/workflows/package-export.yml"
|
|
47993
|
+
TRUSTED_REF = "refs/heads/development"
|
|
47994
|
+
REGISTRY = "https://npm.pkg.github.com"
|
|
47995
|
+
MAX_PACKUMENT_BYTES = 16 * 1024 * 1024
|
|
47996
|
+
VERSION_INPUTS = {"@mutmutco/mmwork-ds": "DS_VERSION", "@mutmutco/mmwork-screens": "SCREENS_VERSION"}
|
|
47997
|
+
|
|
47998
|
+
|
|
47999
|
+
def dispatch_identity(env):
|
|
48000
|
+
"""Refuse any run that is not this workflow, dispatched on the trusted ref of the trusted repo."""
|
|
48001
|
+
try:
|
|
48002
|
+
identity = {key: env[name] for key, name in [
|
|
48003
|
+
("repository", "GITHUB_REPOSITORY"), ("repositoryId", "GITHUB_REPOSITORY_ID"),
|
|
48004
|
+
("workflowRef", "GITHUB_WORKFLOW_REF"), ("ref", "GITHUB_REF"), ("headSha", "GITHUB_SHA"),
|
|
48005
|
+
("runId", "GITHUB_RUN_ID"), ("runAttempt", "GITHUB_RUN_ATTEMPT"),
|
|
48006
|
+
("event", "GITHUB_EVENT_NAME"), ("actor", "GITHUB_ACTOR"),
|
|
48007
|
+
("triggeringActor", "GITHUB_TRIGGERING_ACTOR")]}
|
|
48008
|
+
except KeyError:
|
|
48009
|
+
raise ValueError("dispatch-identity") from None
|
|
48010
|
+
if (identity["repository"] != TRUSTED_REPOSITORY or identity["ref"] != TRUSTED_REF
|
|
48011
|
+
or identity["event"] != "workflow_dispatch"
|
|
48012
|
+
or identity["workflowRef"] != f"{TRUSTED_REPOSITORY}/{TRUSTED_WORKFLOW}@{TRUSTED_REF}"
|
|
48013
|
+
or not re.fullmatch(r"[0-9a-f]{40}", identity["headSha"])
|
|
48014
|
+
or not all(re.fullmatch(r"[1-9][0-9]{0,15}", identity[key])
|
|
48015
|
+
for key in ("repositoryId", "runId", "runAttempt"))
|
|
48016
|
+
or not all(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]{0,38}(\\[bot\\])?", identity[key])
|
|
48017
|
+
for key in ("actor", "triggeringActor"))):
|
|
48018
|
+
raise ValueError("dispatch-identity")
|
|
48019
|
+
return identity
|
|
48020
|
+
|
|
48021
|
+
|
|
48022
|
+
class HttpsOnlyRedirect(urllib.request.HTTPRedirectHandler):
|
|
48023
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
48024
|
+
if urlsplit(newurl).scheme != "https":
|
|
48025
|
+
raise ValueError("registry-url")
|
|
48026
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
48027
|
+
|
|
48028
|
+
|
|
48029
|
+
def default_opener():
|
|
48030
|
+
# No proxy handler: the token goes only to the registry host named in the request.
|
|
48031
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}), HttpsOnlyRedirect())
|
|
48032
|
+
|
|
48033
|
+
|
|
48034
|
+
def fetch(opener, url, token, limit):
|
|
48035
|
+
request = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
48036
|
+
# Unredirected: the registry's redirect to blob storage never carries the token.
|
|
48037
|
+
request.add_unredirected_header("Authorization", f"Bearer {token}")
|
|
48038
|
+
with opener.open(request, timeout=60) as response:
|
|
48039
|
+
body = response.read(limit + 1)
|
|
48040
|
+
if len(body) > limit or not body:
|
|
48041
|
+
raise ValueError("byte-limit")
|
|
48042
|
+
return body
|
|
48043
|
+
|
|
48044
|
+
|
|
48045
|
+
def export_package(opener, token, name, version):
|
|
48046
|
+
check_exact_version(version)
|
|
48047
|
+
try:
|
|
48048
|
+
packument_bytes = fetch(opener, f"{REGISTRY}/{name.replace('/', '%2f')}", token, MAX_PACKUMENT_BYTES)
|
|
48049
|
+
except urllib.error.HTTPError as error:
|
|
48050
|
+
# Name the package and the registry's own short reason (body only; no request headers).
|
|
48051
|
+
reason = error.read(300).decode("utf-8", "replace").replace(token, "***")
|
|
48052
|
+
raise ValueError(f"registry {name}: HTTP {error.code} {reason}") from None
|
|
48053
|
+
try:
|
|
48054
|
+
packument = json.loads(packument_bytes.decode("utf-8"), object_pairs_hook=unique_object,
|
|
48055
|
+
parse_constant=reject_constant)
|
|
48056
|
+
if (type(packument) is not dict or packument.get("name") != name
|
|
48057
|
+
or type(packument.get("versions")) is not dict):
|
|
48058
|
+
raise ValueError()
|
|
48059
|
+
document = packument["versions"][version]
|
|
48060
|
+
if type(document) is not dict or type(document.get("dist")) is not dict:
|
|
48061
|
+
raise ValueError()
|
|
48062
|
+
except (ValueError, KeyError, RecursionError):
|
|
48063
|
+
raise ValueError("registry-metadata") from None
|
|
48064
|
+
check_tarball_url(document["dist"].get("tarball"))
|
|
48065
|
+
metadata_bytes = json.dumps(document, sort_keys=True, separators=(",", ":")).encode()
|
|
48066
|
+
tarball_bytes = fetch(opener, document["dist"]["tarball"], token, MAX_TARBALL_BYTES)
|
|
48067
|
+
receipt = verify_registry_archive(name, version, metadata_bytes, tarball_bytes)
|
|
48068
|
+
receipt["packumentSha256"] = hashlib.sha256(packument_bytes).hexdigest()
|
|
48069
|
+
return metadata_bytes, tarball_bytes, receipt
|
|
48070
|
+
|
|
48071
|
+
|
|
48072
|
+
def export(env, out_dir, opener=None):
|
|
48073
|
+
"""Write exactly the five artifact members into a new directory. Nothing else is written."""
|
|
48074
|
+
identity = dispatch_identity(env)
|
|
48075
|
+
versions = {name: env.get(variable) for name, variable in VERSION_INPUTS.items()}
|
|
48076
|
+
for version in versions.values():
|
|
48077
|
+
check_exact_version(version)
|
|
48078
|
+
token = env.get("GITHUB_PACKAGES_TOKEN")
|
|
48079
|
+
if type(token) is not str or not token.strip():
|
|
48080
|
+
raise ValueError("registry-token")
|
|
48081
|
+
opener = opener or default_opener()
|
|
48082
|
+
files = {}
|
|
48083
|
+
receipts = []
|
|
48084
|
+
for name in PACKAGES:
|
|
48085
|
+
metadata_bytes, tarball_bytes, receipt = export_package(opener, token, name, versions[name])
|
|
48086
|
+
short = name.split("/")[1]
|
|
48087
|
+
files[f"{short}.metadata.json"] = metadata_bytes
|
|
48088
|
+
files[f"{short}.tgz"] = tarball_bytes
|
|
48089
|
+
receipts.append(receipt)
|
|
48090
|
+
manifest = {"schema": 1, "registry": REGISTRY, "source": identity, "packages": receipts}
|
|
48091
|
+
files["manifest.json"] = json.dumps(manifest, sort_keys=True, indent=2).encode()
|
|
48092
|
+
if any(token.encode() in payload for payload in files.values()):
|
|
48093
|
+
raise ValueError("registry-token")
|
|
48094
|
+
out = pathlib.Path(out_dir)
|
|
48095
|
+
out.mkdir(parents=True, exist_ok=False)
|
|
48096
|
+
for member, payload in files.items():
|
|
48097
|
+
(out / member).write_bytes(payload)
|
|
48098
|
+
return manifest
|
|
48099
|
+
|
|
48100
|
+
|
|
48101
|
+
def verify_export(provider_bytes, zip_bytes, expected, versions, now):
|
|
48102
|
+
"""Consumer side: bind the artifact to external selectors, then re-verify every package offline.
|
|
48103
|
+
|
|
48104
|
+
versions maps each allowlisted package to the exact version the consumer asked for. The
|
|
48105
|
+
manifest inside the artifact is checked against recomputed receipts, never trusted.
|
|
48106
|
+
"""
|
|
48107
|
+
receipt = verify_artifact_receipt(provider_bytes, zip_bytes, expected, now)
|
|
48108
|
+
members = inspect_artifact_zip(zip_bytes)
|
|
48109
|
+
if type(versions) is not dict or set(versions) != set(PACKAGES):
|
|
48110
|
+
raise ValueError("package-identity")
|
|
48111
|
+
packages = []
|
|
48112
|
+
for name in PACKAGES:
|
|
48113
|
+
short = name.split("/")[1]
|
|
48114
|
+
packages.append(verify_registry_archive(name, versions[name], members[f"{short}.metadata.json"],
|
|
48115
|
+
members[f"{short}.tgz"]))
|
|
48116
|
+
try:
|
|
48117
|
+
manifest = json.loads(members["manifest.json"].decode("utf-8"), object_pairs_hook=unique_object,
|
|
48118
|
+
parse_constant=reject_constant)
|
|
48119
|
+
source = manifest["source"]
|
|
48120
|
+
claimed = manifest["packages"]
|
|
48121
|
+
if (manifest.get("registry") != REGISTRY or type(claimed) is not list or len(claimed) != len(packages)
|
|
48122
|
+
or any(type(entry) is not dict or {key: entry.get(key) for key in package} != package
|
|
48123
|
+
for entry, package in zip(claimed, packages))
|
|
48124
|
+
or source.get("repository") != TRUSTED_REPOSITORY
|
|
48125
|
+
or source.get("workflowRef") != f"{TRUSTED_REPOSITORY}/{TRUSTED_WORKFLOW}@{TRUSTED_REF}"
|
|
48126
|
+
or source.get("headSha") != expected["headSha"]
|
|
48127
|
+
or source.get("runId") != str(expected["runId"])
|
|
48128
|
+
or source.get("repositoryId") != str(expected["repositoryId"])):
|
|
48129
|
+
raise ValueError()
|
|
48130
|
+
except (ValueError, KeyError, TypeError, AttributeError, RecursionError):
|
|
48131
|
+
raise ValueError("artifact-manifest") from None
|
|
48132
|
+
return {"artifact": receipt, "packages": packages}
|
|
48133
|
+
|
|
48134
|
+
|
|
48135
|
+
def verify_dir(work_dir, ds_version, screens_version):
|
|
48136
|
+
"""Consumer CLI entry (#7833): verify_export over files a caller fetched into work_dir.
|
|
48137
|
+
|
|
48138
|
+
Reads provider.json, artifact.zip and expected.json; writes only the four verified package
|
|
48139
|
+
members into work_dir/verified (a new directory). The caller acts only on those bytes.
|
|
48140
|
+
"""
|
|
48141
|
+
work = pathlib.Path(work_dir)
|
|
48142
|
+
result = verify_export((work / "provider.json").read_bytes(), (work / "artifact.zip").read_bytes(),
|
|
48143
|
+
json.loads((work / "expected.json").read_text("utf-8")),
|
|
48144
|
+
{PACKAGES[0]: ds_version, PACKAGES[1]: screens_version}, datetime.now(timezone.utc))
|
|
48145
|
+
members = inspect_artifact_zip((work / "artifact.zip").read_bytes())
|
|
48146
|
+
out = work / "verified"
|
|
48147
|
+
out.mkdir(exist_ok=False)
|
|
48148
|
+
for member, payload in members.items():
|
|
48149
|
+
if member != "manifest.json":
|
|
48150
|
+
(out / member).write_bytes(payload)
|
|
48151
|
+
return result
|
|
48152
|
+
|
|
48153
|
+
|
|
48154
|
+
if __name__ == "__main__":
|
|
48155
|
+
if len(sys.argv) == 5 and sys.argv[1] == "verify":
|
|
48156
|
+
try:
|
|
48157
|
+
print(json.dumps(verify_dir(*sys.argv[2:]), sort_keys=True, indent=2))
|
|
48158
|
+
except (ValueError, OSError) as error:
|
|
48159
|
+
sys.exit(f"package-export verify refused: {error}")
|
|
48160
|
+
sys.exit(0)
|
|
48161
|
+
if len(sys.argv) != 3 or sys.argv[1] != "export":
|
|
48162
|
+
sys.exit("usage: package_export.py export <new-output-dir> | verify <work-dir> <ds-version> <screens-version>")
|
|
48163
|
+
try:
|
|
48164
|
+
result = export(os.environ, sys.argv[2])
|
|
48165
|
+
except (ValueError, OSError, urllib.error.URLError) as error:
|
|
48166
|
+
# Error text carries no request headers: HTTPError renders only the status line.
|
|
48167
|
+
sys.exit(f"package-export refused: {error}")
|
|
48168
|
+
print(json.dumps(result, sort_keys=True, indent=2))
|
|
48169
|
+
`;
|
|
48170
|
+
|
|
48171
|
+
// src/packages-adopt-export.ts
|
|
48172
|
+
var EXPORT_REPO = "mutmutco/MMI-Hub";
|
|
48173
|
+
var EXPORT_WORKFLOW = ".github/workflows/package-export.yml";
|
|
48174
|
+
var PACKAGES = [
|
|
48175
|
+
{ name: "@mutmutco/mmwork-ds", short: "mmwork-ds" },
|
|
48176
|
+
{ name: "@mutmutco/mmwork-screens", short: "mmwork-screens" }
|
|
48177
|
+
];
|
|
48178
|
+
var SPEC_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
48179
|
+
var SHAPE_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies", "peerDependenciesMeta", "bin", "os", "cpu", "hasInstallScript"];
|
|
48180
|
+
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);
|
|
48181
|
+
function lockShape(meta, field) {
|
|
48182
|
+
if (field === "hasInstallScript") {
|
|
48183
|
+
const scripts = meta.scripts ?? {};
|
|
48184
|
+
return meta.hasInstallScript === true || Boolean(scripts.preinstall || scripts.install || scripts.postinstall) || void 0;
|
|
48185
|
+
}
|
|
48186
|
+
if (field === "bin" && meta.bin && typeof meta.bin === "object") {
|
|
48187
|
+
return Object.fromEntries(Object.entries(meta.bin).map(([k, v]) => [k, String(v).replace(/^(\.\/)+/, "")]));
|
|
48188
|
+
}
|
|
48189
|
+
const value = meta[field];
|
|
48190
|
+
return value && typeof value === "object" && Object.keys(value).length === 0 ? void 0 : value;
|
|
48191
|
+
}
|
|
48192
|
+
function formatLike(original, value) {
|
|
48193
|
+
const indent = /\n([ \t]+)"/.exec(original)?.[1] ?? " ";
|
|
48194
|
+
const text = JSON.stringify(value, null, indent) + "\n";
|
|
48195
|
+
return original.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
|
|
48196
|
+
}
|
|
48197
|
+
function planAdoption(packageText, lockText, metadata) {
|
|
48198
|
+
const pkg = JSON.parse(packageText);
|
|
48199
|
+
const lock = JSON.parse(lockText);
|
|
48200
|
+
const root = lock.packages?.[""];
|
|
48201
|
+
if (lock.lockfileVersion < 2 || !root) throw new Error("package-lock.json must be lockfileVersion 2 or 3");
|
|
48202
|
+
const result = { adopted: [], skipped: [] };
|
|
48203
|
+
for (const { name } of PACKAGES) {
|
|
48204
|
+
const section = SPEC_SECTIONS.find((key) => pkg[key]?.[name] !== void 0);
|
|
48205
|
+
if (!section) {
|
|
48206
|
+
result.skipped.push(name);
|
|
48207
|
+
continue;
|
|
48208
|
+
}
|
|
48209
|
+
const meta = metadata[name];
|
|
48210
|
+
const entry = lock.packages[`node_modules/${name}`];
|
|
48211
|
+
if (!entry) throw new Error(`${name} is in package.json but not in package-lock.json; run a normal registry install first`);
|
|
48212
|
+
const nested = Object.keys(lock.packages).filter((key) => key.endsWith(`/node_modules/${name}`));
|
|
48213
|
+
if (nested.length) throw new Error(`${name} has nested lock copies (${nested.join(", ")}); adopt-export edits only the top-level entry`);
|
|
48214
|
+
const changed = SHAPE_FIELDS.filter((field) => stable(field === "hasInstallScript" ? entry[field] || void 0 : entry[field]) !== stable(lockShape(meta, field)));
|
|
48215
|
+
if (changed.length) {
|
|
48216
|
+
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`);
|
|
48217
|
+
}
|
|
48218
|
+
result.adopted.push({ name, from: entry.version, to: meta.version, resolved: meta.dist.tarball, integrity: meta.dist.integrity });
|
|
48219
|
+
entry.version = meta.version;
|
|
48220
|
+
entry.resolved = meta.dist.tarball;
|
|
48221
|
+
entry.integrity = meta.dist.integrity;
|
|
48222
|
+
for (const field of ["license", "engines"]) {
|
|
48223
|
+
if (meta[field] === void 0) delete entry[field];
|
|
48224
|
+
else entry[field] = meta[field];
|
|
48225
|
+
}
|
|
48226
|
+
pkg[section][name] = meta.version;
|
|
48227
|
+
root[section] = { ...root[section], [name]: meta.version };
|
|
48228
|
+
}
|
|
48229
|
+
return { packageText: formatLike(packageText, pkg), lockText: formatLike(lockText, lock), result };
|
|
48230
|
+
}
|
|
48231
|
+
function expectedFromRun(run, opts) {
|
|
48232
|
+
if (run.id !== opts.run || run.repository?.full_name !== EXPORT_REPO || run.path !== EXPORT_WORKFLOW) {
|
|
48233
|
+
throw new Error(`run ${opts.run} is not a ${EXPORT_REPO} ${EXPORT_WORKFLOW} run`);
|
|
48234
|
+
}
|
|
48235
|
+
return {
|
|
48236
|
+
artifactId: opts.artifact,
|
|
48237
|
+
artifactName: `mmi-package-export-${run.id}-${run.run_attempt}`,
|
|
48238
|
+
repositoryId: run.repository.id,
|
|
48239
|
+
runId: run.id,
|
|
48240
|
+
headSha: run.head_sha,
|
|
48241
|
+
headBranch: run.head_branch,
|
|
48242
|
+
digest: opts.digest
|
|
48243
|
+
};
|
|
48244
|
+
}
|
|
48245
|
+
async function adoptExport(opts, deps) {
|
|
48246
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(opts.digest)) throw new Error("--digest must be sha256:<64 hex>, from the export run summary");
|
|
48247
|
+
const packagePath = (0, import_node_path44.join)(opts.dir, "package.json");
|
|
48248
|
+
const lockPath = (0, import_node_path44.join)(opts.dir, "package-lock.json");
|
|
48249
|
+
const packageText = (0, import_node_fs45.readFileSync)(packagePath, "utf8");
|
|
48250
|
+
const lockText = (0, import_node_fs45.readFileSync)(lockPath, "utf8");
|
|
48251
|
+
const work = (0, import_node_fs45.mkdtempSync)((0, import_node_path44.join)((0, import_node_os21.tmpdir)(), "mmi-adopt-export-"));
|
|
48252
|
+
try {
|
|
48253
|
+
const run = JSON.parse((await deps.ghApi(`repos/${EXPORT_REPO}/actions/runs/${opts.run}`)).toString("utf8"));
|
|
48254
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
|
|
48255
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
|
|
48256
|
+
(0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
|
|
48257
|
+
const script = (0, import_node_path44.join)(work, "package_export.py");
|
|
48258
|
+
(0, import_node_fs45.writeFileSync)(script, package_export_default);
|
|
48259
|
+
await deps.python(script, ["verify", work, opts.ds, opts.screens]);
|
|
48260
|
+
const verified = (0, import_node_path44.join)(work, "verified");
|
|
48261
|
+
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"))]));
|
|
48262
|
+
const plan = planAdoption(packageText, lockText, metadata);
|
|
48263
|
+
for (const { name } of plan.result.adopted) {
|
|
48264
|
+
const short3 = PACKAGES.find((p) => p.name === name).short;
|
|
48265
|
+
await deps.npm(["cache", "add", (0, import_node_path44.join)(verified, `${short3}.tgz`)], opts.dir);
|
|
48266
|
+
}
|
|
48267
|
+
(0, import_node_fs45.writeFileSync)(packagePath, plan.packageText);
|
|
48268
|
+
(0, import_node_fs45.writeFileSync)(lockPath, plan.lockText);
|
|
48269
|
+
try {
|
|
48270
|
+
await deps.npm(["ci", "--offline", "--ignore-scripts", "--no-audit", "--no-fund"], opts.dir);
|
|
48271
|
+
} catch (error) {
|
|
48272
|
+
(0, import_node_fs45.writeFileSync)(packagePath, packageText);
|
|
48273
|
+
(0, import_node_fs45.writeFileSync)(lockPath, lockText);
|
|
48274
|
+
throw new Error(`npm ci --offline failed, package.json and package-lock.json restored: ${error.message}`);
|
|
48275
|
+
}
|
|
48276
|
+
return plan.result;
|
|
48277
|
+
} finally {
|
|
48278
|
+
(0, import_node_fs45.rmSync)(work, { recursive: true, force: true });
|
|
48279
|
+
}
|
|
48280
|
+
}
|
|
48281
|
+
var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process18.execFile);
|
|
48282
|
+
var MAX_OUTPUT = 200 * 1024 * 1024;
|
|
48283
|
+
async function findPython() {
|
|
48284
|
+
for (const bin of ["python3", "python"]) {
|
|
48285
|
+
try {
|
|
48286
|
+
await execFileBuffer(bin, ["-I", "-S", "-B", "-c", "import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)"], { windowsHide: true, timeout: 1e4 });
|
|
48287
|
+
return bin;
|
|
48288
|
+
} catch {
|
|
48289
|
+
}
|
|
48290
|
+
}
|
|
48291
|
+
throw new Error("Python 3.11+ is required to run the Hub package-export verifier");
|
|
48292
|
+
}
|
|
48293
|
+
function adoptExportProdDeps() {
|
|
48294
|
+
const failText = (error) => String(error?.stderr || error?.message || error).trim();
|
|
48295
|
+
return {
|
|
48296
|
+
ghApi: async (path2) => {
|
|
48297
|
+
try {
|
|
48298
|
+
return (await execFileBuffer("gh", ["api", "--method", "GET", path2], { encoding: "buffer", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 3e5 })).stdout;
|
|
48299
|
+
} catch (error) {
|
|
48300
|
+
throw new Error(`gh api ${path2} failed: ${failText(error)}`);
|
|
48301
|
+
}
|
|
48302
|
+
},
|
|
48303
|
+
python: async (script, args) => {
|
|
48304
|
+
try {
|
|
48305
|
+
return (await execFileBuffer(await findPython(), ["-I", "-S", "-B", script, ...args], { encoding: "utf8", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 3e5 })).stdout;
|
|
48306
|
+
} catch (error) {
|
|
48307
|
+
throw new Error(failText(error));
|
|
48308
|
+
}
|
|
48309
|
+
},
|
|
48310
|
+
npm: async (args, cwd) => {
|
|
48311
|
+
const [file, argv] = process.platform === "win32" ? ["cmd.exe", ["/c", "npm", ...args]] : ["npm", args];
|
|
48312
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "npm_config_allow_scripts"));
|
|
48313
|
+
try {
|
|
48314
|
+
return (await execFileBuffer(file, argv, { cwd, env, encoding: "utf8", maxBuffer: MAX_OUTPUT, windowsHide: true, timeout: 6e5 })).stdout;
|
|
48315
|
+
} catch (error) {
|
|
48316
|
+
throw new Error(`npm ${args[0]} failed: ${failText(error)}`);
|
|
48317
|
+
}
|
|
48318
|
+
}
|
|
48319
|
+
};
|
|
48320
|
+
}
|
|
48321
|
+
function registerPackagesCommands(program3) {
|
|
48322
|
+
const packages = program3.command("packages").description("consumer adoption of verified package exports (#7833)");
|
|
48323
|
+
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) => {
|
|
48324
|
+
const id = (value, flag) => {
|
|
48325
|
+
if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
|
|
48326
|
+
return Number(value);
|
|
48327
|
+
};
|
|
48328
|
+
try {
|
|
48329
|
+
const result = await adoptExport({ ...opts, run: id(opts.run, "--run"), artifact: id(opts.artifact, "--artifact") }, adoptExportProdDeps());
|
|
48330
|
+
for (const a of result.adopted) console.log(`adopted ${a.name} ${a.from} -> ${a.to} (${a.resolved}, ${a.integrity})`);
|
|
48331
|
+
for (const name of result.skipped) console.log(`skipped ${name}: not a dependency in this package.json`);
|
|
48332
|
+
console.log("npm ci --offline --ignore-scripts passed; commit package.json and package-lock.json");
|
|
48333
|
+
} catch (error) {
|
|
48334
|
+
fail(`packages adopt-export refused: ${error.message}`);
|
|
48335
|
+
}
|
|
48336
|
+
});
|
|
48337
|
+
}
|
|
48338
|
+
|
|
47594
48339
|
// src/discovery-commands.ts
|
|
47595
48340
|
init_cli_shared();
|
|
47596
48341
|
|
|
47597
48342
|
// src/wave-status.ts
|
|
47598
|
-
var
|
|
48343
|
+
var import_node_fs46 = require("node:fs");
|
|
47599
48344
|
function readStageSummary(worktreePath) {
|
|
47600
48345
|
const statePath = stageStatePath(worktreePath);
|
|
47601
|
-
if (!(0,
|
|
48346
|
+
if (!(0, import_node_fs46.existsSync)(statePath)) return void 0;
|
|
47602
48347
|
try {
|
|
47603
|
-
const state = JSON.parse((0,
|
|
48348
|
+
const state = JSON.parse((0, import_node_fs46.readFileSync)(statePath, "utf8"));
|
|
47604
48349
|
const port = typeof state.port === "number" ? state.port : void 0;
|
|
47605
48350
|
if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
|
|
47606
48351
|
return { port, url: typeof state.url === "string" ? state.url : void 0 };
|
|
@@ -47657,9 +48402,9 @@ async function collectWaveStatus(deps) {
|
|
|
47657
48402
|
// src/discovery-commands.ts
|
|
47658
48403
|
init_github_client();
|
|
47659
48404
|
init_cli_shared();
|
|
47660
|
-
var
|
|
47661
|
-
var
|
|
47662
|
-
var
|
|
48405
|
+
var import_node_fs47 = require("node:fs");
|
|
48406
|
+
var import_node_os22 = require("node:os");
|
|
48407
|
+
var import_node_path45 = require("node:path");
|
|
47663
48408
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
47664
48409
|
async function collectStatus() {
|
|
47665
48410
|
const repo = await resolveRepo();
|
|
@@ -47848,10 +48593,10 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
47848
48593
|
else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
|
|
47849
48594
|
else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
|
|
47850
48595
|
}
|
|
47851
|
-
const home = (0,
|
|
48596
|
+
const home = (0, import_node_os22.homedir)();
|
|
47852
48597
|
const plugin = onboardPluginGate({
|
|
47853
|
-
readKnown: () => readFileSyncSafe((0,
|
|
47854
|
-
readSettings: () => readFileSyncSafe((0,
|
|
48598
|
+
readKnown: () => readFileSyncSafe((0, import_node_path45.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs47.readFileSync),
|
|
48599
|
+
readSettings: () => readFileSyncSafe((0, import_node_path45.join)(home, ".claude", "settings.json"), import_node_fs47.readFileSync)
|
|
47855
48600
|
});
|
|
47856
48601
|
return {
|
|
47857
48602
|
guide: "Read the MMI org-guide skill before project work; load only the relevant topic. Use mmi-help for a short introduction.",
|
|
@@ -50955,8 +51700,8 @@ function registerSessionReport(program3) {
|
|
|
50955
51700
|
}
|
|
50956
51701
|
|
|
50957
51702
|
// src/stage-commands.ts
|
|
50958
|
-
var
|
|
50959
|
-
var
|
|
51703
|
+
var import_node_fs48 = require("node:fs");
|
|
51704
|
+
var import_node_path46 = require("node:path");
|
|
50960
51705
|
init_cli_shared();
|
|
50961
51706
|
init_clean_exit();
|
|
50962
51707
|
|
|
@@ -51202,8 +51947,8 @@ function registerStageCommands(program3) {
|
|
|
51202
51947
|
return {
|
|
51203
51948
|
resolution: decideStage({
|
|
51204
51949
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
51205
|
-
hasCompose: (0,
|
|
51206
|
-
hasEnvExample: (0,
|
|
51950
|
+
hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
|
|
51951
|
+
hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
|
|
51207
51952
|
repo
|
|
51208
51953
|
}),
|
|
51209
51954
|
project: project2,
|
|
@@ -51285,7 +52030,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
51285
52030
|
if (!res.derived) return void 0;
|
|
51286
52031
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
51287
52032
|
}
|
|
51288
|
-
program3.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (
|
|
52033
|
+
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
52034
|
const cfg = await loadConfig();
|
|
51290
52035
|
const reg = registryClientDeps(cfg);
|
|
51291
52036
|
const slug = slugOf(repo);
|
|
@@ -51488,19 +52233,19 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
51488
52233
|
|
|
51489
52234
|
// src/tenant-artifact.ts
|
|
51490
52235
|
var import_node_crypto16 = require("node:crypto");
|
|
51491
|
-
var
|
|
52236
|
+
var import_node_fs49 = require("node:fs");
|
|
51492
52237
|
var import_promises11 = require("node:fs/promises");
|
|
51493
|
-
var
|
|
52238
|
+
var import_node_path47 = require("node:path");
|
|
51494
52239
|
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
52240
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
51496
52241
|
async function sha256File(path2) {
|
|
51497
52242
|
const hash = (0, import_node_crypto16.createHash)("sha256");
|
|
51498
|
-
for await (const chunk of (0,
|
|
52243
|
+
for await (const chunk of (0, import_node_fs49.createReadStream)(path2)) hash.update(chunk);
|
|
51499
52244
|
return hash.digest("hex");
|
|
51500
52245
|
}
|
|
51501
52246
|
async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
51502
52247
|
if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
|
|
51503
|
-
const path2 = (0,
|
|
52248
|
+
const path2 = (0, import_node_path47.resolve)(inputPath);
|
|
51504
52249
|
const info = await (0, import_promises11.stat)(path2);
|
|
51505
52250
|
if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
|
|
51506
52251
|
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 +52263,7 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
|
51518
52263
|
return [key, value];
|
|
51519
52264
|
}));
|
|
51520
52265
|
headers["content-length"] = String(info.size);
|
|
51521
|
-
const stream = (0,
|
|
52266
|
+
const stream = (0, import_node_fs49.createReadStream)(path2);
|
|
51522
52267
|
let uploaded;
|
|
51523
52268
|
try {
|
|
51524
52269
|
uploaded = await fetch(body.uploadUrl, {
|
|
@@ -51657,7 +52402,7 @@ function renderVerifySecrets(body) {
|
|
|
51657
52402
|
}
|
|
51658
52403
|
|
|
51659
52404
|
// src/command-register-developer.ts
|
|
51660
|
-
var
|
|
52405
|
+
var import_node_fs53 = require("node:fs");
|
|
51661
52406
|
init_clean_exit();
|
|
51662
52407
|
init_cli_shared();
|
|
51663
52408
|
init_error_codes();
|
|
@@ -51690,7 +52435,7 @@ async function resolveWhoami(deps) {
|
|
|
51690
52435
|
}
|
|
51691
52436
|
|
|
51692
52437
|
// src/command-register-developer.ts
|
|
51693
|
-
var
|
|
52438
|
+
var import_node_path52 = require("node:path");
|
|
51694
52439
|
init_house_map();
|
|
51695
52440
|
init_hub_url();
|
|
51696
52441
|
|
|
@@ -51723,21 +52468,21 @@ async function executeWaveLand(plan, deps) {
|
|
|
51723
52468
|
}
|
|
51724
52469
|
|
|
51725
52470
|
// src/dist-drift.ts
|
|
51726
|
-
var
|
|
52471
|
+
var import_node_child_process19 = require("node:child_process");
|
|
51727
52472
|
var import_node_crypto18 = require("node:crypto");
|
|
51728
|
-
var
|
|
51729
|
-
var
|
|
51730
|
-
var
|
|
52473
|
+
var import_node_fs51 = require("node:fs");
|
|
52474
|
+
var import_node_os23 = require("node:os");
|
|
52475
|
+
var import_node_path49 = require("node:path");
|
|
51731
52476
|
|
|
51732
52477
|
// ../scripts/distribution-digest.mjs
|
|
51733
52478
|
var import_node_crypto17 = require("node:crypto");
|
|
51734
|
-
var
|
|
51735
|
-
var
|
|
52479
|
+
var import_node_fs50 = require("node:fs");
|
|
52480
|
+
var import_node_path48 = require("node:path");
|
|
51736
52481
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
51737
52482
|
function repoPath(root, declaredPath, label) {
|
|
51738
|
-
const absoluteRoot = (0,
|
|
51739
|
-
const target = (0,
|
|
51740
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
52483
|
+
const absoluteRoot = (0, import_node_path48.resolve)(root);
|
|
52484
|
+
const target = (0, import_node_path48.resolve)(root, declaredPath);
|
|
52485
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path48.sep}`)) {
|
|
51741
52486
|
throw new Error(`${label} ${declaredPath} escapes the repository root`);
|
|
51742
52487
|
}
|
|
51743
52488
|
return target;
|
|
@@ -51745,7 +52490,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
51745
52490
|
function digestFiles(files) {
|
|
51746
52491
|
const hash = (0, import_node_crypto17.createHash)("sha256");
|
|
51747
52492
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
51748
|
-
const content = file.stat.isSymbolicLink() ? Buffer.from((0,
|
|
52493
|
+
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs50.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs50.readFileSync)(file.absolute);
|
|
51749
52494
|
hash.update(file.relative, "utf8");
|
|
51750
52495
|
hash.update("\0");
|
|
51751
52496
|
hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
|
|
@@ -51760,8 +52505,8 @@ function digestFiles(files) {
|
|
|
51760
52505
|
function digestPackedFiles(packageRoot, packedFiles) {
|
|
51761
52506
|
return digestFiles(packedFiles.map((path2) => {
|
|
51762
52507
|
const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
|
|
51763
|
-
if (!(0,
|
|
51764
|
-
return { absolute, relative: slash(path2), stat: (0,
|
|
52508
|
+
if (!(0, import_node_fs50.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
|
|
52509
|
+
return { absolute, relative: slash(path2), stat: (0, import_node_fs50.lstatSync)(absolute) };
|
|
51765
52510
|
}));
|
|
51766
52511
|
}
|
|
51767
52512
|
|
|
@@ -51845,13 +52590,13 @@ function renderDistDriftReceipt(receipt) {
|
|
|
51845
52590
|
return lines2;
|
|
51846
52591
|
}
|
|
51847
52592
|
function readOrNull(path2) {
|
|
51848
|
-
return (0,
|
|
52593
|
+
return (0, import_node_fs51.existsSync)(path2) ? (0, import_node_fs51.readFileSync)(path2) : null;
|
|
51849
52594
|
}
|
|
51850
52595
|
function walkFiles(root) {
|
|
51851
52596
|
const files = [];
|
|
51852
52597
|
const walk2 = (directory) => {
|
|
51853
|
-
for (const entry of (0,
|
|
51854
|
-
const child2 = (0,
|
|
52598
|
+
for (const entry of (0, import_node_fs51.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
52599
|
+
const child2 = (0, import_node_path49.join)(directory, entry.name);
|
|
51855
52600
|
if (entry.isDirectory()) walk2(child2);
|
|
51856
52601
|
else files.push(child2);
|
|
51857
52602
|
}
|
|
@@ -51861,14 +52606,14 @@ function walkFiles(root) {
|
|
|
51861
52606
|
}
|
|
51862
52607
|
function bomPathFor(root) {
|
|
51863
52608
|
try {
|
|
51864
|
-
const registry2 = JSON.parse((0,
|
|
51865
|
-
return (0,
|
|
52609
|
+
const registry2 = JSON.parse((0, import_node_fs51.readFileSync)((0, import_node_path49.join)(root, "surfaces.json"), "utf8"));
|
|
52610
|
+
return (0, import_node_path49.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
|
|
51866
52611
|
} catch {
|
|
51867
|
-
return (0,
|
|
52612
|
+
return (0, import_node_path49.join)(root, "distribution-bom.json");
|
|
51868
52613
|
}
|
|
51869
52614
|
}
|
|
51870
52615
|
function rebuildTo(packageRoot, outDir) {
|
|
51871
|
-
(0,
|
|
52616
|
+
(0, import_node_child_process19.execFileSync)(process.execPath, ["build.mjs"], {
|
|
51872
52617
|
cwd: packageRoot,
|
|
51873
52618
|
env: { ...process.env, MMI_DIST_OUTDIR: outDir },
|
|
51874
52619
|
windowsHide: true,
|
|
@@ -51877,35 +52622,35 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
51877
52622
|
});
|
|
51878
52623
|
}
|
|
51879
52624
|
function runDistStatus(root) {
|
|
51880
|
-
const stage = (0,
|
|
52625
|
+
const stage = (0, import_node_fs51.mkdtempSync)((0, import_node_path49.join)((0, import_node_os23.tmpdir)(), "mmi-dist-drift-"));
|
|
51881
52626
|
let overlayCount = 0;
|
|
51882
52627
|
try {
|
|
51883
|
-
const cliOut = (0,
|
|
51884
|
-
const hubOut = (0,
|
|
51885
|
-
rebuildTo((0,
|
|
51886
|
-
rebuildTo((0,
|
|
52628
|
+
const cliOut = (0, import_node_path49.join)(stage, "cli-dist");
|
|
52629
|
+
const hubOut = (0, import_node_path49.join)(stage, "hub-dist");
|
|
52630
|
+
rebuildTo((0, import_node_path49.join)(root, "cli"), cliOut);
|
|
52631
|
+
rebuildTo((0, import_node_path49.join)(root, "updater"), hubOut);
|
|
51887
52632
|
const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
|
|
51888
52633
|
const rebuilt = (path2) => {
|
|
51889
52634
|
const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
|
|
51890
|
-
return spec ? readOrNull((0,
|
|
52635
|
+
return spec ? readOrNull((0, import_node_path49.join)(outDirFor(spec.packageDir), spec.output)) : null;
|
|
51891
52636
|
};
|
|
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,
|
|
52637
|
+
const committed = (path2) => readOrNull((0, import_node_path49.join)(root, path2));
|
|
52638
|
+
const tree = (path2) => readOrNull((0, import_node_path49.join)(root, path2));
|
|
52639
|
+
const distRoot = (0, import_node_path49.join)(root, "cli", "dist");
|
|
52640
|
+
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path49.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
|
|
52641
|
+
const bom = JSON.parse((0, import_node_fs51.readFileSync)(bomPathFor(root), "utf8"));
|
|
51897
52642
|
const digest = (entries) => {
|
|
51898
|
-
const overlay = (0,
|
|
52643
|
+
const overlay = (0, import_node_path49.join)(stage, `overlay-${overlayCount++}`);
|
|
51899
52644
|
for (const entry of entries) {
|
|
51900
|
-
const target = (0,
|
|
51901
|
-
(0,
|
|
51902
|
-
(0,
|
|
52645
|
+
const target = (0, import_node_path49.join)(overlay, entry.path);
|
|
52646
|
+
(0, import_node_fs51.mkdirSync)((0, import_node_path49.dirname)(target), { recursive: true });
|
|
52647
|
+
(0, import_node_fs51.writeFileSync)(target, entry.bytes);
|
|
51903
52648
|
}
|
|
51904
52649
|
return digestPackedFiles(overlay, entries.map((entry) => entry.path));
|
|
51905
52650
|
};
|
|
51906
52651
|
return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
|
|
51907
52652
|
} finally {
|
|
51908
|
-
(0,
|
|
52653
|
+
(0, import_node_fs51.rmSync)(stage, { recursive: true, force: true });
|
|
51909
52654
|
}
|
|
51910
52655
|
}
|
|
51911
52656
|
|
|
@@ -51984,7 +52729,7 @@ init_hub_auth();
|
|
|
51984
52729
|
|
|
51985
52730
|
// src/schedules-lift-command.ts
|
|
51986
52731
|
var import_promises12 = require("node:fs/promises");
|
|
51987
|
-
var
|
|
52732
|
+
var import_node_path50 = require("node:path");
|
|
51988
52733
|
init_clean_exit();
|
|
51989
52734
|
init_cli_shared();
|
|
51990
52735
|
var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
|
|
@@ -52013,7 +52758,7 @@ async function readWorkflowFiles(dir) {
|
|
|
52013
52758
|
const files = [];
|
|
52014
52759
|
for (const name of names.sort()) {
|
|
52015
52760
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
52016
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0,
|
|
52761
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path50.join)(dir, name), "utf8") });
|
|
52017
52762
|
}
|
|
52018
52763
|
return files;
|
|
52019
52764
|
}
|
|
@@ -52095,9 +52840,9 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
|
52095
52840
|
}
|
|
52096
52841
|
|
|
52097
52842
|
// src/spawn-policy-core.ts
|
|
52098
|
-
var
|
|
52099
|
-
var
|
|
52100
|
-
var
|
|
52843
|
+
var import_node_child_process20 = require("node:child_process");
|
|
52844
|
+
var import_node_fs52 = require("node:fs");
|
|
52845
|
+
var import_node_path51 = require("node:path");
|
|
52101
52846
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
52102
52847
|
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
52103
52848
|
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
@@ -52166,7 +52911,7 @@ function findViolationsInSource(raw) {
|
|
|
52166
52911
|
return found;
|
|
52167
52912
|
}
|
|
52168
52913
|
function policedFiles(root) {
|
|
52169
|
-
const r = (0,
|
|
52914
|
+
const r = (0, import_node_child_process20.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
52170
52915
|
cwd: root,
|
|
52171
52916
|
encoding: "utf8",
|
|
52172
52917
|
windowsHide: true,
|
|
@@ -52183,7 +52928,7 @@ function runSpawnPolicy(root) {
|
|
|
52183
52928
|
for (const file of files) {
|
|
52184
52929
|
let raw;
|
|
52185
52930
|
try {
|
|
52186
|
-
raw = (0,
|
|
52931
|
+
raw = (0, import_node_fs52.readFileSync)((0, import_node_path51.join)(root, file), "utf8");
|
|
52187
52932
|
} catch {
|
|
52188
52933
|
continue;
|
|
52189
52934
|
}
|
|
@@ -52203,19 +52948,19 @@ function runSpawnPolicy(root) {
|
|
|
52203
52948
|
function registerDeveloperCommands(program3) {
|
|
52204
52949
|
const rules = program3.command("rules").description("org-managed .gitignore delivery");
|
|
52205
52950
|
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,
|
|
52951
|
+
const path2 = (0, import_node_path52.join)(process.cwd(), ".gitignore");
|
|
52952
|
+
const current = (0, import_node_fs53.existsSync)(path2) ? (0, import_node_fs53.readFileSync)(path2, "utf8") : null;
|
|
52208
52953
|
const plan = planManagedGitignore(current);
|
|
52209
52954
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
52210
52955
|
if (opts.json) {
|
|
52211
|
-
if (opts.write && plan.changed) (0,
|
|
52956
|
+
if (opts.write && plan.changed) (0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
|
|
52212
52957
|
console.log(JSON.stringify(plan, null, 2));
|
|
52213
52958
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
52214
52959
|
return;
|
|
52215
52960
|
}
|
|
52216
52961
|
if (opts.write) {
|
|
52217
52962
|
if (plan.changed) {
|
|
52218
|
-
(0,
|
|
52963
|
+
(0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
|
|
52219
52964
|
console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
|
|
52220
52965
|
} else {
|
|
52221
52966
|
console.log("mmi-cli devops org rules gitignore: up to date");
|
|
@@ -52478,12 +53223,12 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
52478
53223
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
52479
53224
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
52480
53225
|
function envHealLockPath(home) {
|
|
52481
|
-
return (0,
|
|
53226
|
+
return (0, import_node_path53.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
52482
53227
|
}
|
|
52483
53228
|
async function withEnvHealLock(what, run) {
|
|
52484
53229
|
try {
|
|
52485
53230
|
return await withFileLock(
|
|
52486
|
-
envHealLockPath((0,
|
|
53231
|
+
envHealLockPath((0, import_node_os24.homedir)()),
|
|
52487
53232
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
52488
53233
|
run
|
|
52489
53234
|
);
|
|
@@ -52580,7 +53325,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52580
53325
|
const configRoot = surfaceConfigRoot(surface);
|
|
52581
53326
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
52582
53327
|
const plan = buildPluginCachePlan(
|
|
52583
|
-
(0,
|
|
53328
|
+
(0, import_node_os24.homedir)(),
|
|
52584
53329
|
running,
|
|
52585
53330
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
52586
53331
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -52608,14 +53353,14 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52608
53353
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
52609
53354
|
const installed = installedActivePluginVersion(surface);
|
|
52610
53355
|
const plan = buildPluginCachePlan(
|
|
52611
|
-
(0,
|
|
53356
|
+
(0, import_node_os24.homedir)(),
|
|
52612
53357
|
running,
|
|
52613
53358
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
52614
53359
|
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
52615
53360
|
);
|
|
52616
53361
|
const result = applyPluginCachePlan(
|
|
52617
53362
|
plan,
|
|
52618
|
-
(p) => (0,
|
|
53363
|
+
(p) => (0, import_node_fs54.rmSync)(p, { recursive: true }),
|
|
52619
53364
|
stagingApplyFsGuard(configRoot)
|
|
52620
53365
|
);
|
|
52621
53366
|
return {
|
|
@@ -52650,7 +53395,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52650
53395
|
// has no generated routing index would
|
|
52651
53396
|
// get a permanent — demanding an artifact it never asked for.
|
|
52652
53397
|
docsIndexState: (root) => {
|
|
52653
|
-
if (!(0,
|
|
53398
|
+
if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
52654
53399
|
const real = createDocsIndexDeps(root);
|
|
52655
53400
|
let docs;
|
|
52656
53401
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -52659,7 +53404,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
52659
53404
|
},
|
|
52660
53405
|
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
52661
53406
|
healDocsIndex: (root) => {
|
|
52662
|
-
if (!(0,
|
|
53407
|
+
if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
52663
53408
|
const real = createDocsIndexDeps(root);
|
|
52664
53409
|
let docs;
|
|
52665
53410
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -53367,7 +54112,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
53367
54112
|
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
54113
|
if (o.secretsFile) {
|
|
53369
54114
|
try {
|
|
53370
|
-
vars.push(`secrets=${(0,
|
|
54115
|
+
vars.push(`secrets=${(0, import_node_fs54.readFileSync)(o.secretsFile, "utf8")}`);
|
|
53371
54116
|
} catch (e) {
|
|
53372
54117
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
53373
54118
|
}
|
|
@@ -53755,16 +54500,16 @@ function ciAuditDeps2() {
|
|
|
53755
54500
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
53756
54501
|
readSeedFile: (path2) => {
|
|
53757
54502
|
if (!root) return null;
|
|
53758
|
-
const fullPath = (0,
|
|
53759
|
-
return (0,
|
|
54503
|
+
const fullPath = (0, import_node_path53.join)(root, path2);
|
|
54504
|
+
return (0, import_node_fs54.existsSync)(fullPath) ? (0, import_node_fs54.readFileSync)(fullPath, "utf8") : null;
|
|
53760
54505
|
}
|
|
53761
54506
|
};
|
|
53762
54507
|
}
|
|
53763
54508
|
function hubRoot2() {
|
|
53764
|
-
const fromPkg = (0,
|
|
54509
|
+
const fromPkg = (0, import_node_path53.join)(__dirname, "..", "..");
|
|
53765
54510
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
53766
|
-
if ((0,
|
|
53767
|
-
if ((0,
|
|
54511
|
+
if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(fromPkg, marker))) return fromPkg;
|
|
54512
|
+
if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(process.cwd(), marker))) return process.cwd();
|
|
53768
54513
|
return null;
|
|
53769
54514
|
}
|
|
53770
54515
|
registerQueryCommands(program2);
|
|
@@ -53773,6 +54518,7 @@ registerTrainCommandsAndInventory(program2);
|
|
|
53773
54518
|
registerFleetLockstepEnforce(program2);
|
|
53774
54519
|
registerDeployCommands(program2);
|
|
53775
54520
|
registerSuiteCommands(program2);
|
|
54521
|
+
registerPackagesCommands(program2);
|
|
53776
54522
|
registerDiscoveryCommands(program2);
|
|
53777
54523
|
registerExplainCommand(program2);
|
|
53778
54524
|
registerPrLifecycleCommands(program2);
|
|
@@ -53888,10 +54634,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
53888
54634
|
targets = resolution.targets;
|
|
53889
54635
|
}
|
|
53890
54636
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
53891
|
-
const fileMatrix = (0,
|
|
54637
|
+
const fileMatrix = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
53892
54638
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
53893
54639
|
const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
53894
|
-
const sanctioned = (0,
|
|
54640
|
+
const sanctioned = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
53895
54641
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
53896
54642
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
53897
54643
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -53922,16 +54668,16 @@ function directoryBytes(path2) {
|
|
|
53922
54668
|
let total = 0;
|
|
53923
54669
|
let entries;
|
|
53924
54670
|
try {
|
|
53925
|
-
entries = (0,
|
|
54671
|
+
entries = (0, import_node_fs54.readdirSync)(path2, { withFileTypes: true });
|
|
53926
54672
|
} catch {
|
|
53927
54673
|
return 0;
|
|
53928
54674
|
}
|
|
53929
54675
|
for (const entry of entries) {
|
|
53930
|
-
const child2 = (0,
|
|
54676
|
+
const child2 = (0, import_node_path53.join)(path2, entry.name);
|
|
53931
54677
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
53932
54678
|
else {
|
|
53933
54679
|
try {
|
|
53934
|
-
total += (0,
|
|
54680
|
+
total += (0, import_node_fs54.statSync)(child2).size;
|
|
53935
54681
|
} catch {
|
|
53936
54682
|
}
|
|
53937
54683
|
}
|
|
@@ -53939,25 +54685,25 @@ function directoryBytes(path2) {
|
|
|
53939
54685
|
return total;
|
|
53940
54686
|
}
|
|
53941
54687
|
function listDirEntries(dir) {
|
|
53942
|
-
return (0,
|
|
54688
|
+
return (0, import_node_fs54.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
53943
54689
|
}
|
|
53944
54690
|
function readInstalledPluginRefs(configRoot) {
|
|
53945
54691
|
const p = installedPluginsPathForConfig(configRoot);
|
|
53946
|
-
if (!(0,
|
|
54692
|
+
if (!(0, import_node_fs54.existsSync)(p)) return [];
|
|
53947
54693
|
try {
|
|
53948
|
-
return installedPluginPaths((0,
|
|
54694
|
+
return installedPluginPaths((0, import_node_fs54.readFileSync)(p, "utf8"));
|
|
53949
54695
|
} catch {
|
|
53950
54696
|
return null;
|
|
53951
54697
|
}
|
|
53952
54698
|
}
|
|
53953
54699
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
53954
54700
|
return {
|
|
53955
|
-
exists: (p) => (0,
|
|
53956
|
-
listVersionDirs: (root) => (0,
|
|
54701
|
+
exists: (p) => (0, import_node_fs54.existsSync)(p),
|
|
54702
|
+
listVersionDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
53957
54703
|
dirBytes,
|
|
53958
|
-
listStagingDirs: (root) => (0,
|
|
54704
|
+
listStagingDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
53959
54705
|
try {
|
|
53960
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
54706
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path53.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs54.statSync)(p).mtimeMs) };
|
|
53961
54707
|
} catch {
|
|
53962
54708
|
return { name: d.name, mtimeMs: Date.now() };
|
|
53963
54709
|
}
|
|
@@ -53971,10 +54717,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
53971
54717
|
return {
|
|
53972
54718
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
53973
54719
|
mtimeMs: (name) => {
|
|
53974
|
-
const p = (0,
|
|
53975
|
-
if (!(0,
|
|
54720
|
+
const p = (0, import_node_path53.join)(stagingRoot, name);
|
|
54721
|
+
if (!(0, import_node_fs54.existsSync)(p)) return null;
|
|
53976
54722
|
try {
|
|
53977
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
54723
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs54.statSync)(q).mtimeMs);
|
|
53978
54724
|
} catch {
|
|
53979
54725
|
return null;
|
|
53980
54726
|
}
|
|
@@ -53994,13 +54740,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
53994
54740
|
return;
|
|
53995
54741
|
}
|
|
53996
54742
|
const plan = buildPluginCachePlan(
|
|
53997
|
-
(0,
|
|
54743
|
+
(0, import_node_os24.homedir)(),
|
|
53998
54744
|
running,
|
|
53999
54745
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
54000
54746
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
54001
54747
|
);
|
|
54002
54748
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
54003
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
54749
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs54.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
54004
54750
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
54005
54751
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
54006
54752
|
else console.log(renderPluginCachePlan(plan, result));
|