@mutmutco/cli 3.136.0 → 3.138.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +443 -89
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5849,6 +5849,21 @@ async function sweepDeferredWorktrees(store, deps, removalContext) {
|
|
|
5849
5849
|
continue;
|
|
5850
5850
|
}
|
|
5851
5851
|
const owner = removalContext ? lookupWorktreeOwner(removalContext.primaryRoot, entry.path) : void 0;
|
|
5852
|
+
if (!current && deps.pathExists?.(entry.path) === false) {
|
|
5853
|
+
removed.push(entry.path);
|
|
5854
|
+
if (removalContext) {
|
|
5855
|
+
recordWorktreeRemoval(removalContext.primaryRoot, {
|
|
5856
|
+
action: "removed",
|
|
5857
|
+
command: removalContext.command,
|
|
5858
|
+
target: entry.path,
|
|
5859
|
+
branch: entry.branch,
|
|
5860
|
+
actor: removalContext.actor,
|
|
5861
|
+
owner,
|
|
5862
|
+
reason: "deferred worktree already absent from Git registration and filesystem"
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
continue;
|
|
5866
|
+
}
|
|
5852
5867
|
if (removalContext) {
|
|
5853
5868
|
const activeRoot = removalContext.activeWorkspaceRoot ?? resolveActiveWorkspaceRoot();
|
|
5854
5869
|
const activeGuard = decideActiveWorkspaceGuard(entry.path, activeRoot, process.platform, {
|
|
@@ -8398,6 +8413,12 @@ function loadBootstrapSeeds(manifestJson) {
|
|
|
8398
8413
|
if (s.ownership !== "org" && s.ownership !== "repo") {
|
|
8399
8414
|
throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
|
|
8400
8415
|
}
|
|
8416
|
+
if (s.managedBlock) {
|
|
8417
|
+
const block = s.managedBlock;
|
|
8418
|
+
if (!block.source || !block.begin || !block.end || block.begin === block.end || /[\r\n]/.test(block.begin + block.end)) {
|
|
8419
|
+
throw new Error(`invalid managedBlock for ${s.target} (needs source and distinct single-line begin/end markers)`);
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8401
8422
|
}
|
|
8402
8423
|
return {
|
|
8403
8424
|
seeds,
|
|
@@ -8413,6 +8434,68 @@ function missingPlaceholders(rendered) {
|
|
|
8413
8434
|
for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
8414
8435
|
return [...out];
|
|
8415
8436
|
}
|
|
8437
|
+
function isPropagatableSeed(seed) {
|
|
8438
|
+
return seed.ownership === "org" && seed.source === "self" || seed.managedBlock != null;
|
|
8439
|
+
}
|
|
8440
|
+
function sourceLines(content) {
|
|
8441
|
+
const lines = [];
|
|
8442
|
+
const newline = /\r\n|\n|\r/g;
|
|
8443
|
+
let start = 0;
|
|
8444
|
+
for (const match of content.matchAll(newline)) {
|
|
8445
|
+
const index = match.index;
|
|
8446
|
+
lines.push({ start, textEnd: index, end: index + match[0].length, text: content.slice(start, index) });
|
|
8447
|
+
start = index + match[0].length;
|
|
8448
|
+
}
|
|
8449
|
+
lines.push({ start, textEnd: content.length, end: content.length, text: content.slice(start) });
|
|
8450
|
+
return lines;
|
|
8451
|
+
}
|
|
8452
|
+
function normalizeEol(content) {
|
|
8453
|
+
return content.replace(/\r\n|\r|\n/g, "\n");
|
|
8454
|
+
}
|
|
8455
|
+
function upsertManagedSeedBlock(current, desired, begin, end) {
|
|
8456
|
+
const desiredLines = sourceLines(desired);
|
|
8457
|
+
const desiredBegins = desiredLines.filter((line) => line.text === begin);
|
|
8458
|
+
const desiredEnds = desiredLines.filter((line) => line.text === end);
|
|
8459
|
+
if (desiredBegins.length !== 1 || desiredEnds.length !== 1 || desiredEnds[0].start <= desiredBegins[0].start) {
|
|
8460
|
+
return { ok: false, reason: "managed block source must contain exactly one well-ordered marker pair" };
|
|
8461
|
+
}
|
|
8462
|
+
const desiredBegin = desiredBegins[0];
|
|
8463
|
+
const desiredEnd = desiredEnds[0];
|
|
8464
|
+
if (desired.slice(0, desiredBegin.start).trim() || desired.slice(desiredEnd.end).trim()) {
|
|
8465
|
+
return { ok: false, reason: "managed block source must contain only the bounded block" };
|
|
8466
|
+
}
|
|
8467
|
+
const canonical = desired.slice(desiredBegin.start, desiredEnd.textEnd);
|
|
8468
|
+
const currentLines = sourceLines(current);
|
|
8469
|
+
const begins = currentLines.filter((line) => line.text === begin);
|
|
8470
|
+
const ends = currentLines.filter((line) => line.text === end);
|
|
8471
|
+
if (begins.length === 0 && ends.length === 0) {
|
|
8472
|
+
const eol2 = current.match(/\r\n|\n|\r/)?.[0] ?? "\n";
|
|
8473
|
+
const block2 = normalizeEol(canonical).replace(/\n/g, eol2);
|
|
8474
|
+
if (current.length === 0) return { ok: true, content: `${block2}${eol2}`, changed: true, action: "insert" };
|
|
8475
|
+
const separator = /(?:\r\n|\n|\r)$/.test(current) ? eol2 : `${eol2}${eol2}`;
|
|
8476
|
+
return { ok: true, content: `${current}${separator}${block2}${eol2}`, changed: true, action: "insert" };
|
|
8477
|
+
}
|
|
8478
|
+
if (begins.length !== 1 || ends.length !== 1) {
|
|
8479
|
+
return { ok: false, reason: `malformed managed block markers (begin=${begins.length}, end=${ends.length})` };
|
|
8480
|
+
}
|
|
8481
|
+
const currentBegin = begins[0];
|
|
8482
|
+
const currentEnd = ends[0];
|
|
8483
|
+
if (currentEnd.start <= currentBegin.start) {
|
|
8484
|
+
return { ok: false, reason: "malformed managed block markers (end precedes begin)" };
|
|
8485
|
+
}
|
|
8486
|
+
const existing = current.slice(currentBegin.start, currentEnd.textEnd);
|
|
8487
|
+
if (normalizeEol(existing) === normalizeEol(canonical)) {
|
|
8488
|
+
return { ok: true, content: current, changed: false, action: "current" };
|
|
8489
|
+
}
|
|
8490
|
+
const eol = current.match(/\r\n|\n|\r/)?.[0] ?? "\n";
|
|
8491
|
+
const block = normalizeEol(canonical).replace(/\n/g, eol);
|
|
8492
|
+
return {
|
|
8493
|
+
ok: true,
|
|
8494
|
+
content: `${current.slice(0, currentBegin.start)}${block}${current.slice(currentEnd.textEnd)}`,
|
|
8495
|
+
changed: true,
|
|
8496
|
+
action: "replace"
|
|
8497
|
+
};
|
|
8498
|
+
}
|
|
8416
8499
|
async function resolveHubSeedSource(execGit, defaultBranch = "development") {
|
|
8417
8500
|
let status;
|
|
8418
8501
|
try {
|
|
@@ -9477,6 +9560,9 @@ function seedMatchesProjectType(seed, projectType) {
|
|
|
9477
9560
|
return projectType != null && seed.projectTypes.includes(projectType);
|
|
9478
9561
|
}
|
|
9479
9562
|
function planSeedAction(seed, exists) {
|
|
9563
|
+
if (seed.managedBlock) {
|
|
9564
|
+
return exists ? { target: seed.target, action: "update", ownership: "repo", reason: "repo-owned file; Hub-managed block reconciled in place" } : { target: seed.target, action: "create", ownership: "repo", reason: "repo-owned, missing; full template created with Hub-managed block" };
|
|
9565
|
+
}
|
|
9480
9566
|
if (seed.source === "managed-block") {
|
|
9481
9567
|
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
|
|
9482
9568
|
}
|
|
@@ -9535,6 +9621,18 @@ function resolveSeedContent(seed, vars, readFile9) {
|
|
|
9535
9621
|
}
|
|
9536
9622
|
return null;
|
|
9537
9623
|
}
|
|
9624
|
+
function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
|
|
9625
|
+
if (!seed.managedBlock) {
|
|
9626
|
+
return { ok: true, content: resolveSeedContent(seed, vars, readFile9), managed: seed.source === "managed-block" };
|
|
9627
|
+
}
|
|
9628
|
+
const base = remoteContent ?? resolveSeedContent(seed, vars, readFile9);
|
|
9629
|
+
if (base == null) return { ok: true, content: null, managed: true };
|
|
9630
|
+
const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
|
|
9631
|
+
const desired = resolveSeedContent(blockSeed, vars, readFile9);
|
|
9632
|
+
if (desired == null) return { ok: true, content: null, managed: true };
|
|
9633
|
+
const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
|
|
9634
|
+
return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
|
|
9635
|
+
}
|
|
9538
9636
|
function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
9539
9637
|
const parsedRepo = parseOwnerRepo(repo);
|
|
9540
9638
|
const slug = parsedRepo.slug;
|
|
@@ -14122,6 +14220,11 @@ function semverRelation(a, b) {
|
|
|
14122
14220
|
function classifyRow(row) {
|
|
14123
14221
|
const findings = [];
|
|
14124
14222
|
const reasons = /* @__PURE__ */ new Set();
|
|
14223
|
+
if (row.onboarding.status === "drift") {
|
|
14224
|
+
findings.push(`canonical onboarding link drift: ${row.onboarding.detail}`);
|
|
14225
|
+
} else if (row.onboarding.status === "unknown") {
|
|
14226
|
+
reasons.add(`canonical onboarding link evidence unknown: ${row.onboarding.detail}`);
|
|
14227
|
+
}
|
|
14125
14228
|
const releaseRequired = row.track !== "trunk" && !row.classifications.includes("no-deploy-surface");
|
|
14126
14229
|
const tagIsVersion = Boolean(row.release.version) && row.unknowns.some((u) => u.startsWith(NO_VERSION_LOCK_PREFIX));
|
|
14127
14230
|
for (const unknown of row.unknowns) {
|
|
@@ -14384,10 +14487,10 @@ var rollout_plan_default = {
|
|
|
14384
14487
|
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)."
|
|
14385
14488
|
},
|
|
14386
14489
|
baseline: {
|
|
14387
|
-
version: "3.
|
|
14388
|
-
tag: "v3.
|
|
14389
|
-
commit: "
|
|
14390
|
-
npm: "@mutmutco/cli@3.
|
|
14490
|
+
version: "3.138.0",
|
|
14491
|
+
tag: "v3.138.0",
|
|
14492
|
+
commit: "8735687f4693",
|
|
14493
|
+
npm: "@mutmutco/cli@3.138.0"
|
|
14391
14494
|
},
|
|
14392
14495
|
exitCriterion: "fleet-n-of-n",
|
|
14393
14496
|
hubOnlyShortcut: "forbidden",
|
|
@@ -14404,14 +14507,14 @@ var rollout_plan_default = {
|
|
|
14404
14507
|
repo: "mutmutco/mmi-hub",
|
|
14405
14508
|
role: "canary",
|
|
14406
14509
|
schedule: "train",
|
|
14407
|
-
v3Target: "v3.
|
|
14510
|
+
v3Target: "v3.138.0"
|
|
14408
14511
|
}
|
|
14409
14512
|
],
|
|
14410
14513
|
rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
|
|
14411
14514
|
rollback: {
|
|
14412
14515
|
independent: true,
|
|
14413
|
-
mechanism: "npm dist-tag latest -> 3.
|
|
14414
|
-
v3Target: "v3.
|
|
14516
|
+
mechanism: "npm dist-tag latest -> 3.138.0 and redeploy the Hub Lambda from tag v3.138.0 (8735687f4693); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
14517
|
+
v3Target: "v3.138.0 (@mutmutco/cli@3.138.0, tag commit 8735687f4693 \u2014 the preserved latest-v3 distribution, D6b)"
|
|
14415
14518
|
}
|
|
14416
14519
|
},
|
|
14417
14520
|
{
|
|
@@ -15619,6 +15722,7 @@ function registerQueryCommands(program3) {
|
|
|
15619
15722
|
|
|
15620
15723
|
// src/fleet-track-inventory.ts
|
|
15621
15724
|
var NO_DEPLOY_SURFACE_MODELS = /* @__PURE__ */ new Set(["none", "content"]);
|
|
15725
|
+
var CANONICAL_ONBOARDING_URL = "https://github.com/mutmutco/MMI-Hub/blob/main/docs/Architecture/agentic-dev-environment.md";
|
|
15622
15726
|
function ghErrorText(e) {
|
|
15623
15727
|
const err = e;
|
|
15624
15728
|
return String(err?.stderr || err?.message || e).trim().replace(/\s+/g, " ");
|
|
@@ -15649,6 +15753,23 @@ async function readFleetPluginProbe(deps, repo, branch) {
|
|
|
15649
15753
|
return isGhNotFound2(e) ? { repo, state: "absent" } : { repo, state: "unknown", unknown: `plugin surface read failed: ${ghErrorText(e)}` };
|
|
15650
15754
|
}
|
|
15651
15755
|
}
|
|
15756
|
+
async function readFleetOnboardingProbe(deps, repo, branch) {
|
|
15757
|
+
const [owner, name] = repo.split("/");
|
|
15758
|
+
const ref = branch ? `?ref=${encodeURIComponent(branch)}` : "";
|
|
15759
|
+
try {
|
|
15760
|
+
const raw = await deps.ghJson([
|
|
15761
|
+
"api",
|
|
15762
|
+
`repos/${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}/contents/README.md${ref}`
|
|
15763
|
+
]);
|
|
15764
|
+
if (raw.encoding !== "base64" || typeof raw.content !== "string") {
|
|
15765
|
+
return { repo, status: "unknown", detail: "README contents read returned no base64 content" };
|
|
15766
|
+
}
|
|
15767
|
+
const readme = Buffer.from(raw.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
15768
|
+
return readme.includes(CANONICAL_ONBOARDING_URL) ? { repo, status: "canonical", detail: CANONICAL_ONBOARDING_URL } : { repo, status: "drift", detail: `README.md does not link ${CANONICAL_ONBOARDING_URL}` };
|
|
15769
|
+
} catch (e) {
|
|
15770
|
+
return isGhNotFound2(e) ? { repo, status: "drift", detail: `README.md is absent; expected ${CANONICAL_ONBOARDING_URL}` } : { repo, status: "unknown", detail: `onboarding README read failed: ${ghErrorText(e)}` };
|
|
15771
|
+
}
|
|
15772
|
+
}
|
|
15652
15773
|
async function mapBounded2(values, limit, read) {
|
|
15653
15774
|
const results = new Array(values.length);
|
|
15654
15775
|
let next = 0;
|
|
@@ -15679,9 +15800,10 @@ function classificationsOf(surfaces) {
|
|
|
15679
15800
|
if (surfaces.deploy === "absent") classes.push("no-deploy-surface");
|
|
15680
15801
|
return classes;
|
|
15681
15802
|
}
|
|
15682
|
-
function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt) {
|
|
15803
|
+
function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generatedAt, onboardingProbes = []) {
|
|
15683
15804
|
const releaseByRepo = new Map(releaseProbes.map((p) => [p.repo.toLowerCase(), p]));
|
|
15684
15805
|
const pluginByRepo = new Map(pluginProbes.map((p) => [p.repo.toLowerCase(), p]));
|
|
15806
|
+
const onboardingByRepo = new Map(onboardingProbes.map((p) => [p.repo.toLowerCase(), p]));
|
|
15685
15807
|
const projectByRepo = /* @__PURE__ */ new Map();
|
|
15686
15808
|
const anomalies = [];
|
|
15687
15809
|
for (const project2 of projects) {
|
|
@@ -15703,11 +15825,13 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
|
|
|
15703
15825
|
const project2 = projectByRepo.get(repo.toLowerCase());
|
|
15704
15826
|
const release = releaseByRepo.get(repo.toLowerCase());
|
|
15705
15827
|
const plugin = pluginByRepo.get(repo.toLowerCase());
|
|
15828
|
+
const onboarding = onboardingByRepo.get(repo.toLowerCase()) ?? { repo, status: "unknown", detail: "onboarding README was not probed" };
|
|
15706
15829
|
const surfaces = classifySurfaces(project2, plugin);
|
|
15707
15830
|
const unknowns = [...release?.unknowns ?? []];
|
|
15708
15831
|
if (!release) unknowns.push("GitHub release evidence was not collected");
|
|
15709
15832
|
if (plugin?.unknown) unknowns.push(plugin.unknown);
|
|
15710
15833
|
if (!plugin) unknowns.push("plugin surface was not probed");
|
|
15834
|
+
if (onboarding.status === "unknown") unknowns.push(onboarding.detail);
|
|
15711
15835
|
return {
|
|
15712
15836
|
repo,
|
|
15713
15837
|
slug: String(project2?.slug ?? repo.split("/")[1] ?? repo).toLowerCase(),
|
|
@@ -15719,6 +15843,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
|
|
|
15719
15843
|
surfaces,
|
|
15720
15844
|
classifications: classificationsOf(surfaces),
|
|
15721
15845
|
pluginVersion: plugin?.version ?? null,
|
|
15846
|
+
onboarding,
|
|
15722
15847
|
release: {
|
|
15723
15848
|
tag: release?.releaseTag ?? null,
|
|
15724
15849
|
version: release?.releasedVersion ?? null,
|
|
@@ -15731,7 +15856,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
|
|
|
15731
15856
|
requiredChecks: Array.isArray(project2?.requiredChecks) ? project2.requiredChecks : null,
|
|
15732
15857
|
exemptReason: typeof project2?.ciExemptReason === "string" ? project2.ciExemptReason : null
|
|
15733
15858
|
},
|
|
15734
|
-
source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live" },
|
|
15859
|
+
source: { registry: "hub-registry-live", release: "github-releases-live", plugin: "github-contents-live", onboarding: "github-readme-live" },
|
|
15735
15860
|
freshness: { readAt: generatedAt, releasedAt: release?.releasedAt ?? null },
|
|
15736
15861
|
unknowns
|
|
15737
15862
|
};
|
|
@@ -15744,6 +15869,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
|
|
|
15744
15869
|
roster: "hub-registry-live",
|
|
15745
15870
|
releases: "github-releases-live",
|
|
15746
15871
|
plugin: "github-contents-live",
|
|
15872
|
+
onboarding: "github-readme-live",
|
|
15747
15873
|
committedInventory: "never"
|
|
15748
15874
|
},
|
|
15749
15875
|
counts: {
|
|
@@ -15754,6 +15880,7 @@ function buildFleetTrackInventory(projects, releaseProbes, pluginProbes, generat
|
|
|
15754
15880
|
noVault: repos.filter((r) => r.classifications.includes("no-vault")).length,
|
|
15755
15881
|
noPlugin: repos.filter((r) => r.classifications.includes("no-plugin")).length,
|
|
15756
15882
|
noDeploySurface: repos.filter((r) => r.classifications.includes("no-deploy-surface")).length,
|
|
15883
|
+
onboardingDrift: repos.filter((r) => r.onboarding.status === "drift").length,
|
|
15757
15884
|
unknownRows: repos.filter((r) => r.unknowns.length > 0).length,
|
|
15758
15885
|
anomalies: anomalies.length
|
|
15759
15886
|
},
|
|
@@ -15785,11 +15912,12 @@ async function readFleetTrackInventory(deps = defaultFleetTrackInventoryDeps())
|
|
|
15785
15912
|
}
|
|
15786
15913
|
}
|
|
15787
15914
|
}
|
|
15788
|
-
const [releaseProbes, pluginProbes] = await Promise.all([
|
|
15915
|
+
const [releaseProbes, pluginProbes, onboardingProbes] = await Promise.all([
|
|
15789
15916
|
mapBounded2(repos, 6, (repo) => readOrgVersionProbe(deps.query, repo)),
|
|
15790
|
-
mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
|
|
15917
|
+
mapBounded2(repos, 6, (repo) => readFleetPluginProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase()))),
|
|
15918
|
+
mapBounded2(repos, 6, (repo) => readFleetOnboardingProbe(deps.query, repo, branchByRepo.get(repo.toLowerCase())))
|
|
15791
15919
|
]);
|
|
15792
|
-
return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString());
|
|
15920
|
+
return buildFleetTrackInventory(projects, releaseProbes, pluginProbes, deps.now().toISOString(), onboardingProbes);
|
|
15793
15921
|
}
|
|
15794
15922
|
function formatFleetTrackInventory(report) {
|
|
15795
15923
|
const { counts } = report;
|
|
@@ -15797,13 +15925,13 @@ function formatFleetTrackInventory(report) {
|
|
|
15797
15925
|
`fleet version-track inventory \u2014 ${counts.registeredRepos} registered repositories (${counts.projects} registry projects)`,
|
|
15798
15926
|
`source: live Hub registry + live GitHub releases/contents \xB7 read ${report.generatedAt} \xB7 committed inventory: never`,
|
|
15799
15927
|
`tracks: ${counts.tracks.full} full \xB7 ${counts.tracks.direct} direct \xB7 ${counts.tracks.trunk} trunk`,
|
|
15800
|
-
`classified: ${counts.noBoard} no-board \xB7 ${counts.noVault} no-vault \xB7 ${counts.noPlugin} no-plugin \xB7 ${counts.noDeploySurface} no-deploy-surface \xB7 ${counts.unknownRows} rows with unknowns`,
|
|
15928
|
+
`classified: ${counts.noBoard} no-board \xB7 ${counts.noVault} no-vault \xB7 ${counts.noPlugin} no-plugin \xB7 ${counts.noDeploySurface} no-deploy-surface \xB7 ${counts.onboardingDrift} onboarding-link drift \xB7 ${counts.unknownRows} rows with unknowns`,
|
|
15801
15929
|
"",
|
|
15802
|
-
"repository | track | deploy model | released | plugin | classifications"
|
|
15930
|
+
"repository | track | deploy model | released | plugin | onboarding | classifications"
|
|
15803
15931
|
];
|
|
15804
15932
|
for (const row of report.repos) {
|
|
15805
15933
|
lines.push(
|
|
15806
|
-
`${row.repo} | ${row.track}${row.declaredTrack ? "" : " (derived)"} | ${row.deployModel ?? "UNDECLARED"} | ${row.release.version ?? "UNKNOWN"} | ${row.pluginVersion ?? (row.surfaces.plugin === "present" ? "present" : row.surfaces.plugin)} | ${row.classifications.join(", ") || "none"}`
|
|
15934
|
+
`${row.repo} | ${row.track}${row.declaredTrack ? "" : " (derived)"} | ${row.deployModel ?? "UNDECLARED"} | ${row.release.version ?? "UNKNOWN"} | ${row.pluginVersion ?? (row.surfaces.plugin === "present" ? "present" : row.surfaces.plugin)} | ${row.onboarding.status} | ${row.classifications.join(", ") || "none"}`
|
|
15807
15935
|
);
|
|
15808
15936
|
for (const unknown of row.unknowns) lines.push(` ? ${unknown}`);
|
|
15809
15937
|
}
|
|
@@ -15818,7 +15946,7 @@ function formatFleetTrackInventory(report) {
|
|
|
15818
15946
|
function registerFleetTrackInventory(program3) {
|
|
15819
15947
|
const train = program3.commands.find((c) => c.name() === "train");
|
|
15820
15948
|
if (!train) throw new Error("train inventory registration requires the train command group");
|
|
15821
|
-
train.command("inventory").description("live fleet version-track inventory (#4451) \u2014 every registered repo's track,
|
|
15949
|
+
train.command("inventory").description("live fleet version-track inventory (#4451) \u2014 every registered repo's track, release evidence, surface classification, and canonical Hub onboarding-link evidence").option("--json", "machine-readable output (the typed D2b input shape)").action(async (o) => {
|
|
15822
15950
|
try {
|
|
15823
15951
|
const report = await readFleetTrackInventory();
|
|
15824
15952
|
console.log(o.json ? JSON.stringify(report, null, 2) : formatFleetTrackInventory(report));
|
|
@@ -20069,7 +20197,7 @@ var import_node_net = require("node:net");
|
|
|
20069
20197
|
var import_node_util5 = require("node:util");
|
|
20070
20198
|
|
|
20071
20199
|
// src/rules-sync.ts
|
|
20072
|
-
function
|
|
20200
|
+
function normalizeEol2(s) {
|
|
20073
20201
|
return s.replace(/\r\n/g, "\n");
|
|
20074
20202
|
}
|
|
20075
20203
|
|
|
@@ -20122,8 +20250,8 @@ function envFileKeys(content) {
|
|
|
20122
20250
|
return keys;
|
|
20123
20251
|
}
|
|
20124
20252
|
function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
20125
|
-
const example =
|
|
20126
|
-
const target =
|
|
20253
|
+
const example = normalizeEol2(exampleContent);
|
|
20254
|
+
const target = normalizeEol2(targetContent);
|
|
20127
20255
|
const exampleKeys = envFileKeys(example);
|
|
20128
20256
|
const targetKeys = envFileKeys(target);
|
|
20129
20257
|
for (const key of exampleKeys) {
|
|
@@ -25448,7 +25576,7 @@ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync
|
|
|
25448
25576
|
return [];
|
|
25449
25577
|
}
|
|
25450
25578
|
}
|
|
25451
|
-
function rebuildRepoIndex(cwd,
|
|
25579
|
+
function rebuildRepoIndex(cwd, repoSlug3) {
|
|
25452
25580
|
const candidates = listCandidatePaths(cwd).filter(isIndexablePath);
|
|
25453
25581
|
const ignored = defaultIsIgnored(cwd, candidates);
|
|
25454
25582
|
const readmeHints = loadReadmeHints(cwd, candidates.filter((p) => !ignored.has(p)));
|
|
@@ -25480,7 +25608,7 @@ function rebuildRepoIndex(cwd, repoSlug2) {
|
|
|
25480
25608
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
25481
25609
|
const projection = {
|
|
25482
25610
|
schema: REPO_INDEX_SCHEMA,
|
|
25483
|
-
repo:
|
|
25611
|
+
repo: repoSlug3,
|
|
25484
25612
|
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25485
25613
|
entries
|
|
25486
25614
|
};
|
|
@@ -28756,6 +28884,71 @@ function renderPropagationReport(plan) {
|
|
|
28756
28884
|
return lines.join("\n");
|
|
28757
28885
|
}
|
|
28758
28886
|
|
|
28887
|
+
// src/bootstrap-propagation-identity.ts
|
|
28888
|
+
var import_node_crypto8 = require("node:crypto");
|
|
28889
|
+
var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
|
|
28890
|
+
var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
|
|
28891
|
+
function safeBranchPart(value, maxLength, fallback) {
|
|
28892
|
+
const safe = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/g, "");
|
|
28893
|
+
return safe || fallback;
|
|
28894
|
+
}
|
|
28895
|
+
function repoSlug2(repo) {
|
|
28896
|
+
return repo.trim().split("/").pop()?.toLowerCase() || "repo";
|
|
28897
|
+
}
|
|
28898
|
+
function propagationBranch(repo, target) {
|
|
28899
|
+
const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
|
|
28900
|
+
const targetPart = safeBranchPart(target, 48, "target");
|
|
28901
|
+
const hash = (0, import_node_crypto8.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
|
|
28902
|
+
return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
|
|
28903
|
+
}
|
|
28904
|
+
function legacyPropagationBranch(repo) {
|
|
28905
|
+
return `${PROPAGATION_BRANCH_PREFIX}${repoSlug2(repo)}`;
|
|
28906
|
+
}
|
|
28907
|
+
function renderPropagationTargetMarker(target) {
|
|
28908
|
+
return `<!-- ${TARGET_MARKER_NAME}: ${JSON.stringify(target)} -->`;
|
|
28909
|
+
}
|
|
28910
|
+
function targetMarkers(body) {
|
|
28911
|
+
const present = body.includes(`${TARGET_MARKER_NAME}:`);
|
|
28912
|
+
const targets = [];
|
|
28913
|
+
const marker = new RegExp(`<!--\\s*${TARGET_MARKER_NAME}:\\s*(.*?)\\s*-->`, "g");
|
|
28914
|
+
for (const match of body.matchAll(marker)) {
|
|
28915
|
+
try {
|
|
28916
|
+
const parsed = JSON.parse(match[1]);
|
|
28917
|
+
if (typeof parsed === "string") targets.push(parsed);
|
|
28918
|
+
} catch {
|
|
28919
|
+
}
|
|
28920
|
+
}
|
|
28921
|
+
return { present, targets };
|
|
28922
|
+
}
|
|
28923
|
+
function canonicalProseTargets(body) {
|
|
28924
|
+
const targets = [];
|
|
28925
|
+
const command = /bootstrap propagate --target (.+?) --execute`/g;
|
|
28926
|
+
const prose = /(?:org-owned copy of|declared marker-bounded block inside repo-owned) `([^`]+)` — the file this PR carries and nothing else/g;
|
|
28927
|
+
for (const match of body.matchAll(command)) targets.push(match[1]);
|
|
28928
|
+
for (const match of body.matchAll(prose)) targets.push(match[1]);
|
|
28929
|
+
return targets;
|
|
28930
|
+
}
|
|
28931
|
+
function propagationPrMatchesTarget(pr2, target, identity) {
|
|
28932
|
+
const body = pr2.body ?? "";
|
|
28933
|
+
const markers = targetMarkers(body);
|
|
28934
|
+
if (markers.present) {
|
|
28935
|
+
if (markers.targets.length !== 1 || markers.targets[0] !== target) return false;
|
|
28936
|
+
return !pr2.files?.length || pr2.files.length === 1 && pr2.files[0] === target;
|
|
28937
|
+
}
|
|
28938
|
+
if (identity === "target-specific") return false;
|
|
28939
|
+
const proseTargets = canonicalProseTargets(body);
|
|
28940
|
+
if (proseTargets.some((candidate) => candidate !== target)) return false;
|
|
28941
|
+
if (pr2.files?.length) {
|
|
28942
|
+
return pr2.files.length === 1 && pr2.files[0] === target;
|
|
28943
|
+
}
|
|
28944
|
+
return proseTargets.length > 0;
|
|
28945
|
+
}
|
|
28946
|
+
function propagationPrCandidatesForTarget(target, targetSpecificPrs, legacyPrs) {
|
|
28947
|
+
const targetSpecific = targetSpecificPrs.filter((pr2) => propagationPrMatchesTarget(pr2, target, "target-specific"));
|
|
28948
|
+
if (targetSpecific.length) return targetSpecific;
|
|
28949
|
+
return legacyPrs.filter((pr2) => propagationPrMatchesTarget(pr2, target, "legacy"));
|
|
28950
|
+
}
|
|
28951
|
+
|
|
28759
28952
|
// src/bootstrap-rollback.ts
|
|
28760
28953
|
function resolveRollbackRecord(records, repo, target) {
|
|
28761
28954
|
const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
|
|
@@ -28819,6 +29012,11 @@ var ENVIRONMENT_BRANCHES = {
|
|
|
28819
29012
|
rc: "rc",
|
|
28820
29013
|
main: "main"
|
|
28821
29014
|
};
|
|
29015
|
+
function classifyEnvironmentDrift(drift) {
|
|
29016
|
+
if (drift.missing) return { environment: drift.environment, action: "CREATE", reason: "environment is missing" };
|
|
29017
|
+
const why = [...drift.problems, ...drift.notes].join("; ");
|
|
29018
|
+
return why ? { environment: drift.environment, action: "UPDATE", reason: why } : { environment: drift.environment, action: "SKIP", reason: "already current" };
|
|
29019
|
+
}
|
|
28822
29020
|
var TRACK_ENVIRONMENT_EXCEPTIONS = {
|
|
28823
29021
|
direct: { rc: "direct release track has no rc stage" },
|
|
28824
29022
|
trunk: {
|
|
@@ -28857,9 +29055,9 @@ function reviewerKey(reviewer) {
|
|
|
28857
29055
|
return `${reviewer.type ?? ""}:${reviewer.id ?? ""}`;
|
|
28858
29056
|
}
|
|
28859
29057
|
function environmentDrift(live, desired) {
|
|
28860
|
-
if (!live) return ["environment is missing"];
|
|
29058
|
+
if (!live) return { environment: desired.name, missing: true, problems: ["environment is missing"], notes: [] };
|
|
28861
29059
|
const problems = [];
|
|
28862
|
-
|
|
29060
|
+
const notes = [];
|
|
28863
29061
|
const policy = live.deployment_branch_policy;
|
|
28864
29062
|
if (policy?.protected_branches !== desired.deploymentBranchPolicy.protectedBranches) {
|
|
28865
29063
|
problems.push(`protected_branches=${String(policy?.protected_branches)} (want true)`);
|
|
@@ -28867,16 +29065,67 @@ function environmentDrift(live, desired) {
|
|
|
28867
29065
|
if (policy?.custom_branch_policies !== desired.deploymentBranchPolicy.customBranchPolicies) {
|
|
28868
29066
|
problems.push(`custom_branch_policies=${String(policy?.custom_branch_policies)} (want false)`);
|
|
28869
29067
|
}
|
|
29068
|
+
if (live.wait_timer !== desired.waitTimer) notes.push(`wait_timer=${live.wait_timer ?? "unset"} (want ${desired.waitTimer})`);
|
|
28870
29069
|
const reviewers = (live.reviewers ?? []).map(reviewerKey).sort();
|
|
28871
|
-
if (reviewers.length !== 0)
|
|
28872
|
-
return problems;
|
|
29070
|
+
if (reviewers.length !== 0) notes.push(`reviewers=${reviewers.join(", ")} (want none)`);
|
|
29071
|
+
return { environment: desired.name, missing: false, problems, notes };
|
|
29072
|
+
}
|
|
29073
|
+
function environmentBody(spec) {
|
|
29074
|
+
return {
|
|
29075
|
+
wait_timer: spec.waitTimer,
|
|
29076
|
+
reviewers: spec.reviewers,
|
|
29077
|
+
deployment_branch_policy: {
|
|
29078
|
+
protected_branches: spec.deploymentBranchPolicy.protectedBranches,
|
|
29079
|
+
custom_branch_policies: spec.deploymentBranchPolicy.customBranchPolicies
|
|
29080
|
+
}
|
|
29081
|
+
};
|
|
29082
|
+
}
|
|
29083
|
+
var PLAN_GATED_PROTECTION_RE = /billing plan/i;
|
|
29084
|
+
function isPlanGatedProtectionError(error) {
|
|
29085
|
+
return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
|
|
29086
|
+
}
|
|
29087
|
+
async function reconcileRepoEnvironments(repo, releaseTrack, client) {
|
|
29088
|
+
const raw = await client.rest("GET", `repos/${repo}/environments`);
|
|
29089
|
+
const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
|
|
29090
|
+
const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
|
|
29091
|
+
const created = [];
|
|
29092
|
+
const updated = [];
|
|
29093
|
+
const skipped = [];
|
|
29094
|
+
const planGated = [];
|
|
29095
|
+
for (const spec of planned) {
|
|
29096
|
+
const current = live.get(spec.name);
|
|
29097
|
+
const drift = environmentDrift(current, spec);
|
|
29098
|
+
if (drift.problems.length === 0 && drift.notes.length === 0) {
|
|
29099
|
+
skipped.push(spec.name);
|
|
29100
|
+
continue;
|
|
29101
|
+
}
|
|
29102
|
+
const path2 = `repos/${repo}/environments/${encodeURIComponent(spec.name)}`;
|
|
29103
|
+
let gatedProtections = [];
|
|
29104
|
+
try {
|
|
29105
|
+
await client.rest("PUT", path2, { body: environmentBody(spec) });
|
|
29106
|
+
} catch (error) {
|
|
29107
|
+
if (!isPlanGatedProtectionError(error)) throw error;
|
|
29108
|
+
await client.rest("PUT", path2, {
|
|
29109
|
+
body: {
|
|
29110
|
+
deployment_branch_policy: {
|
|
29111
|
+
protected_branches: spec.deploymentBranchPolicy.protectedBranches,
|
|
29112
|
+
custom_branch_policies: spec.deploymentBranchPolicy.customBranchPolicies
|
|
29113
|
+
}
|
|
29114
|
+
}
|
|
29115
|
+
});
|
|
29116
|
+
gatedProtections = ["wait_timer", "reviewers"];
|
|
29117
|
+
}
|
|
29118
|
+
if (gatedProtections.length) planGated.push({ environment: spec.name, protections: gatedProtections });
|
|
29119
|
+
(current ? updated : created).push(spec.name);
|
|
29120
|
+
}
|
|
29121
|
+
return { repo, planned, created, updated, skipped, planGated };
|
|
28873
29122
|
}
|
|
28874
29123
|
async function verifyRepoEnvironments(repo, releaseTrack, client) {
|
|
28875
29124
|
const raw = await client.rest("GET", `repos/${repo}/environments`);
|
|
28876
29125
|
const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
|
|
28877
29126
|
return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
|
|
28878
|
-
const
|
|
28879
|
-
return { environment: spec.name, missing: !live.has(spec.name), problems };
|
|
29127
|
+
const drift = environmentDrift(live.get(spec.name), spec);
|
|
29128
|
+
return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
|
|
28880
29129
|
});
|
|
28881
29130
|
}
|
|
28882
29131
|
|
|
@@ -29127,7 +29376,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
29127
29376
|
checks.push({
|
|
29128
29377
|
ok: drift.problems.length === 0,
|
|
29129
29378
|
label: `repository environment protected: ${drift.environment}`,
|
|
29130
|
-
detail: drift.problems.length ? drift.problems.join("; ") : void 0
|
|
29379
|
+
detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
|
|
29131
29380
|
});
|
|
29132
29381
|
}
|
|
29133
29382
|
for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full")).filter((entry) => entry.exception)) {
|
|
@@ -29574,7 +29823,7 @@ function registerBootstrapCommands(program3) {
|
|
|
29574
29823
|
}
|
|
29575
29824
|
if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
|
|
29576
29825
|
});
|
|
29577
|
-
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
29826
|
+
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
29578
29827
|
const o = {
|
|
29579
29828
|
class: rawValue("--class", "deployable"),
|
|
29580
29829
|
projectType: rawValue("--project-type", ""),
|
|
@@ -29595,6 +29844,45 @@ function registerBootstrapCommands(program3) {
|
|
|
29595
29844
|
} catch (e) {
|
|
29596
29845
|
return fail(`bootstrap apply: ${e.message}`);
|
|
29597
29846
|
}
|
|
29847
|
+
let effectiveTrack = bootstrapReleaseTrack;
|
|
29848
|
+
if (!o.releaseTrack) {
|
|
29849
|
+
try {
|
|
29850
|
+
const meta = await fetchProjectBySlug(parsedRepo.slug, registryClientDeps(await loadConfig()));
|
|
29851
|
+
if (meta?.releaseTrack && isReleaseTrack(meta.releaseTrack)) effectiveTrack = meta.releaseTrack;
|
|
29852
|
+
} catch {
|
|
29853
|
+
}
|
|
29854
|
+
}
|
|
29855
|
+
if (o.only.trim() === "environments") {
|
|
29856
|
+
const client = defaultGitHubClient();
|
|
29857
|
+
const envJson = { repo, track: effectiveTrack };
|
|
29858
|
+
if (!o.execute) {
|
|
29859
|
+
const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
|
|
29860
|
+
const classified = drift.map(classifyEnvironmentDrift);
|
|
29861
|
+
envJson.plan = classified;
|
|
29862
|
+
if (o.json) console.log(JSON.stringify(envJson, null, 2));
|
|
29863
|
+
else {
|
|
29864
|
+
console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
|
|
29865
|
+
for (const c of classified) console.log(` ${c.action} ${c.environment} \u2014 ${c.reason}`);
|
|
29866
|
+
console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
|
|
29867
|
+
}
|
|
29868
|
+
} else {
|
|
29869
|
+
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
|
|
29870
|
+
const lines = [];
|
|
29871
|
+
if (envResult.created.length) lines.push(`environments created: ${envResult.created.join(", ")}`);
|
|
29872
|
+
if (envResult.updated.length) lines.push(`environments updated: ${envResult.updated.join(", ")}`);
|
|
29873
|
+
if (envResult.skipped.length) lines.push(`environments current: ${envResult.skipped.join(", ")}`);
|
|
29874
|
+
for (const gated of envResult.planGated) {
|
|
29875
|
+
lines.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
|
|
29876
|
+
}
|
|
29877
|
+
envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
|
|
29878
|
+
if (o.json) console.log(JSON.stringify(envJson, null, 2));
|
|
29879
|
+
else {
|
|
29880
|
+
console.log(`LIVE environments apply to ${repo}:`);
|
|
29881
|
+
lines.forEach((l) => console.log(` ${l}`));
|
|
29882
|
+
}
|
|
29883
|
+
}
|
|
29884
|
+
return;
|
|
29885
|
+
}
|
|
29598
29886
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
29599
29887
|
if (!(0, import_node_fs38.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
29600
29888
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
@@ -29609,6 +29897,7 @@ function registerBootstrapCommands(program3) {
|
|
|
29609
29897
|
return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
|
|
29610
29898
|
${known}`);
|
|
29611
29899
|
}
|
|
29900
|
+
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
29612
29901
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
29613
29902
|
const readFile9 = (p) => (0, import_node_fs38.existsSync)(p) ? (0, import_node_fs38.readFileSync)(p, "utf8") : null;
|
|
29614
29903
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
@@ -29716,9 +30005,22 @@ function registerBootstrapCommands(program3) {
|
|
|
29716
30005
|
exists = false;
|
|
29717
30006
|
}
|
|
29718
30007
|
const planned = planSeedAction(resolved, exists);
|
|
29719
|
-
const
|
|
29720
|
-
|
|
29721
|
-
|
|
30008
|
+
const isLegacyBlock = resolved.source === "managed-block";
|
|
30009
|
+
let content = null;
|
|
30010
|
+
let isManaged = isLegacyBlock || resolved.managedBlock != null;
|
|
30011
|
+
if (planned.action === "create" || planned.action === "update") {
|
|
30012
|
+
if (isLegacyBlock) {
|
|
30013
|
+
content = upsertManagedGitignoreBlock(remoteContent).content;
|
|
30014
|
+
} else {
|
|
30015
|
+
const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, remoteContent);
|
|
30016
|
+
if (!writeContent.ok) {
|
|
30017
|
+
return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
30018
|
+
}
|
|
30019
|
+
content = writeContent.content;
|
|
30020
|
+
isManaged = writeContent.managed;
|
|
30021
|
+
}
|
|
30022
|
+
}
|
|
30023
|
+
const action = reconcileSeedAction(planned, content, isManaged, remoteContent);
|
|
29722
30024
|
actions.push(action);
|
|
29723
30025
|
const docBody = content ?? remoteContent;
|
|
29724
30026
|
if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
|
|
@@ -29771,11 +30073,11 @@ function registerBootstrapCommands(program3) {
|
|
|
29771
30073
|
"--head",
|
|
29772
30074
|
seedPlan.branch,
|
|
29773
30075
|
"--title",
|
|
29774
|
-
onlyTarget ? `chore: propagate org-owned ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
30076
|
+
onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
29775
30077
|
"--body",
|
|
29776
30078
|
onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
29777
30079
|
|
|
29778
|
-
\`${onlyTarget}
|
|
30080
|
+
${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owned \`${onlyTarget}\`` : `The org-owned \`${onlyTarget}\` seed`} is declared by MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR reconciles that declared scope and preserves everything outside it. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
|
|
29779
30081
|
|
|
29780
30082
|
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli devops bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected"). Seeds are from MMI-Hub@${seedSource.sha}.`
|
|
29781
30083
|
]);
|
|
@@ -29830,6 +30132,19 @@ function registerBootstrapCommands(program3) {
|
|
|
29830
30132
|
}
|
|
29831
30133
|
}
|
|
29832
30134
|
}
|
|
30135
|
+
if (o.execute && !onlyTarget) {
|
|
30136
|
+
try {
|
|
30137
|
+
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
|
|
30138
|
+
if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
|
|
30139
|
+
if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
|
|
30140
|
+
if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
|
|
30141
|
+
for (const gated of envResult.planGated) {
|
|
30142
|
+
applied.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
|
|
30143
|
+
}
|
|
30144
|
+
} catch (e) {
|
|
30145
|
+
applied.push(`environments (failed: ${e.message})`);
|
|
30146
|
+
}
|
|
30147
|
+
}
|
|
29833
30148
|
if (o.execute && !onlyTarget) {
|
|
29834
30149
|
for (const l of manifest.labels) {
|
|
29835
30150
|
try {
|
|
@@ -29881,23 +30196,24 @@ LIVE apply to ${repo}:
|
|
|
29881
30196
|
${applied.join("\n ")}`);
|
|
29882
30197
|
}
|
|
29883
30198
|
});
|
|
29884
|
-
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
|
|
30199
|
+
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 () => {
|
|
29885
30200
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
29886
30201
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
29887
30202
|
if (!(0, import_node_fs38.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
|
|
29888
30203
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
29889
30204
|
if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
|
|
29890
30205
|
const manifest = loadBootstrapSeeds((0, import_node_fs38.readFileSync)(manifestPath, "utf8"));
|
|
29891
|
-
const propagatable = manifest.seeds.filter(
|
|
30206
|
+
const propagatable = manifest.seeds.filter(isPropagatableSeed);
|
|
29892
30207
|
if (!o.target) {
|
|
29893
30208
|
return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
|
|
29894
30209
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
29895
30210
|
}
|
|
29896
30211
|
const seed = propagatable.find((s) => s.target === o.target);
|
|
29897
|
-
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no
|
|
30212
|
+
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable targets:
|
|
29898
30213
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
29899
|
-
if (!(0, import_node_fs38.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
29900
|
-
const hubContent = (0, import_node_fs38.readFileSync)(seed.target, "utf8");
|
|
30214
|
+
if (!seed.managedBlock && !(0, import_node_fs38.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
30215
|
+
const hubContent = seed.managedBlock ? null : (0, import_node_fs38.readFileSync)(seed.target, "utf8");
|
|
30216
|
+
const readSeedFile = (path2) => (0, import_node_fs38.existsSync)(path2) ? (0, import_node_fs38.readFileSync)(path2, "utf8") : null;
|
|
29901
30217
|
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
29902
30218
|
const cfg = await loadConfig();
|
|
29903
30219
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -29953,11 +30269,12 @@ LIVE apply to ${repo}:
|
|
|
29953
30269
|
});
|
|
29954
30270
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
29955
30271
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
29956
|
-
const branchPrefix = "seed-propagate";
|
|
29957
30272
|
const reads = [];
|
|
30273
|
+
const desiredByRepo = /* @__PURE__ */ new Map();
|
|
29958
30274
|
for (const r of repos) {
|
|
29959
30275
|
if (r.waiver) continue;
|
|
29960
|
-
const
|
|
30276
|
+
const repoClass = classOf(r.repo);
|
|
30277
|
+
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
29961
30278
|
let content = null;
|
|
29962
30279
|
try {
|
|
29963
30280
|
const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
|
|
@@ -29966,13 +30283,35 @@ LIVE apply to ${repo}:
|
|
|
29966
30283
|
} catch {
|
|
29967
30284
|
content = null;
|
|
29968
30285
|
}
|
|
29969
|
-
|
|
30286
|
+
let desired;
|
|
30287
|
+
if (seed.managedBlock) {
|
|
30288
|
+
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass);
|
|
30289
|
+
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile, content);
|
|
30290
|
+
if (!resolved.ok) {
|
|
30291
|
+
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
30292
|
+
}
|
|
30293
|
+
if (resolved.content == null) {
|
|
30294
|
+
return fail(`bootstrap propagate: cannot resolve the full template or managed block source for '${seed.target}' \u2014 refusing a partial fleet plan`);
|
|
30295
|
+
}
|
|
30296
|
+
desired = resolved.content;
|
|
30297
|
+
} else {
|
|
30298
|
+
desired = hubContent;
|
|
30299
|
+
}
|
|
30300
|
+
desiredByRepo.set(r.repo, desired);
|
|
30301
|
+
const drift = compareSeedBytes(desired, content);
|
|
29970
30302
|
let pr2;
|
|
29971
30303
|
try {
|
|
29972
|
-
const
|
|
29973
|
-
|
|
29974
|
-
|
|
29975
|
-
|
|
30304
|
+
const listBranchPrs = async (branch) => {
|
|
30305
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body,files", "--limit", "100"]);
|
|
30306
|
+
return JSON.parse(listed.stdout || "[]");
|
|
30307
|
+
};
|
|
30308
|
+
const targetSpecific = (await listBranchPrs(propagationBranch(r.repo, seed.target))).map((p2) => ({ ...p2, files: p2.files?.map((f) => f.path) }));
|
|
30309
|
+
let identified = propagationPrCandidatesForTarget(seed.target, targetSpecific, []);
|
|
30310
|
+
if (!identified.length) {
|
|
30311
|
+
const legacy = (await listBranchPrs(legacyPropagationBranch(r.repo))).map((p2) => ({ ...p2, files: p2.files?.map((f) => f.path) }));
|
|
30312
|
+
identified = propagationPrCandidatesForTarget(seed.target, [], legacy);
|
|
30313
|
+
}
|
|
30314
|
+
const p = identified[0];
|
|
29976
30315
|
if (p) {
|
|
29977
30316
|
const rollup = p.statusCheckRollup ?? [];
|
|
29978
30317
|
const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
|
|
@@ -30005,9 +30344,8 @@ LIVE apply to ${repo}:
|
|
|
30005
30344
|
const headSha = seedSource.sha;
|
|
30006
30345
|
for (const rec of plan.records) {
|
|
30007
30346
|
if (rec.action !== "open-pr") continue;
|
|
30008
|
-
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
30009
30347
|
const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
|
|
30010
|
-
const branch =
|
|
30348
|
+
const branch = propagationBranch(rec.repo, seed.target);
|
|
30011
30349
|
const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
30012
30350
|
const baseSha = baseRef.stdout.trim();
|
|
30013
30351
|
let branchExists = true;
|
|
@@ -30025,7 +30363,9 @@ LIVE apply to ${repo}:
|
|
|
30025
30363
|
existingSha = void 0;
|
|
30026
30364
|
}
|
|
30027
30365
|
const tmp = (0, import_node_path36.join)((0, import_node_os16.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
30028
|
-
|
|
30366
|
+
const desiredContent = desiredByRepo.get(rec.repo);
|
|
30367
|
+
if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
|
|
30368
|
+
(0, import_node_fs38.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
|
|
30029
30369
|
try {
|
|
30030
30370
|
await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
30031
30371
|
} finally {
|
|
@@ -30034,8 +30374,14 @@ LIVE apply to ${repo}:
|
|
|
30034
30374
|
} catch {
|
|
30035
30375
|
}
|
|
30036
30376
|
}
|
|
30037
|
-
const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
30038
|
-
const
|
|
30377
|
+
const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url,body,files"]);
|
|
30378
|
+
const listedOpenPrs = JSON.parse(openPrs.stdout || "[]");
|
|
30379
|
+
const identifiedOpenPrs = propagationPrCandidatesForTarget(
|
|
30380
|
+
seed.target,
|
|
30381
|
+
listedOpenPrs.map((p) => ({ ...p, files: p.files?.map((f) => f.path) })),
|
|
30382
|
+
[]
|
|
30383
|
+
);
|
|
30384
|
+
const prDecision = decideSeedPrAction(identifiedOpenPrs);
|
|
30039
30385
|
let prUrl;
|
|
30040
30386
|
if (prDecision.action === "reuse") {
|
|
30041
30387
|
prUrl = prDecision.url;
|
|
@@ -30050,11 +30396,12 @@ LIVE apply to ${repo}:
|
|
|
30050
30396
|
"--head",
|
|
30051
30397
|
branch,
|
|
30052
30398
|
"--title",
|
|
30053
|
-
`chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
|
|
30399
|
+
`chore: propagate ${seed.managedBlock ? "Hub-managed block in" : "org-owned"} ${seed.target} from MMI-Hub (wave ${rec.wave})`,
|
|
30054
30400
|
"--body",
|
|
30055
30401
|
`Auto-opened by \`mmi-cli devops bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
|
|
30056
30402
|
|
|
30057
|
-
Propagates MMI-Hub@${headSha}
|
|
30403
|
+
Propagates MMI-Hub@${headSha}'s ${seed.managedBlock ? "declared marker-bounded block inside repo-owned" : "org-owned copy of"} \`${seed.target}\` \u2014 the file this PR carries and nothing else; content outside a managed block remains untouched.
|
|
30404
|
+
${renderPropagationTargetMarker(seed.target)}
|
|
30058
30405
|
|
|
30059
30406
|
Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
|
|
30060
30407
|
]);
|
|
@@ -30092,18 +30439,18 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
30092
30439
|
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
30093
30440
|
if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
|
|
30094
30441
|
const manifest = loadBootstrapSeeds((0, import_node_fs38.readFileSync)(manifestPath, "utf8"));
|
|
30095
|
-
const propagatable = manifest.seeds.filter(
|
|
30442
|
+
const propagatable = manifest.seeds.filter(isPropagatableSeed);
|
|
30096
30443
|
if (!o.target) {
|
|
30097
30444
|
return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
|
|
30098
30445
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
30099
30446
|
}
|
|
30100
30447
|
const seed = propagatable.find((s) => s.target === o.target);
|
|
30101
|
-
if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no
|
|
30448
|
+
if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
|
|
30102
30449
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
30103
30450
|
const slug = parsedRepo.slug;
|
|
30104
30451
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
30105
|
-
const
|
|
30106
|
-
const
|
|
30452
|
+
const targetPropagateBranch = propagationBranch(repo, seed.target);
|
|
30453
|
+
const legacyPropagateBranch = legacyPropagationBranch(repo);
|
|
30107
30454
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
30108
30455
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
30109
30456
|
let candidates;
|
|
@@ -30126,9 +30473,17 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
30126
30473
|
} else {
|
|
30127
30474
|
candidates = [];
|
|
30128
30475
|
try {
|
|
30129
|
-
const
|
|
30130
|
-
|
|
30131
|
-
|
|
30476
|
+
const listMergedBranchPrs = async (branch) => {
|
|
30477
|
+
const listed = await gh(["pr", "list", "--repo", repo, "--head", branch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,body,files", "--limit", "100"]);
|
|
30478
|
+
return JSON.parse(listed.stdout || "[]");
|
|
30479
|
+
};
|
|
30480
|
+
const targetSpecific = (await listMergedBranchPrs(targetPropagateBranch)).map((p) => ({ ...p, files: p.files?.map((f) => f.path) }));
|
|
30481
|
+
let identified = propagationPrCandidatesForTarget(seed.target, targetSpecific, []);
|
|
30482
|
+
if (!identified.length) {
|
|
30483
|
+
const legacy = (await listMergedBranchPrs(legacyPropagateBranch)).map((p) => ({ ...p, files: p.files?.map((f) => f.path) }));
|
|
30484
|
+
identified = propagationPrCandidatesForTarget(seed.target, [], legacy);
|
|
30485
|
+
}
|
|
30486
|
+
for (const p of identified) {
|
|
30132
30487
|
if (!p.mergeCommit?.oid) continue;
|
|
30133
30488
|
candidates.push({
|
|
30134
30489
|
repo,
|
|
@@ -30137,11 +30492,11 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
30137
30492
|
url: p.url,
|
|
30138
30493
|
mergeSha: p.mergeCommit.oid,
|
|
30139
30494
|
mergedAt: p.mergedAt ?? "",
|
|
30140
|
-
files:
|
|
30495
|
+
files: p.files ?? []
|
|
30141
30496
|
});
|
|
30142
30497
|
}
|
|
30143
30498
|
} catch (e) {
|
|
30144
|
-
return fail(`bootstrap rollback: could not read ${repo}'s merged ${
|
|
30499
|
+
return fail(`bootstrap rollback: could not read ${repo}'s merged ${targetPropagateBranch} or positively matching legacy PR history: ${e.message}`);
|
|
30145
30500
|
}
|
|
30146
30501
|
}
|
|
30147
30502
|
const plan = planRollback(repo, seed.target, slug, candidates);
|
|
@@ -32136,7 +32491,7 @@ var import_node_os18 = require("node:os");
|
|
|
32136
32491
|
var import_node_path42 = require("node:path");
|
|
32137
32492
|
|
|
32138
32493
|
// src/worktree-install-cache.ts
|
|
32139
|
-
var
|
|
32494
|
+
var import_node_crypto9 = require("node:crypto");
|
|
32140
32495
|
var import_node_fs42 = require("node:fs");
|
|
32141
32496
|
var import_node_path41 = require("node:path");
|
|
32142
32497
|
var CACHE_DIR = "worktree-install-cache";
|
|
@@ -32164,7 +32519,7 @@ var realWorktreeInstallCacheFs = {
|
|
|
32164
32519
|
}
|
|
32165
32520
|
};
|
|
32166
32521
|
function hashLockfileBytes(contents) {
|
|
32167
|
-
return (0,
|
|
32522
|
+
return (0, import_node_crypto9.createHash)("sha256").update(contents).digest("hex");
|
|
32168
32523
|
}
|
|
32169
32524
|
function resolveWorktreeInstallLockfile(packageDir, fs2 = realWorktreeInstallCacheFs) {
|
|
32170
32525
|
for (const name of LOCKFILE_NAMES) {
|
|
@@ -33190,7 +33545,7 @@ ${err.stderr ?? ""}`;
|
|
|
33190
33545
|
|
|
33191
33546
|
// src/issue-commands.ts
|
|
33192
33547
|
var import_node_fs44 = require("node:fs");
|
|
33193
|
-
var
|
|
33548
|
+
var import_node_crypto10 = require("node:crypto");
|
|
33194
33549
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
33195
33550
|
var ReparentConflictError = class extends Error {
|
|
33196
33551
|
constructor(message, payload) {
|
|
@@ -33481,7 +33836,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
33481
33836
|
const identity = `${spec.type}
|
|
33482
33837
|
${spec.title.trim()}
|
|
33483
33838
|
${spec.body ?? ""}`;
|
|
33484
|
-
const hash = (0,
|
|
33839
|
+
const hash = (0, import_node_crypto10.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
33485
33840
|
return `${batchKey}:${hash}`;
|
|
33486
33841
|
}
|
|
33487
33842
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -34160,7 +34515,7 @@ function onboardPluginGate(deps) {
|
|
|
34160
34515
|
declared,
|
|
34161
34516
|
settingsDeclared: readSettingsAutoUpdate(deps.readSettings(), MMI_MARKETPLACE_NAME)
|
|
34162
34517
|
}).effective;
|
|
34163
|
-
return autoUpdate ? { ok: false, detail: "background auto-update is ON \u2014 duplicate writer; run `mmi-
|
|
34518
|
+
return autoUpdate ? { ok: false, detail: "background auto-update is ON \u2014 duplicate writer; run `mmi-hub update` outside Claude Code to disable it" } : { ok: true, detail: "background auto-update off; mmi-hub is the single release-gated writer" };
|
|
34164
34519
|
}
|
|
34165
34520
|
async function collectOnboardStatus(opts = {}) {
|
|
34166
34521
|
const cfg = await loadConfig();
|
|
@@ -36460,13 +36815,13 @@ var surfaces_default = {
|
|
|
36460
36815
|
publishVisibility: "public"
|
|
36461
36816
|
},
|
|
36462
36817
|
{
|
|
36463
|
-
id: "mmi-
|
|
36818
|
+
id: "mmi-hub",
|
|
36464
36819
|
classification: "capability",
|
|
36465
36820
|
kind: "cli",
|
|
36466
36821
|
ownerPath: "updater/package.json",
|
|
36467
36822
|
deliveryPath: "updater/package.json",
|
|
36468
36823
|
delivery: "npm",
|
|
36469
|
-
applicability: "editor-agnostic
|
|
36824
|
+
applicability: "editor-agnostic MMI installation and maintenance; npm package @mutmutco/hub exposing mmi-hub",
|
|
36470
36825
|
versionCoordinated: true,
|
|
36471
36826
|
versionPaths: [
|
|
36472
36827
|
{
|
|
@@ -36944,7 +37299,7 @@ function checkCliVersion(input, releasedNote) {
|
|
|
36944
37299
|
warn: true,
|
|
36945
37300
|
label: "mmi-cli",
|
|
36946
37301
|
detail: `${report.currentVersion} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
36947
|
-
fix: "check
|
|
37302
|
+
fix: "check actual maintenance state with `mmi-hub status`; converge with `mmi-hub update`",
|
|
36948
37303
|
verbose: evidence
|
|
36949
37304
|
};
|
|
36950
37305
|
}
|
|
@@ -36954,7 +37309,7 @@ function checkCliVersion(input, releasedNote) {
|
|
|
36954
37309
|
ok: false,
|
|
36955
37310
|
label: "mmi-cli",
|
|
36956
37311
|
detail: `${report.currentVersion} \u2192 ${report.releasedVersion}`,
|
|
36957
|
-
fix:
|
|
37312
|
+
fix: "run `mmi-hub update` to converge the CLI and every present host surface",
|
|
36958
37313
|
verbose: evidence
|
|
36959
37314
|
};
|
|
36960
37315
|
}
|
|
@@ -36967,8 +37322,8 @@ function checkFleetDrift(probe) {
|
|
|
36967
37322
|
id,
|
|
36968
37323
|
ok: true,
|
|
36969
37324
|
label,
|
|
36970
|
-
detail: "no
|
|
36971
|
-
verbose: [`journal: ${probe.journalPath} (absent)`, "
|
|
37325
|
+
detail: "no MMI Hub maintenance journal on this machine",
|
|
37326
|
+
verbose: [`journal: ${probe.journalPath} (absent)`, "bootstrap with `npm install -g @mutmutco/hub` then `mmi-hub install`"]
|
|
36972
37327
|
};
|
|
36973
37328
|
}
|
|
36974
37329
|
if (probe.unreadable) {
|
|
@@ -36979,14 +37334,14 @@ function checkFleetDrift(probe) {
|
|
|
36979
37334
|
warn: true,
|
|
36980
37335
|
label,
|
|
36981
37336
|
detail: `journal could not be read \u2014 ${probe.unreadable}`,
|
|
36982
|
-
fix: `read ${probe.journalPath} directly, or
|
|
37337
|
+
fix: `read ${probe.journalPath} directly, or repair maintenance with \`mmi-hub install\``,
|
|
36983
37338
|
verbose: [`journal: ${probe.journalPath}`, `read failed: ${probe.unreadable}`]
|
|
36984
37339
|
};
|
|
36985
37340
|
}
|
|
36986
37341
|
const evidence = [
|
|
36987
37342
|
`journal: ${probe.journalPath}${probe.lastEventAt ? ` (last event ${probe.lastEventAt})` : ""}${probe.truncated ? ` \u2014 newest ${JOURNAL_TAIL_BYTES / 1024} KiB read` : ""}`,
|
|
36988
37343
|
`expected: ${probe.expected ?? "(no gated release recorded yet)"}`,
|
|
36989
|
-
...probe.surfaces.map((s) => `${s.surface}:
|
|
37344
|
+
...probe.surfaces.map((s) => `${s.surface}: last recorded ${s.installed ?? "unknown"} \u2014 ${s.verdict}${s.detail ? ` (${s.detail})` : ""}${s.compat ? ` [compat ${s.compat}]` : ""}`),
|
|
36990
37345
|
...probe.absent?.length ? [`absent since their last arm, skipped by the reconciler: ${probe.absent.join(", ")}`] : []
|
|
36991
37346
|
];
|
|
36992
37347
|
if (!probe.expected) {
|
|
@@ -37031,7 +37386,7 @@ function checkFleetDrift(probe) {
|
|
|
37031
37386
|
warn: true,
|
|
37032
37387
|
label,
|
|
37033
37388
|
detail: `${probe.expected} \u2014 ${probe.surfaces.length - unverified.length} converged, ${unverifiedNote}: no installed version recorded`,
|
|
37034
|
-
fix: `read why in ${probe.journalPath} (each arm records its own reason) \u2014 \`mmi-
|
|
37389
|
+
fix: `read why in ${probe.journalPath} (each arm records its own reason) \u2014 \`mmi-hub update\` retries now; doctor never installs`,
|
|
37035
37390
|
verbose: evidence
|
|
37036
37391
|
};
|
|
37037
37392
|
}
|
|
@@ -37042,7 +37397,7 @@ function checkFleetDrift(probe) {
|
|
|
37042
37397
|
label,
|
|
37043
37398
|
...unverified.length ? { verified: false } : {},
|
|
37044
37399
|
detail: `${drifted.length} of ${probe.surfaces.length} surface(s) behind ${probe.expected}: ${drifted.map((s) => `${s.surface} ${s.installed}`).join(", ")}${unverifiedNote ? `; ${unverifiedNote}` : ""}`,
|
|
37045
|
-
fix: `
|
|
37400
|
+
fix: `hourly Hub maintenance converges these on its next tick \u2014 \`mmi-hub update\` runs one now; doctor never installs${unverified.length ? `. The unverified surface(s) recorded no version \u2014 read their reason in ${probe.journalPath}` : ""}`,
|
|
37046
37401
|
verbose: evidence
|
|
37047
37402
|
};
|
|
37048
37403
|
}
|
|
@@ -37335,7 +37690,7 @@ function gcReapable(plan) {
|
|
|
37335
37690
|
async function runDoctorClean(opts, io, deps) {
|
|
37336
37691
|
const full = !opts.fast && !opts.banner && !opts.preflight;
|
|
37337
37692
|
const applyEnv = full;
|
|
37338
|
-
const applyRepo = full && opts.repoWrites
|
|
37693
|
+
const applyRepo = full && opts.repoWrites === true;
|
|
37339
37694
|
const lane = {
|
|
37340
37695
|
banner: Boolean(opts.banner),
|
|
37341
37696
|
fast: Boolean(opts.fast),
|
|
@@ -37450,7 +37805,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
37450
37805
|
...cli,
|
|
37451
37806
|
ok: false,
|
|
37452
37807
|
detail: cli.ok ? `missing commands: ${missing.join(", ")}` : cli.detail,
|
|
37453
|
-
fix: `run
|
|
37808
|
+
fix: `run \`mmi-hub update\` \u2014 the CLI lacks ${missing.join(", ")} (or wait for hourly Hub maintenance)`,
|
|
37454
37809
|
verbose: [...cli.verbose ?? [], `missing commands: ${missing.join(", ")}`]
|
|
37455
37810
|
});
|
|
37456
37811
|
}
|
|
@@ -37483,7 +37838,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
37483
37838
|
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
37484
37839
|
restartPending = true;
|
|
37485
37840
|
} else {
|
|
37486
|
-
emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor
|
|
37841
|
+
emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
37487
37842
|
}
|
|
37488
37843
|
}
|
|
37489
37844
|
async function runPluginCacheRow() {
|
|
@@ -37906,7 +38261,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
37906
38261
|
// #3574: `${n} stale` was prepended to `fix` because the ✗ branch would not render `detail` —
|
|
37907
38262
|
// the same fact this row already puts in `detail` on its ✓ path four lines up. One convention now.
|
|
37908
38263
|
detail: `${n} stale`,
|
|
37909
|
-
fix: "run `mmi-cli doctor
|
|
38264
|
+
fix: "run `mmi-cli doctor --apply` to reap merged branches, stale refs, and dead worktrees",
|
|
37910
38265
|
verbose: gcEvidence
|
|
37911
38266
|
});
|
|
37912
38267
|
}
|
|
@@ -37924,7 +38279,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
37924
38279
|
restartPending = true;
|
|
37925
38280
|
}
|
|
37926
38281
|
} else {
|
|
37927
|
-
emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor
|
|
38282
|
+
emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
|
|
37928
38283
|
}
|
|
37929
38284
|
}
|
|
37930
38285
|
const prefix = [
|
|
@@ -38386,9 +38741,8 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
38386
38741
|
pluginTrustState: () => codexHookTrustState(),
|
|
38387
38742
|
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
38388
38743
|
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
38389
|
-
// #4954: the
|
|
38390
|
-
//
|
|
38391
|
-
// journal `mmi-updater stamp` reports from, parsed here into one row.
|
|
38744
|
+
// #4954/#5023: the Hub maintenance journal is read-only last-run evidence. `@mutmutco/hub`
|
|
38745
|
+
// owns every version install; `mmi-hub status` separately probes actual installed state.
|
|
38392
38746
|
fleetDrift: () => readFleetDrift(process.env),
|
|
38393
38747
|
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
38394
38748
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
@@ -39245,7 +39599,7 @@ withExamples(mutating(
|
|
|
39245
39599
|
return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
|
|
39246
39600
|
}
|
|
39247
39601
|
const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
|
|
39248
|
-
const wtPath = o.path
|
|
39602
|
+
const wtPath = o.path ? (0, import_node_path46.resolve)(o.path) : defaultWorktreePath(repoRoot2, branch);
|
|
39249
39603
|
const { base: fallbackBase, fetchBranch, preferRemote } = resolveWorktreeBase(fromRef, o.remote);
|
|
39250
39604
|
let base = fallbackBase;
|
|
39251
39605
|
step = `fetch the base ref ${fromRef}`;
|
|
@@ -42001,14 +42355,14 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
42001
42355
|
});
|
|
42002
42356
|
access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
42003
42357
|
var isWin2 = process.platform === "win32";
|
|
42004
|
-
program2.command("doctor").description("heal plugin wiring and
|
|
42358
|
+
program2.command("doctor").description("safely heal active plugin wiring and report hygiene; repository cleanup requires explicit --apply (#5023), while version convergence belongs to `mmi-hub update`").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the canonical MMI Agentic Onboarding URL").option("--json", "machine-readable output (an output format \u2014 repairs still run by lane)").option("--apply", "advanced explicit lane: also apply guarded repository cleanup (gitignore, docs index, board mechanics, merged branches, dead worktrees, aged scratch)").option("--no-repo-writes", "compatibility spelling for the safe default: env/plugin repairs only, never mutate the repository").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run may heal safe machine-global plugin wiring but never mutates the repository. Run\n`mmi-cli doctor --apply` only when you explicitly want the guarded full repository cleanup lane.\nCLI and host version convergence belongs to `mmi-hub update`; doctor reports lag and last-run evidence.\n--banner/--fast/--self/--preflight are read-only lanes.\n").action(async (opts) => {
|
|
42005
42359
|
if (opts.guide) {
|
|
42006
|
-
consoleIo.log(
|
|
42360
|
+
consoleIo.log(`MMI Agentic Onboarding: ${CANONICAL_ONBOARDING_URL}`);
|
|
42007
42361
|
return;
|
|
42008
42362
|
}
|
|
42009
42363
|
process.exitCode = await runDoctorClean(
|
|
42010
42364
|
{
|
|
42011
|
-
repoWrites: opts.repoWrites,
|
|
42365
|
+
repoWrites: opts.apply === true && opts.repoWrites !== false,
|
|
42012
42366
|
banner: opts.banner,
|
|
42013
42367
|
preflight: opts.preflight,
|
|
42014
42368
|
fast: opts.fast || opts.self,
|
package/package.json
CHANGED