@mutmutco/cli 3.25.0 → 3.27.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 +221 -68
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3616,13 +3616,29 @@ var CLI_EXIT_WATCHDOG_MS = 1e4;
|
|
|
3616
3616
|
async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
|
|
3617
3617
|
const timer = setTimeout(() => {
|
|
3618
3618
|
console.error(
|
|
3619
|
-
`mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked; forcing exit (#2904). Please report this.`
|
|
3619
|
+
`mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked (${describeActiveHandles()}); forcing exit (#2904). Please report this.`
|
|
3620
3620
|
);
|
|
3621
3621
|
void flushStdio().finally(() => process.exit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
3622
3622
|
}, watchdogMs);
|
|
3623
3623
|
timer.unref?.();
|
|
3624
3624
|
await closeHttpPool();
|
|
3625
3625
|
}
|
|
3626
|
+
function describeActiveHandles() {
|
|
3627
|
+
let summary;
|
|
3628
|
+
try {
|
|
3629
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3630
|
+
for (const type of process.getActiveResourcesInfo()) counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
3631
|
+
summary = [...counts.entries()].sort().map(([type, n]) => n > 1 ? `${type}\xD7${n}` : type).join(", ") || "none reported";
|
|
3632
|
+
} catch {
|
|
3633
|
+
summary = "unavailable";
|
|
3634
|
+
}
|
|
3635
|
+
try {
|
|
3636
|
+
const children = (process._getActiveHandles?.() ?? []).filter((h) => h?.constructor?.name === "ChildProcess").map((h) => `${h.spawnfile ?? "?"}(pid=${h.pid ?? "?"})`);
|
|
3637
|
+
if (children.length) summary += `; live children: ${children.join(", ")}`;
|
|
3638
|
+
} catch {
|
|
3639
|
+
}
|
|
3640
|
+
return summary;
|
|
3641
|
+
}
|
|
3626
3642
|
async function failGraceful(msg) {
|
|
3627
3643
|
console.error(`mmi-cli ${msg}`);
|
|
3628
3644
|
return cleanExit(1);
|
|
@@ -5453,6 +5469,7 @@ function buildPrMergeArgs(input) {
|
|
|
5453
5469
|
function basePolicyBlocksImmediateMerge(message) {
|
|
5454
5470
|
return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message);
|
|
5455
5471
|
}
|
|
5472
|
+
var PR_MERGE_ENQUEUED_EXIT_CODE = 2;
|
|
5456
5473
|
function ghPrMergeLocalBranchDeleteWarning(message) {
|
|
5457
5474
|
return /used by worktree|cannot delete branch/i.test(message);
|
|
5458
5475
|
}
|
|
@@ -6196,7 +6213,7 @@ function formatGcPlan(plan, apply) {
|
|
|
6196
6213
|
}
|
|
6197
6214
|
|
|
6198
6215
|
// src/project-model.ts
|
|
6199
|
-
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
|
|
6216
|
+
var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-app", "desktop-game", "non-deployable", "cli-tool", "worker"];
|
|
6200
6217
|
var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
|
|
6201
6218
|
var RELEASE_TRACKS = ["full", "direct", "trunk"];
|
|
6202
6219
|
var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
|
|
@@ -6233,12 +6250,12 @@ function resolveDeployModel(meta, repo) {
|
|
|
6233
6250
|
const projectType = resolveProjectType(meta, repo);
|
|
6234
6251
|
if (projectType === "content" || meta?.class === "content") return "content";
|
|
6235
6252
|
if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
|
|
6236
|
-
if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
|
|
6253
|
+
if (projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable") return "none";
|
|
6237
6254
|
if (projectType === "cli-tool") return "registry-publish";
|
|
6238
6255
|
return "tenant-container";
|
|
6239
6256
|
}
|
|
6240
6257
|
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
6241
|
-
return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
6258
|
+
return projectType === "content" || projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
6242
6259
|
}
|
|
6243
6260
|
function inferReleaseTrackFromBranches(hints) {
|
|
6244
6261
|
if (hints.hasRcBranch) return void 0;
|
|
@@ -9200,7 +9217,7 @@ function resolveMergeCiPolicy(input) {
|
|
|
9200
9217
|
(p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
|
|
9201
9218
|
);
|
|
9202
9219
|
if (!ciWorkflows.length) {
|
|
9203
|
-
return { policy: "no-ci", reason: "no .github/workflows CI" };
|
|
9220
|
+
return { policy: "no-ci", reason: "no pull_request-triggered .github/workflows CI" };
|
|
9204
9221
|
}
|
|
9205
9222
|
return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
|
|
9206
9223
|
}
|
|
@@ -9211,13 +9228,18 @@ function parseGhPrChecksOutput(stdout) {
|
|
|
9211
9228
|
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
9212
9229
|
if (!lines.length) return "no-checks-reported";
|
|
9213
9230
|
let anyPending = false;
|
|
9231
|
+
let anyFailure = false;
|
|
9214
9232
|
for (const line of lines) {
|
|
9215
9233
|
const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
|
|
9216
9234
|
const state = (parts[1] ?? "").toLowerCase();
|
|
9217
|
-
if (state === "fail" || state === "failure" || state === "error")
|
|
9235
|
+
if (state === "fail" || state === "failure" || state === "error") {
|
|
9236
|
+
anyFailure = true;
|
|
9237
|
+
continue;
|
|
9238
|
+
}
|
|
9218
9239
|
if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
|
|
9219
9240
|
anyPending = true;
|
|
9220
9241
|
}
|
|
9242
|
+
if (anyFailure) return anyPending ? "failing" : "failure";
|
|
9221
9243
|
return anyPending ? "pending" : "success";
|
|
9222
9244
|
}
|
|
9223
9245
|
function parseGhPrChecksResult(stdout, stderr) {
|
|
@@ -9230,14 +9252,16 @@ function parseGhPrChecksResult(stdout, stderr) {
|
|
|
9230
9252
|
function pollStateFromParsed(parsed) {
|
|
9231
9253
|
return parsed === "error" ? "failure" : parsed;
|
|
9232
9254
|
}
|
|
9255
|
+
var PR_CHECKS_TIMEOUT_EXIT_CODE = 2;
|
|
9233
9256
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
9234
|
-
var PR_CHECKS_TIMEOUT_MS =
|
|
9257
|
+
var PR_CHECKS_TIMEOUT_MS = 30 * 6e4;
|
|
9235
9258
|
var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
|
|
9236
9259
|
var PR_CHECKS_CONFIRM_MS = 5e3;
|
|
9260
|
+
var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
|
|
9237
9261
|
var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
|
|
9238
9262
|
function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
|
|
9239
9263
|
if (checks === "no-checks-reported") return { action: "proceed" };
|
|
9240
|
-
const staleNote = `
|
|
9264
|
+
const staleNote = `merge CI policy says no-ci (${policyReason}), but live PR checks exist`;
|
|
9241
9265
|
if (checks === "success") {
|
|
9242
9266
|
return { action: "proceed", note: `${staleNote}; proceeding because they are green` };
|
|
9243
9267
|
}
|
|
@@ -9264,12 +9288,39 @@ async function waitForPrChecks(deps) {
|
|
|
9264
9288
|
queuedStates.push(firstState);
|
|
9265
9289
|
}
|
|
9266
9290
|
const now = deps.now ?? (() => Date.now());
|
|
9267
|
-
const
|
|
9291
|
+
const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
|
|
9292
|
+
const started = now();
|
|
9293
|
+
const deadline = started + timeoutMs;
|
|
9294
|
+
const report = (state) => {
|
|
9295
|
+
try {
|
|
9296
|
+
const elapsedMs = now() - started;
|
|
9297
|
+
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
|
|
9298
|
+
} catch {
|
|
9299
|
+
}
|
|
9300
|
+
};
|
|
9268
9301
|
let lastDetail = "pending";
|
|
9269
9302
|
let successStreak = 0;
|
|
9303
|
+
let failureStreak = 0;
|
|
9304
|
+
report("starting");
|
|
9270
9305
|
while (now() < deadline) {
|
|
9271
9306
|
const state = queuedStates.shift() ?? await deps.pollChecks();
|
|
9272
|
-
|
|
9307
|
+
report(state);
|
|
9308
|
+
if (state !== "success") successStreak = 0;
|
|
9309
|
+
if (state !== "failure") failureStreak = 0;
|
|
9310
|
+
if (state === "failure") {
|
|
9311
|
+
failureStreak += 1;
|
|
9312
|
+
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
9313
|
+
return { policy, status: "failure", reason, detail: "checks-failure" };
|
|
9314
|
+
}
|
|
9315
|
+
lastDetail = "confirming-failure";
|
|
9316
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9317
|
+
continue;
|
|
9318
|
+
}
|
|
9319
|
+
if (state === "failing") {
|
|
9320
|
+
lastDetail = "failing (a check failed; others still running)";
|
|
9321
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9322
|
+
continue;
|
|
9323
|
+
}
|
|
9273
9324
|
if (state === "success") {
|
|
9274
9325
|
successStreak += 1;
|
|
9275
9326
|
if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
|
|
@@ -9279,7 +9330,6 @@ async function waitForPrChecks(deps) {
|
|
|
9279
9330
|
await deps.sleep(PR_CHECKS_CONFIRM_MS);
|
|
9280
9331
|
continue;
|
|
9281
9332
|
}
|
|
9282
|
-
successStreak = 0;
|
|
9283
9333
|
if (state === "no-checks-reported") {
|
|
9284
9334
|
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
9285
9335
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
@@ -9288,7 +9338,7 @@ async function waitForPrChecks(deps) {
|
|
|
9288
9338
|
lastDetail = state;
|
|
9289
9339
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9290
9340
|
}
|
|
9291
|
-
return { policy, status: "timeout", reason, detail: lastDetail };
|
|
9341
|
+
return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
|
|
9292
9342
|
}
|
|
9293
9343
|
|
|
9294
9344
|
// src/bootstrap-ruleset.ts
|
|
@@ -9376,6 +9426,11 @@ function workflowTriggersPullRequest(yaml) {
|
|
|
9376
9426
|
if (!onBlock) return false;
|
|
9377
9427
|
return /\bpull_request\b/.test(onBlock);
|
|
9378
9428
|
}
|
|
9429
|
+
function workflowReportsPrChecks(yaml) {
|
|
9430
|
+
const onBlock = extractOnBlock(yaml);
|
|
9431
|
+
if (!onBlock) return false;
|
|
9432
|
+
return /\bpull_request(_target)?\b/.test(onBlock);
|
|
9433
|
+
}
|
|
9379
9434
|
function extractOnBlock(yaml) {
|
|
9380
9435
|
const lines = yaml.split(/\r?\n/);
|
|
9381
9436
|
let inOn = false;
|
|
@@ -9608,6 +9663,10 @@ function seedMatchesDeployModel(seed, deployModel) {
|
|
|
9608
9663
|
if (!seed.deployModels?.length) return true;
|
|
9609
9664
|
return deployModel != null && seed.deployModels.includes(deployModel);
|
|
9610
9665
|
}
|
|
9666
|
+
function seedMatchesProjectType(seed, projectType) {
|
|
9667
|
+
if (!seed.projectTypes?.length) return true;
|
|
9668
|
+
return projectType != null && seed.projectTypes.includes(projectType);
|
|
9669
|
+
}
|
|
9611
9670
|
function planSeedAction(seed, exists) {
|
|
9612
9671
|
if (seed.source === "fanout") {
|
|
9613
9672
|
return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
|
|
@@ -9620,18 +9679,25 @@ function planSeedAction(seed, exists) {
|
|
|
9620
9679
|
}
|
|
9621
9680
|
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
|
|
9622
9681
|
}
|
|
9623
|
-
function reconcileSeedAction(action, content, isManagedBlock) {
|
|
9682
|
+
function reconcileSeedAction(action, content, isManagedBlock, remoteContent = null) {
|
|
9624
9683
|
if (action.action === "skip") return action;
|
|
9625
9684
|
if (content == null) {
|
|
9626
9685
|
return { ...action, action: "skip", reason: "no resolvable content" };
|
|
9627
9686
|
}
|
|
9628
|
-
if (isManagedBlock)
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9687
|
+
if (!isManagedBlock) {
|
|
9688
|
+
const unfilled = missingPlaceholders(content);
|
|
9689
|
+
if (unfilled.length) {
|
|
9690
|
+
return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
|
|
9691
|
+
}
|
|
9692
|
+
}
|
|
9693
|
+
if (action.action === "update" && remoteContent != null && sameIgnoringEol(content, remoteContent)) {
|
|
9694
|
+
return { ...action, action: "skip", reason: "already current" };
|
|
9632
9695
|
}
|
|
9633
9696
|
return action;
|
|
9634
9697
|
}
|
|
9698
|
+
function sameIgnoringEol(a, b) {
|
|
9699
|
+
return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
|
|
9700
|
+
}
|
|
9635
9701
|
function renderSeedPlan(actions) {
|
|
9636
9702
|
const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
|
|
9637
9703
|
for (const a of actions) {
|
|
@@ -9915,12 +9981,12 @@ async function listWorkflowPaths(deps, repo, branch) {
|
|
|
9915
9981
|
`repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
|
|
9916
9982
|
);
|
|
9917
9983
|
return (entries ?? []).filter((e) => e.type === "file" && /\.ya?ml$/i.test(e.name ?? "")).map((e) => `.github/workflows/${e.name}`);
|
|
9918
|
-
} catch {
|
|
9919
|
-
return [];
|
|
9984
|
+
} catch (e) {
|
|
9985
|
+
return e?.status === 404 ? [] : void 0;
|
|
9920
9986
|
}
|
|
9921
9987
|
}
|
|
9922
9988
|
async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
9923
|
-
const paths = await listWorkflowPaths(deps, repo, branch);
|
|
9989
|
+
const paths = await listWorkflowPaths(deps, repo, branch) ?? [];
|
|
9924
9990
|
const workflows = [];
|
|
9925
9991
|
for (const path2 of paths) {
|
|
9926
9992
|
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
@@ -9973,12 +10039,25 @@ async function resolveRepoMergeCiPolicy(repo, deps) {
|
|
|
9973
10039
|
}
|
|
9974
10040
|
const baseBranch = "development";
|
|
9975
10041
|
const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
10042
|
+
const workflowPaths = repoClass === "hub" ? [".github/workflows/gate.yml"] : hasGate ? [PRODUCT_GATE_PATH] : await listWorkflowPaths(deps, repo, baseBranch);
|
|
10043
|
+
if (workflowPaths === void 0) {
|
|
10044
|
+
return { policy: "wait-for-checks", reason: "workflows listing unreadable (API error) \u2014 failing safe to wait-for-checks" };
|
|
10045
|
+
}
|
|
10046
|
+
const prTriggered = await filterPullRequestTriggered(deps, repo, baseBranch, workflowPaths);
|
|
9976
10047
|
return resolveMergeCiPolicy({
|
|
9977
|
-
workflowPaths:
|
|
10048
|
+
workflowPaths: prTriggered,
|
|
9978
10049
|
registryCi: meta?.ci,
|
|
9979
10050
|
registryRequiredChecks: meta?.requiredChecks
|
|
9980
10051
|
});
|
|
9981
10052
|
}
|
|
10053
|
+
async function filterPullRequestTriggered(deps, repo, branch, paths) {
|
|
10054
|
+
const kept = [];
|
|
10055
|
+
for (const path2 of paths) {
|
|
10056
|
+
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
10057
|
+
if (body == null || workflowReportsPrChecks(body)) kept.push(path2);
|
|
10058
|
+
}
|
|
10059
|
+
return kept;
|
|
10060
|
+
}
|
|
9982
10061
|
async function auditRepoCi(repo, deps) {
|
|
9983
10062
|
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
9984
10063
|
const repoClass = classifyRepo(repo, meta);
|
|
@@ -11927,7 +12006,6 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
11927
12006
|
{ label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
|
|
11928
12007
|
{ label: "attach GitHub Project v2 and register Hub registry META", gated: true },
|
|
11929
12008
|
{ label: "seed README.md and architecture.md", gated: true },
|
|
11930
|
-
{ label: "commit .cursor/rules/<repo>.mdc", gated: true },
|
|
11931
12009
|
{ label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
|
|
11932
12010
|
];
|
|
11933
12011
|
}
|
|
@@ -14957,9 +15035,6 @@ async function contentText(deps, repo, branch, path2) {
|
|
|
14957
15035
|
return null;
|
|
14958
15036
|
}
|
|
14959
15037
|
}
|
|
14960
|
-
function repoSlugFromFullName(repo) {
|
|
14961
|
-
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
14962
|
-
}
|
|
14963
15038
|
async function getProtection(deps, repo, branch) {
|
|
14964
15039
|
try {
|
|
14965
15040
|
return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
|
|
@@ -15109,13 +15184,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
15109
15184
|
label: "architecture.md is filled (no template placeholders)",
|
|
15110
15185
|
detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
|
|
15111
15186
|
});
|
|
15112
|
-
const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
|
|
15113
|
-
const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
|
|
15114
|
-
checks.push({
|
|
15115
|
-
ok: agentRulesOk,
|
|
15116
|
-
label: "Cursor agent rules file exists",
|
|
15117
|
-
detail: agentRulesOk ? void 0 : `expected: ${agentRulesPath}`
|
|
15118
|
-
});
|
|
15119
15187
|
const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
|
|
15120
15188
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
15121
15189
|
for (const label of requiredLabels) {
|
|
@@ -15699,10 +15767,16 @@ function attestedLine(att) {
|
|
|
15699
15767
|
var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
|
|
15700
15768
|
function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
|
|
15701
15769
|
const attested = appAttestationOf(meta);
|
|
15702
|
-
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
|
|
15770
|
+
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-app" && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
|
|
15703
15771
|
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
15704
15772
|
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
15705
15773
|
if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
15774
|
+
if (projectType === "desktop-app") {
|
|
15775
|
+
return [
|
|
15776
|
+
"Desktop app: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned installer/packaging outside the tenant train.",
|
|
15777
|
+
"Keep README.md and architecture.md explicit about the per-OS build and distribution path (installer, and any registry publish alongside it) so readiness does not guess web infra."
|
|
15778
|
+
];
|
|
15779
|
+
}
|
|
15706
15780
|
if (projectType === "desktop-game") {
|
|
15707
15781
|
return [
|
|
15708
15782
|
"Desktop game: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned release packaging outside the tenant train.",
|
|
@@ -15836,7 +15910,7 @@ function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
|
|
|
15836
15910
|
patch.requiredRuntimeSecrets = next;
|
|
15837
15911
|
}
|
|
15838
15912
|
}
|
|
15839
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
15913
|
+
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-app|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
15840
15914
|
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
15841
15915
|
return { slug, patch, appOwnedGaps };
|
|
15842
15916
|
}
|
|
@@ -16447,6 +16521,15 @@ function settableVarHelp() {
|
|
|
16447
16521
|
});
|
|
16448
16522
|
return `META field to set (repeatable): ${keys.join(", ")}`;
|
|
16449
16523
|
}
|
|
16524
|
+
function duplicateVarKeyAcrossFlags(vars, sets) {
|
|
16525
|
+
const keyOf = (v) => v.includes("=") ? v.slice(0, v.indexOf("=")) : v;
|
|
16526
|
+
const varKeys = new Set(vars.map(keyOf));
|
|
16527
|
+
for (const s of sets) {
|
|
16528
|
+
const key = keyOf(s);
|
|
16529
|
+
if (varKeys.has(key)) return key;
|
|
16530
|
+
}
|
|
16531
|
+
return null;
|
|
16532
|
+
}
|
|
16450
16533
|
function buildProjectSetPatch(input) {
|
|
16451
16534
|
const patch = {};
|
|
16452
16535
|
if (input.class) {
|
|
@@ -16475,7 +16558,11 @@ function buildProjectSetPatch(input) {
|
|
|
16475
16558
|
}
|
|
16476
16559
|
for (const value of input.vars) {
|
|
16477
16560
|
const eq = value.indexOf("=");
|
|
16478
|
-
if (eq <= 0)
|
|
16561
|
+
if (eq <= 0) {
|
|
16562
|
+
const looksPositional = value.includes("/") || !value.includes("=");
|
|
16563
|
+
const hint = looksPositional ? " \u2014 if that is the [owner/repo] argument, put it BEFORE --var; a variadic flag swallows the tokens after it" : "";
|
|
16564
|
+
throw new Error(`project set: --var must be KEY=VALUE (got "${value}")${hint}`);
|
|
16565
|
+
}
|
|
16479
16566
|
const key = value.slice(0, eq);
|
|
16480
16567
|
const raw = value.slice(eq + 1);
|
|
16481
16568
|
if (!SETTABLE_VAR_KEY_SET.has(key)) {
|
|
@@ -18200,6 +18287,28 @@ function validateBatchSpecs(specs) {
|
|
|
18200
18287
|
}
|
|
18201
18288
|
return { ok: errors.length === 0, errors, validated };
|
|
18202
18289
|
}
|
|
18290
|
+
var NORTH_STAR_LABEL_COLOR = "5319e7";
|
|
18291
|
+
function northStarLabelsForBatch(specs) {
|
|
18292
|
+
const labels = /* @__PURE__ */ new Set();
|
|
18293
|
+
for (const spec of specs) {
|
|
18294
|
+
if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
|
|
18295
|
+
for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
|
|
18296
|
+
}
|
|
18297
|
+
return [...labels].sort((a, b) => a.localeCompare(b));
|
|
18298
|
+
}
|
|
18299
|
+
async function ensureNorthStarLabels(client, repo, specs) {
|
|
18300
|
+
const minted = [];
|
|
18301
|
+
for (const name of northStarLabelsForBatch(specs)) {
|
|
18302
|
+
try {
|
|
18303
|
+
await client.rest("POST", `repos/${repo}/labels`, {
|
|
18304
|
+
body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
|
|
18305
|
+
});
|
|
18306
|
+
minted.push(name);
|
|
18307
|
+
} catch {
|
|
18308
|
+
}
|
|
18309
|
+
}
|
|
18310
|
+
return minted;
|
|
18311
|
+
}
|
|
18203
18312
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
18204
18313
|
const client = deps.client ?? defaultGitHubClient();
|
|
18205
18314
|
const validation = validateBatchSpecs(specs);
|
|
@@ -18210,6 +18319,7 @@ ${lines}`);
|
|
|
18210
18319
|
}
|
|
18211
18320
|
const repo = await resolveRepo(options.repo);
|
|
18212
18321
|
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
18322
|
+
await ensureNorthStarLabels(client, repo, validation.validated.map((v) => v.spec));
|
|
18213
18323
|
const created = [];
|
|
18214
18324
|
const failures = [];
|
|
18215
18325
|
for (const entry of validation.validated) {
|
|
@@ -19588,7 +19698,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
19588
19698
|
const { state, rows } = await snap();
|
|
19589
19699
|
const output = { state, checks: rows };
|
|
19590
19700
|
console.log(JSON.stringify(output));
|
|
19591
|
-
if (state === "failure") process.exitCode = 1;
|
|
19701
|
+
if (state === "failure" || state === "failing") process.exitCode = 1;
|
|
19592
19702
|
return;
|
|
19593
19703
|
}
|
|
19594
19704
|
const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
|
|
@@ -20509,11 +20619,15 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
20509
20619
|
}
|
|
20510
20620
|
}
|
|
20511
20621
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
20512
|
-
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
|
|
20622
|
+
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o, cmd) => {
|
|
20513
20623
|
try {
|
|
20624
|
+
if (o.base !== void 0 && cmd.getOptionValueSource("from") === "cli" && o.base !== o.from) {
|
|
20625
|
+
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base} \u2014 pass one`);
|
|
20626
|
+
}
|
|
20627
|
+
const fromRef = o.base ?? o.from;
|
|
20514
20628
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20515
20629
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
|
|
20516
|
-
const { base, fetchBranch } = resolveWorktreeBase(
|
|
20630
|
+
const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
|
|
20517
20631
|
if (fetchBranch) {
|
|
20518
20632
|
const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
|
|
20519
20633
|
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
@@ -20969,7 +21083,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
20969
21083
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
20970
21084
|
return reportWrite("project attest", res);
|
|
20971
21085
|
});
|
|
20972
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
21086
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
20973
21087
|
const cfg = await loadConfig();
|
|
20974
21088
|
let target;
|
|
20975
21089
|
try {
|
|
@@ -20979,7 +21093,10 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
20979
21093
|
}
|
|
20980
21094
|
const slug = slugOf(target);
|
|
20981
21095
|
const repo = target.includes("/") ? target : `mutmutco/${slug}`;
|
|
20982
|
-
const
|
|
21096
|
+
const setAlias = o.set ?? [];
|
|
21097
|
+
const vars = [...o.var ?? [], ...setAlias];
|
|
21098
|
+
const dupe = duplicateVarKeyAcrossFlags(o.var ?? [], setAlias);
|
|
21099
|
+
if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
20983
21100
|
if (o.secretsFile) {
|
|
20984
21101
|
try {
|
|
20985
21102
|
vars.push(`secrets=${(0, import_node_fs24.readFileSync)(o.secretsFile, "utf8")}`);
|
|
@@ -20995,7 +21112,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
20995
21112
|
deployModel: o.deployModel,
|
|
20996
21113
|
releaseTrack: o.releaseTrack,
|
|
20997
21114
|
vars,
|
|
20998
|
-
unsets:
|
|
21115
|
+
unsets: o.unset ?? [],
|
|
20999
21116
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
21000
21117
|
});
|
|
21001
21118
|
} catch (e) {
|
|
@@ -21614,7 +21731,13 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
21614
21731
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
21615
21732
|
const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
|
|
21616
21733
|
if (!(0, import_node_fs24.existsSync)(wfDir)) return [];
|
|
21617
|
-
return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).
|
|
21734
|
+
return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
21735
|
+
try {
|
|
21736
|
+
return workflowReportsPrChecks((0, import_node_fs24.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
|
|
21737
|
+
} catch {
|
|
21738
|
+
return true;
|
|
21739
|
+
}
|
|
21740
|
+
}).map((name) => `.github/workflows/${name}`);
|
|
21618
21741
|
}
|
|
21619
21742
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
21620
21743
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -21671,17 +21794,30 @@ async function pollGhPrChecks(prNumber, repoArgs) {
|
|
|
21671
21794
|
}
|
|
21672
21795
|
return state;
|
|
21673
21796
|
}
|
|
21674
|
-
pr.command("checks-wait <number>").description(
|
|
21797
|
+
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
|
|
21675
21798
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
21799
|
+
let timeoutMs;
|
|
21800
|
+
if (o.timeout !== void 0) {
|
|
21801
|
+
const minutes = Number(o.timeout);
|
|
21802
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return fail("pr checks-wait: --timeout must be a positive number of minutes");
|
|
21803
|
+
timeoutMs = Math.round(minutes * 6e4);
|
|
21804
|
+
}
|
|
21676
21805
|
const result = await waitForPrChecks({
|
|
21677
21806
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
21678
21807
|
pollChecks: () => pollGhPrChecks(number, repoArgs),
|
|
21679
21808
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
21680
|
-
log: (message) => console.warn(message)
|
|
21809
|
+
log: (message) => console.warn(message),
|
|
21810
|
+
timeoutMs,
|
|
21811
|
+
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
21812
|
+
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
21813
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
21681
21814
|
});
|
|
21682
21815
|
if (o.json) printLine(JSON.stringify(result));
|
|
21683
|
-
else
|
|
21684
|
-
|
|
21816
|
+
else if (result.status === "timeout") {
|
|
21817
|
+
printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
|
|
21818
|
+
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
21819
|
+
if (result.status === "failure") process.exitCode = 1;
|
|
21820
|
+
if (result.status === "timeout") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
21685
21821
|
});
|
|
21686
21822
|
async function ghMergeAutoEnqueue(prNumber, repo, method) {
|
|
21687
21823
|
const args = repo ? ["--repo", repo] : [];
|
|
@@ -21764,7 +21900,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
21764
21900
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
21765
21901
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
21766
21902
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
21767
|
-
log: (message) => console.warn(message)
|
|
21903
|
+
log: (message) => console.warn(message),
|
|
21904
|
+
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
|
|
21905
|
+
// 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
|
|
21906
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
21768
21907
|
}),
|
|
21769
21908
|
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
|
|
21770
21909
|
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
@@ -21946,6 +22085,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
21946
22085
|
}
|
|
21947
22086
|
const remoteBefore = await remoteBranchExists2(headRef);
|
|
21948
22087
|
let remoteDeleteAttempted = false;
|
|
22088
|
+
let upgradedToAuto = false;
|
|
21949
22089
|
let remoteNotAttemptedReason = headIsProtected ? "protected-branch" : void 0;
|
|
21950
22090
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
|
|
21951
22091
|
const message = String(e.message || "");
|
|
@@ -21969,14 +22109,30 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
21969
22109
|
return;
|
|
21970
22110
|
}
|
|
21971
22111
|
if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
|
|
21972
|
-
|
|
22112
|
+
console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
|
|
22113
|
+
upgradedToAuto = true;
|
|
22114
|
+
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
22115
|
+
const m2 = String(e2.message || "");
|
|
22116
|
+
if (/already been merged/i.test(m2)) {
|
|
22117
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
22118
|
+
return;
|
|
22119
|
+
}
|
|
22120
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
22121
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
22122
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
22123
|
+
});
|
|
22124
|
+
return;
|
|
21973
22125
|
}
|
|
21974
22126
|
if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
|
|
21975
22127
|
});
|
|
21976
|
-
if (o.auto) {
|
|
22128
|
+
if (o.auto || upgradedToAuto) {
|
|
21977
22129
|
const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS5 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
21978
22130
|
if (state !== "MERGED") {
|
|
21979
|
-
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown" }));
|
|
22131
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
22132
|
+
if (upgradedToAuto && !o.auto) {
|
|
22133
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 auto-merge will land it once required checks pass. Exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
22134
|
+
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
22135
|
+
}
|
|
21980
22136
|
return;
|
|
21981
22137
|
}
|
|
21982
22138
|
}
|
|
@@ -22310,19 +22466,6 @@ function rawValue(flag, fallback) {
|
|
|
22310
22466
|
const inline = process.argv.find((arg) => arg.startsWith(`${flag}=`));
|
|
22311
22467
|
return inline ? inline.slice(flag.length + 1) : fallback;
|
|
22312
22468
|
}
|
|
22313
|
-
function rawValues(flag) {
|
|
22314
|
-
const out = [];
|
|
22315
|
-
for (let i = 0; i < process.argv.length; i++) {
|
|
22316
|
-
const arg = process.argv[i];
|
|
22317
|
-
if (arg === flag && process.argv[i + 1]) {
|
|
22318
|
-
out.push(process.argv[i + 1]);
|
|
22319
|
-
i++;
|
|
22320
|
-
continue;
|
|
22321
|
-
}
|
|
22322
|
-
if (arg.startsWith(`${flag}=`)) out.push(arg.slice(flag.length + 1));
|
|
22323
|
-
}
|
|
22324
|
-
return out;
|
|
22325
|
-
}
|
|
22326
22469
|
function stagePortFromArgv() {
|
|
22327
22470
|
const raw = rawValue("--port", "");
|
|
22328
22471
|
if (!raw) return void 0;
|
|
@@ -22890,7 +23033,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
|
|
|
22890
23033
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
22891
23034
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
22892
23035
|
});
|
|
22893
|
-
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)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
23036
|
+
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)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
22894
23037
|
const o = {
|
|
22895
23038
|
class: rawValue("--class", "deployable"),
|
|
22896
23039
|
projectType: rawValue("--project-type", ""),
|
|
@@ -22916,7 +23059,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22916
23059
|
const readFile4 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
|
|
22917
23060
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
22918
23061
|
const rawVars = {};
|
|
22919
|
-
for (const value of
|
|
23062
|
+
for (const value of cmdOpts.var ?? []) {
|
|
22920
23063
|
const eq = value.indexOf("=");
|
|
22921
23064
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
22922
23065
|
}
|
|
@@ -22937,6 +23080,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22937
23080
|
const actions = [];
|
|
22938
23081
|
const applied = [];
|
|
22939
23082
|
let applyDeployModel;
|
|
23083
|
+
let applyProjectType;
|
|
22940
23084
|
try {
|
|
22941
23085
|
const payload = buildRegisterPayload(repo, o.class, vars, {
|
|
22942
23086
|
projectType: o.projectType || void 0,
|
|
@@ -22944,6 +23088,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22944
23088
|
releaseTrack: o.releaseTrack || void 0
|
|
22945
23089
|
});
|
|
22946
23090
|
applyDeployModel = payload.deployModel;
|
|
23091
|
+
applyProjectType = payload.projectType;
|
|
22947
23092
|
} catch {
|
|
22948
23093
|
}
|
|
22949
23094
|
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
@@ -22968,6 +23113,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22968
23113
|
for (const seed of manifest.seeds) {
|
|
22969
23114
|
if (!seed.classes.includes(o.class)) continue;
|
|
22970
23115
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
23116
|
+
if (!seedMatchesProjectType(seed, applyProjectType)) continue;
|
|
22971
23117
|
const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
|
|
22972
23118
|
let exists = false;
|
|
22973
23119
|
let sha;
|
|
@@ -22991,7 +23137,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22991
23137
|
const planned = planSeedAction(resolved, exists);
|
|
22992
23138
|
const isBlock = resolved.source === "managed-block";
|
|
22993
23139
|
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile4) : null;
|
|
22994
|
-
const action = reconcileSeedAction(planned, content, isBlock);
|
|
23140
|
+
const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
|
|
22995
23141
|
actions.push(action);
|
|
22996
23142
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
22997
23143
|
await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
|
|
@@ -23024,10 +23170,17 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
23024
23170
|
]);
|
|
23025
23171
|
seedPrUrl = created.url;
|
|
23026
23172
|
}
|
|
23173
|
+
let autoMergeEnabled = true;
|
|
23027
23174
|
await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
23028
|
-
|
|
23175
|
+
const message = String(e.message ?? "");
|
|
23176
|
+
if (/already/i.test(message)) return;
|
|
23177
|
+
if (/clean status|enablePullRequestAutoMerge/i.test(message)) {
|
|
23178
|
+
autoMergeEnabled = false;
|
|
23179
|
+
return;
|
|
23180
|
+
}
|
|
23181
|
+
throw e;
|
|
23029
23182
|
});
|
|
23030
|
-
applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
|
|
23183
|
+
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 pr land <n> --repo ${repo})`);
|
|
23031
23184
|
}
|
|
23032
23185
|
if (o.execute && o.class === "deployable") {
|
|
23033
23186
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|