@mutmutco/cli 3.32.0 → 3.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/main.cjs +334 -943
- package/package.json +2 -2
package/dist/main.cjs
CHANGED
|
@@ -3520,7 +3520,10 @@ async function cleanExit(code) {
|
|
|
3520
3520
|
await new Promise((resolve5) => setImmediate(resolve5));
|
|
3521
3521
|
process.exit(code);
|
|
3522
3522
|
}
|
|
3523
|
-
var CLI_EXIT_WATCHDOG_MS =
|
|
3523
|
+
var CLI_EXIT_WATCHDOG_MS = (() => {
|
|
3524
|
+
const testOverride = Number(process.env.MMI_CLI_TEST_EXIT_WATCHDOG_MS);
|
|
3525
|
+
return process.env.NODE_ENV === "test" && Number.isFinite(testOverride) && testOverride >= 25 ? testOverride : 1e4;
|
|
3526
|
+
})();
|
|
3524
3527
|
async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
|
|
3525
3528
|
const timer = setTimeout(() => {
|
|
3526
3529
|
console.error(
|
|
@@ -4928,7 +4931,8 @@ function retriedFetch(deps, url, init) {
|
|
|
4928
4931
|
const headers = { ...clientVersionHeaders(), ...init.headers };
|
|
4929
4932
|
return fetchWithRetry(deps.fetch ?? fetch, url, { ...init, headers }, {
|
|
4930
4933
|
attempts: RETRY_ATTEMPTS,
|
|
4931
|
-
timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS
|
|
4934
|
+
timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS,
|
|
4935
|
+
sleep: deps.retrySleep
|
|
4932
4936
|
});
|
|
4933
4937
|
}
|
|
4934
4938
|
async function fetchTrainAuthority(repo, deps) {
|
|
@@ -4992,23 +4996,6 @@ async function fetchProjectBySlug(slug, deps) {
|
|
|
4992
4996
|
const res = await fetchProjectBySlugChecked(slug, deps);
|
|
4993
4997
|
return res.ok ? res.project : null;
|
|
4994
4998
|
}
|
|
4995
|
-
async function fetchDeployStatusBySlug(slug, deps) {
|
|
4996
|
-
if (!deps.baseUrl || !slug) return null;
|
|
4997
|
-
const token = await deps.token();
|
|
4998
|
-
if (!token) return null;
|
|
4999
|
-
try {
|
|
5000
|
-
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
5001
|
-
method: "GET",
|
|
5002
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
5003
|
-
});
|
|
5004
|
-
if (!res.ok) return null;
|
|
5005
|
-
const body = await res.json();
|
|
5006
|
-
if (!body?.stages) return null;
|
|
5007
|
-
return { stages: body.stages, deployState: body.deployState ?? {} };
|
|
5008
|
-
} catch {
|
|
5009
|
-
return null;
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5012
4999
|
async function fetchDeployFactsBySlug(slug, deps) {
|
|
5013
5000
|
if (!deps.baseUrl || !slug) return null;
|
|
5014
5001
|
const token = await deps.token();
|
|
@@ -5079,8 +5066,8 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
|
5079
5066
|
const token = await deps.token();
|
|
5080
5067
|
if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
|
|
5081
5068
|
const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
|
|
5082
|
-
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs });
|
|
5083
|
-
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs });
|
|
5069
|
+
const sendOnce = (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: 1, timeoutMs, sleep: deps.retrySleep });
|
|
5070
|
+
const send = opts.noRetry ? sendOnce : (url, init) => fetchWithRetry(deps.fetch ?? fetch, url, init, { attempts: RETRY_ATTEMPTS, timeoutMs, sleep: deps.retrySleep });
|
|
5084
5071
|
try {
|
|
5085
5072
|
const res = await send(`${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
5086
5073
|
method,
|
|
@@ -5106,9 +5093,6 @@ async function upsertProject(slug, patch, deps) {
|
|
|
5106
5093
|
async function retireProject(slug, deps) {
|
|
5107
5094
|
return postJson(`/projects/${encodeURIComponent(slug)}`, {}, deps, "DELETE", { noRetry: true });
|
|
5108
5095
|
}
|
|
5109
|
-
async function attestAppGaps(slug, repo, deps) {
|
|
5110
|
-
return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
|
|
5111
|
-
}
|
|
5112
5096
|
async function setDeployCoords(slug, payload, deps) {
|
|
5113
5097
|
return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
|
|
5114
5098
|
}
|
|
@@ -5118,6 +5102,9 @@ async function recordDocsAudit(verdict, deps) {
|
|
|
5118
5102
|
async function tenantControl(payload, deps) {
|
|
5119
5103
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
5120
5104
|
}
|
|
5105
|
+
async function tenantReconcile(payload, deps) {
|
|
5106
|
+
return postJson("/tenant-reconcile", payload, deps, "POST", { noRetry: true });
|
|
5107
|
+
}
|
|
5121
5108
|
async function tenantDeploy(payload, deps) {
|
|
5122
5109
|
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
|
|
5123
5110
|
}
|
|
@@ -6202,9 +6189,6 @@ function resolveDeployModel(meta, repo) {
|
|
|
6202
6189
|
if (projectType === "cli-tool") return "registry-publish";
|
|
6203
6190
|
return "tenant-container";
|
|
6204
6191
|
}
|
|
6205
|
-
function projectTypeClearsWebProfile(projectType, deployModel) {
|
|
6206
|
-
return projectType === "content" || projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
|
|
6207
|
-
}
|
|
6208
6192
|
function inferReleaseTrackFromBranches(hints) {
|
|
6209
6193
|
if (hints.hasRcBranch) return void 0;
|
|
6210
6194
|
if (hints.hasDevelopmentBranch && hints.hasMainBranch) return "direct";
|
|
@@ -7172,9 +7156,10 @@ async function secretsUse(deps, key, opts) {
|
|
|
7172
7156
|
"",
|
|
7173
7157
|
"Consume it WITHOUT committing it:",
|
|
7174
7158
|
` \u2022 Keyless run: \`mmi-cli secrets use ${key}${opts.slug ? ` --slug ${opts.slug}` : ""} -- <command>\` injects it into the command's env (never printed). A granted non-master can USE an org secret this way.`,
|
|
7175
|
-
|
|
7159
|
+
" \u2022 Runtime: the central deploy injects the declared stage env; broker consumers use the workload's scoped runtime token. Never bake a value into an image or commit it.",
|
|
7176
7160
|
` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
|
|
7177
|
-
|
|
7161
|
+
" \u2022 Own project: the stageless canonical and any declared dev/rc/main overrides are project-admin self-service.",
|
|
7162
|
+
" \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted."
|
|
7178
7163
|
].join("\n")
|
|
7179
7164
|
);
|
|
7180
7165
|
return;
|
|
@@ -8934,6 +8919,14 @@ function defaultWorktreePath(repoRoot2, branch) {
|
|
|
8934
8919
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
8935
8920
|
return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", safe);
|
|
8936
8921
|
}
|
|
8922
|
+
async function primaryCheckoutRootOf(git) {
|
|
8923
|
+
try {
|
|
8924
|
+
const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
|
|
8925
|
+
return out ? (0, import_node_path11.dirname)(out) : void 0;
|
|
8926
|
+
} catch {
|
|
8927
|
+
return void 0;
|
|
8928
|
+
}
|
|
8929
|
+
}
|
|
8937
8930
|
function resolveWorktreeBase(from, remote) {
|
|
8938
8931
|
const remotePrefix = `${remote}/`;
|
|
8939
8932
|
const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
|
|
@@ -10386,7 +10379,22 @@ var import_node_util6 = require("node:util");
|
|
|
10386
10379
|
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
|
|
10387
10380
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
10388
10381
|
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
10389
|
-
function
|
|
10382
|
+
function earlyExitGraceMs() {
|
|
10383
|
+
if (process.env.NODE_ENV === "test") {
|
|
10384
|
+
const testOverride = Number(process.env.MMI_CLI_TEST_EARLY_EXIT_GRACE_MS);
|
|
10385
|
+
if (Number.isFinite(testOverride) && testOverride >= 25) return testOverride;
|
|
10386
|
+
}
|
|
10387
|
+
return EARLY_EXIT_GRACE_MS;
|
|
10388
|
+
}
|
|
10389
|
+
var HEALTH_POLL_INTERVAL_MS = 1e3;
|
|
10390
|
+
function healthPollIntervalMs() {
|
|
10391
|
+
if (process.env.NODE_ENV === "test") {
|
|
10392
|
+
const testOverride = Number(process.env.MMI_CLI_TEST_HEALTH_POLL_MS);
|
|
10393
|
+
if (Number.isFinite(testOverride) && testOverride >= 10) return testOverride;
|
|
10394
|
+
}
|
|
10395
|
+
return HEALTH_POLL_INTERVAL_MS;
|
|
10396
|
+
}
|
|
10397
|
+
function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
10390
10398
|
return new Promise((resolve5, reject) => {
|
|
10391
10399
|
let settled = false;
|
|
10392
10400
|
const finish = (fn) => {
|
|
@@ -10801,7 +10809,7 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
|
10801
10809
|
async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
10802
10810
|
await killTree(state.pid);
|
|
10803
10811
|
if (state.teardown?.command.trim()) {
|
|
10804
|
-
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
|
|
10812
|
+
await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
|
|
10805
10813
|
}
|
|
10806
10814
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
10807
10815
|
(0, import_node_fs14.rmSync)(path2, { force: true });
|
|
@@ -10842,7 +10850,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
|
|
|
10842
10850
|
} catch (e) {
|
|
10843
10851
|
last = e.message;
|
|
10844
10852
|
}
|
|
10845
|
-
await new Promise((resolve5) => setTimeout(resolve5,
|
|
10853
|
+
await new Promise((resolve5) => setTimeout(resolve5, healthPollIntervalMs()));
|
|
10846
10854
|
}
|
|
10847
10855
|
throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
|
|
10848
10856
|
}
|
|
@@ -11283,29 +11291,6 @@ function resolveIssueTitle(input, deps) {
|
|
|
11283
11291
|
noun: "title"
|
|
11284
11292
|
});
|
|
11285
11293
|
}
|
|
11286
|
-
var NORTH_STAR_BODY_PREFIX = "North Star:";
|
|
11287
|
-
var NORTH_STAR_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
11288
|
-
function normalizeNorthStarSlug(raw) {
|
|
11289
|
-
const slug = raw.trim();
|
|
11290
|
-
if (!slug || !NORTH_STAR_SLUG_RE.test(slug)) {
|
|
11291
|
-
throw new Error(`invalid North Star slug "${raw}" \u2014 expected kebab-case like agent-session-workflow`);
|
|
11292
|
-
}
|
|
11293
|
-
return slug;
|
|
11294
|
-
}
|
|
11295
|
-
function northStarLabel(slug) {
|
|
11296
|
-
return `northstar:${slug}`;
|
|
11297
|
-
}
|
|
11298
|
-
function appendNorthStarLine(body, slug) {
|
|
11299
|
-
const line = `${NORTH_STAR_BODY_PREFIX} ${slug}`;
|
|
11300
|
-
const existing = new RegExp(`^${NORTH_STAR_BODY_PREFIX}\\s+\\S+\\s*$`, "m");
|
|
11301
|
-
if (existing.test(body)) return body;
|
|
11302
|
-
const trimmed = body.replace(/\s+$/, "");
|
|
11303
|
-
return trimmed ? `${trimmed}
|
|
11304
|
-
|
|
11305
|
-
${line}
|
|
11306
|
-
` : `${line}
|
|
11307
|
-
`;
|
|
11308
|
-
}
|
|
11309
11294
|
|
|
11310
11295
|
// src/issue-view-json.ts
|
|
11311
11296
|
var DEFAULT_ISSUE_VIEW_FIELDS = "number,title,state,url,labels,author,assignees,milestone,body";
|
|
@@ -11504,20 +11489,18 @@ for (const [helpGroup, names] of PRIMARY_GROUPS) {
|
|
|
11504
11489
|
for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
|
|
11505
11490
|
}
|
|
11506
11491
|
HELP_GROUP_ORDER.set("Operations", HELP_GROUP_ORDER.size);
|
|
11507
|
-
HELP_GROUP_ORDER.set("Compatibility", HELP_GROUP_ORDER.size);
|
|
11508
|
-
HELP_GROUP_ORDER.set("Delegated", HELP_GROUP_ORDER.size);
|
|
11509
11492
|
for (const name of OPERATIONAL_TOP_LEVEL) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
|
|
11510
|
-
TOP_LEVEL_ORDER.set("lane", topLevelPosition++);
|
|
11511
|
-
TOP_LEVEL_ORDER.set("fusion", topLevelPosition++);
|
|
11512
11493
|
var PATH_OVERRIDES = {
|
|
11513
11494
|
"board slice-refresh": { category: "internal", discovery: "hidden", help_group: "Operations" },
|
|
11514
|
-
|
|
11515
|
-
|
|
11495
|
+
"secrets find": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11496
|
+
"secrets catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11497
|
+
"secrets doctor": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11498
|
+
"secrets org-catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11499
|
+
"secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11500
|
+
"secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
|
|
11516
11501
|
};
|
|
11517
11502
|
var HIDDEN_OPTIONS = {
|
|
11518
11503
|
"worktree create": ["--base"],
|
|
11519
|
-
"org project doctor": ["--v2"],
|
|
11520
|
-
"org project heal": ["--v2"],
|
|
11521
11504
|
"org project set": ["--set"]
|
|
11522
11505
|
};
|
|
11523
11506
|
function primaryMetadata(name) {
|
|
@@ -11535,7 +11518,6 @@ function primaryMetadata(name) {
|
|
|
11535
11518
|
function topLevelMetadata(name) {
|
|
11536
11519
|
const primary = primaryMetadata(name);
|
|
11537
11520
|
if (primary) return primary;
|
|
11538
|
-
if (name === "lane" || name === "fusion") return PATH_OVERRIDES[name];
|
|
11539
11521
|
if (!OPERATIONAL_TOP_LEVEL.has(name)) {
|
|
11540
11522
|
throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
|
|
11541
11523
|
}
|
|
@@ -11605,113 +11587,7 @@ function commandHelpGroupRank(group) {
|
|
|
11605
11587
|
return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
|
|
11606
11588
|
}
|
|
11607
11589
|
function isCanonicalSuggestion(metadata) {
|
|
11608
|
-
return metadata.category !== "internal"
|
|
11609
|
-
}
|
|
11610
|
-
|
|
11611
|
-
// src/command-shims.ts
|
|
11612
|
-
var COMMAND_SHIMS = [
|
|
11613
|
-
{ legacy: "project deploy list", canonical: "org project deploy get", category: "shim" },
|
|
11614
|
-
{ legacy: "project resolve", canonical: "org project deploy get", category: "obsolete" },
|
|
11615
|
-
{ legacy: "tenant readiness", canonical: "runtime tenant status", category: "shim" },
|
|
11616
|
-
{ legacy: "secrets edit", canonical: "secrets set", category: "shim" },
|
|
11617
|
-
{ legacy: "registry org", canonical: "org config get", category: "shim" },
|
|
11618
|
-
{ legacy: "rules gitignore", canonical: "org rules gitignore", category: "shim" },
|
|
11619
|
-
{ legacy: "deploy status", canonical: "runtime deploy status", category: "shim" },
|
|
11620
|
-
{ legacy: "full-track readiness", canonical: "train readiness", category: "shim" },
|
|
11621
|
-
{ legacy: "docs-audit", canonical: "docs audit", category: "shim", subtree: true },
|
|
11622
|
-
{ legacy: "plugin-heal", canonical: "plugin heal", category: "shim" },
|
|
11623
|
-
{ legacy: "plugin-prune", canonical: "plugin prune", category: "shim" },
|
|
11624
|
-
{ legacy: "session-start", canonical: "plugin session-start", category: "shim" },
|
|
11625
|
-
{ legacy: "port-range", canonical: "stage port-range", category: "shim" },
|
|
11626
|
-
{ legacy: "schedules", canonical: "org schedules", category: "shim" },
|
|
11627
|
-
{ legacy: "guard", canonical: "plugin guard", category: "shim" },
|
|
11628
|
-
{ legacy: "project", canonical: "org project", category: "shim", subtree: true },
|
|
11629
|
-
{ legacy: "oauth", canonical: "org oauth", category: "shim", subtree: true },
|
|
11630
|
-
{ legacy: "access", canonical: "org access", category: "shim", subtree: true },
|
|
11631
|
-
{ legacy: "tenant", canonical: "runtime tenant", category: "shim", subtree: true },
|
|
11632
|
-
{ legacy: "box", canonical: "runtime box", category: "shim", subtree: true },
|
|
11633
|
-
{ legacy: "edge", canonical: "runtime edge", category: "shim", subtree: true },
|
|
11634
|
-
{ legacy: "gc", canonical: "worktree gc", category: "shim", subtree: true }
|
|
11635
|
-
];
|
|
11636
|
-
function rewriteLegacyArgv(argv) {
|
|
11637
|
-
const commandArgs = argv.slice(2);
|
|
11638
|
-
for (const shim of COMMAND_SHIMS) {
|
|
11639
|
-
const legacy = shim.legacy.split(" ");
|
|
11640
|
-
if (!legacy.every((token, index) => commandArgs[index] === token)) continue;
|
|
11641
|
-
const canonical = shim.canonical.split(" ");
|
|
11642
|
-
return {
|
|
11643
|
-
argv: [...argv.slice(0, 2), ...canonical, ...commandArgs.slice(legacy.length)],
|
|
11644
|
-
shim,
|
|
11645
|
-
warning: `mmi-cli: \`${shim.legacy}\` moved to \`${shim.canonical}\`; use \`mmi-cli ${shim.canonical}\`.`
|
|
11646
|
-
};
|
|
11647
|
-
}
|
|
11648
|
-
return { argv: [...argv] };
|
|
11649
|
-
}
|
|
11650
|
-
|
|
11651
|
-
// src/command-consolidation.ts
|
|
11652
|
-
var COMMAND_NAMESPACES_CONSOLIDATED = /* @__PURE__ */ Symbol.for("mmi.commandNamespaces.consolidated");
|
|
11653
|
-
function child(parent, name) {
|
|
11654
|
-
const found = parent.commands.find((command) => command.name() === name);
|
|
11655
|
-
if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
|
|
11656
|
-
return found;
|
|
11657
|
-
}
|
|
11658
|
-
function detach(parent, command) {
|
|
11659
|
-
const commands = parent.commands;
|
|
11660
|
-
const index = commands.indexOf(command);
|
|
11661
|
-
if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
|
|
11662
|
-
commands.splice(index, 1);
|
|
11663
|
-
return command;
|
|
11664
|
-
}
|
|
11665
|
-
function move(parent, destination, name, nextName = name) {
|
|
11666
|
-
const command = detach(parent, child(parent, name));
|
|
11667
|
-
command.name(nextName);
|
|
11668
|
-
destination.addCommand(command);
|
|
11669
|
-
return command;
|
|
11670
|
-
}
|
|
11671
|
-
function remove(parent, name) {
|
|
11672
|
-
detach(parent, child(parent, name));
|
|
11673
|
-
}
|
|
11674
|
-
function consolidateCommandNamespaces(program3) {
|
|
11675
|
-
const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
|
|
11676
|
-
const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
|
|
11677
|
-
const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
|
|
11678
|
-
move(program3, org, "project");
|
|
11679
|
-
move(program3, org, "oauth");
|
|
11680
|
-
move(program3, org, "access");
|
|
11681
|
-
move(program3, org, "schedules");
|
|
11682
|
-
const config = org.command("config").description("organization configuration");
|
|
11683
|
-
const registry2 = child(program3, "registry");
|
|
11684
|
-
move(registry2, config, "org", "get");
|
|
11685
|
-
remove(program3, "registry");
|
|
11686
|
-
const orgRules = org.command("rules").description("organization-managed repository rules");
|
|
11687
|
-
const rules2 = child(program3, "rules");
|
|
11688
|
-
move(rules2, orgRules, "gitignore");
|
|
11689
|
-
remove(program3, "rules");
|
|
11690
|
-
move(program3, runtime, "tenant");
|
|
11691
|
-
const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
|
|
11692
|
-
const deploy = child(program3, "deploy");
|
|
11693
|
-
move(deploy, runtimeDeploy, "status");
|
|
11694
|
-
remove(program3, "deploy");
|
|
11695
|
-
move(program3, runtime, "box");
|
|
11696
|
-
move(program3, runtime, "edge");
|
|
11697
|
-
move(program3, plugin, "guard");
|
|
11698
|
-
move(program3, plugin, "plugin-heal", "heal");
|
|
11699
|
-
move(program3, plugin, "plugin-prune", "prune");
|
|
11700
|
-
move(program3, plugin, "session-start");
|
|
11701
|
-
const docs2 = child(program3, "docs");
|
|
11702
|
-
move(program3, docs2, "docs-audit", "audit");
|
|
11703
|
-
const train = child(program3, "train");
|
|
11704
|
-
const fullTrack2 = child(program3, "full-track");
|
|
11705
|
-
move(fullTrack2, train, "readiness");
|
|
11706
|
-
remove(program3, "full-track");
|
|
11707
|
-
const worktree2 = child(program3, "worktree");
|
|
11708
|
-
move(program3, worktree2, "gc");
|
|
11709
|
-
const stage = child(program3, "stage");
|
|
11710
|
-
move(program3, stage, "port-range");
|
|
11711
|
-
program3[COMMAND_NAMESPACES_CONSOLIDATED] = true;
|
|
11712
|
-
}
|
|
11713
|
-
function hasConsolidatedCommandNamespaces(program3) {
|
|
11714
|
-
return program3[COMMAND_NAMESPACES_CONSOLIDATED] === true;
|
|
11590
|
+
return metadata.category !== "internal";
|
|
11715
11591
|
}
|
|
11716
11592
|
|
|
11717
11593
|
// src/command-manifest.ts
|
|
@@ -11780,63 +11656,6 @@ function buildCommand(cmd, path2) {
|
|
|
11780
11656
|
if (commonMistakes) out.common_mistakes = commonMistakes;
|
|
11781
11657
|
return out;
|
|
11782
11658
|
}
|
|
11783
|
-
function findTreeCommand(root, path2) {
|
|
11784
|
-
if (root.path === path2) return root;
|
|
11785
|
-
for (const child2 of root.subcommands) {
|
|
11786
|
-
const found = findTreeCommand(child2, path2);
|
|
11787
|
-
if (found) return found;
|
|
11788
|
-
}
|
|
11789
|
-
return void 0;
|
|
11790
|
-
}
|
|
11791
|
-
function virtualCopy(source, path2, shim) {
|
|
11792
|
-
return {
|
|
11793
|
-
...source,
|
|
11794
|
-
name: path2.split(" ").at(-1),
|
|
11795
|
-
path: path2,
|
|
11796
|
-
category: shim.category,
|
|
11797
|
-
discovery: "all-only",
|
|
11798
|
-
help_group: "Compatibility",
|
|
11799
|
-
replacement: source.path,
|
|
11800
|
-
subcommands: shim.subtree ? source.subcommands.map((child2) => virtualCopy(child2, `${path2} ${child2.name}`, shim)) : []
|
|
11801
|
-
};
|
|
11802
|
-
}
|
|
11803
|
-
function virtualParent(path2, shim) {
|
|
11804
|
-
return {
|
|
11805
|
-
name: path2.split(" ").at(-1),
|
|
11806
|
-
path: path2,
|
|
11807
|
-
description: `compatibility namespace; use mmi-cli ${shim.canonical}`,
|
|
11808
|
-
arguments: [],
|
|
11809
|
-
options: [],
|
|
11810
|
-
subcommands: [],
|
|
11811
|
-
category: "shim",
|
|
11812
|
-
discovery: "all-only",
|
|
11813
|
-
help_group: "Compatibility",
|
|
11814
|
-
replacement: shim.canonical
|
|
11815
|
-
};
|
|
11816
|
-
}
|
|
11817
|
-
function insertVirtualShim(root, shim) {
|
|
11818
|
-
const canonical = findTreeCommand(root, shim.canonical);
|
|
11819
|
-
if (!canonical) throw new Error(`command manifest: shim target missing "${shim.canonical}"`);
|
|
11820
|
-
const parts = shim.legacy.split(" ");
|
|
11821
|
-
let parent = root;
|
|
11822
|
-
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
11823
|
-
const path2 = parts.slice(0, index + 1).join(" ");
|
|
11824
|
-
let next = parent.subcommands.find((command) => command.name === parts[index]);
|
|
11825
|
-
if (!next) {
|
|
11826
|
-
next = virtualParent(path2, shim);
|
|
11827
|
-
parent.subcommands.push(next);
|
|
11828
|
-
}
|
|
11829
|
-
parent = next;
|
|
11830
|
-
}
|
|
11831
|
-
const copy = virtualCopy(canonical, shim.legacy, shim);
|
|
11832
|
-
const existingIndex = parent.subcommands.findIndex((command) => command.name === copy.name);
|
|
11833
|
-
if (existingIndex >= 0) parent.subcommands[existingIndex] = copy;
|
|
11834
|
-
else parent.subcommands.push(copy);
|
|
11835
|
-
}
|
|
11836
|
-
function addVirtualShims(root) {
|
|
11837
|
-
for (const shim of COMMAND_SHIMS.filter((entry) => entry.subtree)) insertVirtualShim(root, shim);
|
|
11838
|
-
for (const shim of COMMAND_SHIMS.filter((entry) => !entry.subtree)) insertVirtualShim(root, shim);
|
|
11839
|
-
}
|
|
11840
11659
|
function primaryCopy(node, root = false) {
|
|
11841
11660
|
if (!root && node.discovery !== "primary") return void 0;
|
|
11842
11661
|
const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
|
|
@@ -11856,7 +11675,6 @@ function collectLeaves(node, acc) {
|
|
|
11856
11675
|
}
|
|
11857
11676
|
function buildCommandManifest(program3) {
|
|
11858
11677
|
const tree = buildCommand(program3, "");
|
|
11859
|
-
if (hasConsolidatedCommandNamespaces(program3)) addVirtualShims(tree);
|
|
11860
11678
|
const index = [];
|
|
11861
11679
|
collectLeaves(tree, index);
|
|
11862
11680
|
const primaryTree = primaryCopy(tree, true);
|
|
@@ -11913,10 +11731,71 @@ function formatManifestHuman(manifest, options = {}) {
|
|
|
11913
11731
|
}
|
|
11914
11732
|
render(command, 1);
|
|
11915
11733
|
}
|
|
11916
|
-
lines.push("", options.all ? "Use `mmi-cli explain <group|command>` for a focused map." : "Use `mmi-cli explain <group|command>` for detail or `mmi-cli commands --all` for
|
|
11734
|
+
lines.push("", options.all ? "Use `mmi-cli explain <group|command>` for a focused map." : "Use `mmi-cli explain <group|command>` for detail or `mmi-cli commands --all` for operational depth.");
|
|
11917
11735
|
return lines.join("\n");
|
|
11918
11736
|
}
|
|
11919
11737
|
|
|
11738
|
+
// src/command-consolidation.ts
|
|
11739
|
+
function child(parent, name) {
|
|
11740
|
+
const found = parent.commands.find((command) => command.name() === name);
|
|
11741
|
+
if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
|
|
11742
|
+
return found;
|
|
11743
|
+
}
|
|
11744
|
+
function detach(parent, command) {
|
|
11745
|
+
const commands = parent.commands;
|
|
11746
|
+
const index = commands.indexOf(command);
|
|
11747
|
+
if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
|
|
11748
|
+
commands.splice(index, 1);
|
|
11749
|
+
return command;
|
|
11750
|
+
}
|
|
11751
|
+
function move(parent, destination, name, nextName = name) {
|
|
11752
|
+
const command = detach(parent, child(parent, name));
|
|
11753
|
+
command.name(nextName);
|
|
11754
|
+
destination.addCommand(command);
|
|
11755
|
+
return command;
|
|
11756
|
+
}
|
|
11757
|
+
function remove(parent, name) {
|
|
11758
|
+
detach(parent, child(parent, name));
|
|
11759
|
+
}
|
|
11760
|
+
function consolidateCommandNamespaces(program3) {
|
|
11761
|
+
const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
|
|
11762
|
+
const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
|
|
11763
|
+
const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
|
|
11764
|
+
move(program3, org, "project");
|
|
11765
|
+
move(program3, org, "oauth");
|
|
11766
|
+
move(program3, org, "access");
|
|
11767
|
+
move(program3, org, "schedules");
|
|
11768
|
+
const config = org.command("config").description("organization configuration");
|
|
11769
|
+
const registry2 = child(program3, "registry");
|
|
11770
|
+
move(registry2, config, "org", "get");
|
|
11771
|
+
remove(program3, "registry");
|
|
11772
|
+
const orgRules = org.command("rules").description("organization-managed repository rules");
|
|
11773
|
+
const rules2 = child(program3, "rules");
|
|
11774
|
+
move(rules2, orgRules, "gitignore");
|
|
11775
|
+
remove(program3, "rules");
|
|
11776
|
+
move(program3, runtime, "tenant");
|
|
11777
|
+
const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
|
|
11778
|
+
const deploy = child(program3, "deploy");
|
|
11779
|
+
move(deploy, runtimeDeploy, "status");
|
|
11780
|
+
remove(program3, "deploy");
|
|
11781
|
+
move(program3, runtime, "box");
|
|
11782
|
+
move(program3, runtime, "edge");
|
|
11783
|
+
move(program3, plugin, "guard");
|
|
11784
|
+
move(program3, plugin, "plugin-heal", "heal");
|
|
11785
|
+
move(program3, plugin, "plugin-prune", "prune");
|
|
11786
|
+
move(program3, plugin, "session-start");
|
|
11787
|
+
const docs2 = child(program3, "docs");
|
|
11788
|
+
move(program3, docs2, "docs-audit", "audit");
|
|
11789
|
+
const train = child(program3, "train");
|
|
11790
|
+
const fullTrack2 = child(program3, "full-track");
|
|
11791
|
+
move(fullTrack2, train, "readiness");
|
|
11792
|
+
remove(program3, "full-track");
|
|
11793
|
+
const worktree2 = child(program3, "worktree");
|
|
11794
|
+
move(program3, worktree2, "gc");
|
|
11795
|
+
const stage = child(program3, "stage");
|
|
11796
|
+
move(program3, stage, "port-range");
|
|
11797
|
+
}
|
|
11798
|
+
|
|
11920
11799
|
// src/version-lag.ts
|
|
11921
11800
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
11922
11801
|
var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
|
|
@@ -12449,6 +12328,14 @@ function parseVerifySecrets(stdout) {
|
|
|
12449
12328
|
}
|
|
12450
12329
|
return out;
|
|
12451
12330
|
}
|
|
12331
|
+
function parseVerifyBroker(stdout) {
|
|
12332
|
+
const out = [];
|
|
12333
|
+
for (const line of stdout.split("\n")) {
|
|
12334
|
+
const match = /^(\S+):\s*(reachable|missing|denied|error)\b/.exec(line.trim());
|
|
12335
|
+
if (match) out.push({ key: match[1], status: match[2] });
|
|
12336
|
+
}
|
|
12337
|
+
return out;
|
|
12338
|
+
}
|
|
12452
12339
|
|
|
12453
12340
|
// src/train-apply.ts
|
|
12454
12341
|
function resolveDeployModel2(meta, repo) {
|
|
@@ -12732,7 +12619,7 @@ function classifyProjectGetFailure(text) {
|
|
|
12732
12619
|
}
|
|
12733
12620
|
async function loadProjectMeta(deps, ctx) {
|
|
12734
12621
|
try {
|
|
12735
|
-
const out = await deps.runSelf(["project", "get", ctx.repo]);
|
|
12622
|
+
const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
|
|
12736
12623
|
const parsed = JSON.parse(out);
|
|
12737
12624
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12738
12625
|
return { status: "read-error", error: "invalid registry META JSON" };
|
|
@@ -12851,6 +12738,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
|
|
|
12851
12738
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
12852
12739
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
12853
12740
|
}
|
|
12741
|
+
function correlateReconcileRun(deps, since, titleIncludes) {
|
|
12742
|
+
return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
|
|
12743
|
+
}
|
|
12854
12744
|
async function correlateWorkflowRun(deps, args) {
|
|
12855
12745
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
12856
12746
|
}
|
|
@@ -13906,8 +13796,36 @@ async function runTenantRedeploy(deps, options) {
|
|
|
13906
13796
|
const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
|
|
13907
13797
|
return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
13908
13798
|
}
|
|
13799
|
+
async function runTenantReconcile(deps, options) {
|
|
13800
|
+
const parts = options.repo.split("/");
|
|
13801
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`repo must be owner/name, got ${options.repo}`);
|
|
13802
|
+
if (!["dev", "rc", "main"].includes(options.stage)) throw new Error(`stage must be dev, rc, or main, got ${options.stage}`);
|
|
13803
|
+
if (!deps.dispatchTenantReconcile) throw new Error("tenant reconcile dispatch is unavailable");
|
|
13804
|
+
const stage = options.stage;
|
|
13805
|
+
const base = { command: "tenant-reconcile", repo: options.repo, stage };
|
|
13806
|
+
const since = (deps.now ?? Date.now)();
|
|
13807
|
+
const dispatched = await deps.dispatchTenantReconcile({ repo: options.repo, stage });
|
|
13808
|
+
if (!dispatched.ok) {
|
|
13809
|
+
return {
|
|
13810
|
+
...base,
|
|
13811
|
+
dispatched: false,
|
|
13812
|
+
conclusion: "failure",
|
|
13813
|
+
note: `tenant reconcile rejected: ${dispatched.error ?? "request rejected by the Hub"}`
|
|
13814
|
+
};
|
|
13815
|
+
}
|
|
13816
|
+
const slug = parts[1].toLowerCase();
|
|
13817
|
+
const run = await correlateReconcileRun(deps, since, [slug, stage]);
|
|
13818
|
+
const conclusion = options.watch ?? true ? await watchTenantRun(deps, run.runId) : "pending";
|
|
13819
|
+
return {
|
|
13820
|
+
...base,
|
|
13821
|
+
dispatched: true,
|
|
13822
|
+
...run,
|
|
13823
|
+
conclusion,
|
|
13824
|
+
note: conclusion === "success" ? "tenant-reconcile run succeeded" : conclusion === "failure" ? "tenant-reconcile run failed \u2014 inspect the run before redeploying" : "tenant-reconcile dispatched but not confirmed \u2014 do not redeploy until the run succeeds"
|
|
13825
|
+
};
|
|
13826
|
+
}
|
|
13909
13827
|
function tenantControlWatches(action) {
|
|
13910
|
-
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
13828
|
+
return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
|
|
13911
13829
|
}
|
|
13912
13830
|
async function runTenantControl(deps, options) {
|
|
13913
13831
|
const { repo, stage, action } = options;
|
|
@@ -13931,13 +13849,16 @@ async function runTenantControl(deps, options) {
|
|
|
13931
13849
|
if (action === "retire") {
|
|
13932
13850
|
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
13933
13851
|
}
|
|
13934
|
-
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
|
|
13852
|
+
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
|
|
13935
13853
|
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
13936
13854
|
if (action === "status") {
|
|
13937
13855
|
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
13938
|
-
} else {
|
|
13856
|
+
} else if (action === "verify-secrets") {
|
|
13939
13857
|
result.secrets = parseVerifySecrets(output);
|
|
13940
13858
|
result.secretsRaw = output;
|
|
13859
|
+
} else {
|
|
13860
|
+
result.broker = parseVerifyBroker(output);
|
|
13861
|
+
result.brokerRaw = output;
|
|
13941
13862
|
}
|
|
13942
13863
|
}
|
|
13943
13864
|
result.note = conclusion === "success" ? `tenant-control ${action} run succeeded` : conclusion === "failure" ? `tenant-control ${action} run failed \u2014 inspect the run` : runId == null ? `dispatched tenant-control.yml (${action}) \u2014 run not correlated; check the Actions tab` : `dispatched tenant-control.yml (${action}) \u2014 not watched`;
|
|
@@ -13987,6 +13908,26 @@ function renderVerifySecrets(body) {
|
|
|
13987
13908
|
return { lines, failure: null };
|
|
13988
13909
|
}
|
|
13989
13910
|
|
|
13911
|
+
// src/tenant-verify-broker.ts
|
|
13912
|
+
function renderVerifyBroker(input) {
|
|
13913
|
+
if (input.broker.length === 0) {
|
|
13914
|
+
const raw = input.raw?.trim();
|
|
13915
|
+
if (raw?.startsWith("no broker-readable runtime secrets declared")) return { lines: [raw], failure: null };
|
|
13916
|
+
return {
|
|
13917
|
+
lines: raw ? raw.split("\n") : ["verify-broker: no per-key verdicts returned"],
|
|
13918
|
+
failure: "verify-broker returned no per-key verdicts, so broker access was not verified"
|
|
13919
|
+
};
|
|
13920
|
+
}
|
|
13921
|
+
const lines = input.broker.map((item) => `${item.key}: ${item.status}`);
|
|
13922
|
+
const reachable = input.broker.filter((item) => item.status === "reachable").length;
|
|
13923
|
+
const bad = input.broker.length - reachable;
|
|
13924
|
+
lines.push(`verify-broker: ${reachable} reachable, ${bad} failed`);
|
|
13925
|
+
return {
|
|
13926
|
+
lines,
|
|
13927
|
+
failure: bad > 0 ? `${bad} of ${input.broker.length} broker read(s) failed; redeploy refreshes an expired runtime token` : null
|
|
13928
|
+
};
|
|
13929
|
+
}
|
|
13930
|
+
|
|
13990
13931
|
// src/hotfix-coverage.ts
|
|
13991
13932
|
var import_node_child_process8 = require("node:child_process");
|
|
13992
13933
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
@@ -15009,14 +14950,10 @@ function loadAccessTargets(projectsJson) {
|
|
|
15009
14950
|
const seen = /* @__PURE__ */ new Set();
|
|
15010
14951
|
const targets = [];
|
|
15011
14952
|
for (const project2 of projects) {
|
|
15012
|
-
const embeddedContent = new Set(
|
|
15013
|
-
(project2.fanoutTargets ?? []).filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase())
|
|
15014
|
-
);
|
|
15015
14953
|
for (const repo of project2.repos ?? []) {
|
|
15016
14954
|
if (seen.has(repo)) continue;
|
|
15017
14955
|
seen.add(repo);
|
|
15018
|
-
const
|
|
15019
|
-
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
|
|
14956
|
+
const cls = project2.class === "content" || project2.deployModel === "content" ? "content" : "deployable";
|
|
15020
14957
|
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
15021
14958
|
targets.push({ repo, class: cls, releaseTrack });
|
|
15022
14959
|
}
|
|
@@ -15096,9 +15033,13 @@ function renderAccessReport(report) {
|
|
|
15096
15033
|
var import_node_fs16 = require("node:fs");
|
|
15097
15034
|
var import_node_path14 = require("node:path");
|
|
15098
15035
|
var DOCS_INDEX_PATH = "docs/index.md";
|
|
15036
|
+
function isRoutableDocsPath(relPath) {
|
|
15037
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
15038
|
+
return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
|
|
15039
|
+
}
|
|
15099
15040
|
var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
|
|
15100
|
-
function
|
|
15101
|
-
return text.replace(/\r?\n/g, " ").replace(
|
|
15041
|
+
function plainInline(text) {
|
|
15042
|
+
return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
|
|
15102
15043
|
}
|
|
15103
15044
|
function extractDocMeta(relPath, raw) {
|
|
15104
15045
|
const lines = raw.split(/\r?\n/);
|
|
@@ -15126,7 +15067,7 @@ function extractDocMeta(relPath, raw) {
|
|
|
15126
15067
|
break;
|
|
15127
15068
|
}
|
|
15128
15069
|
if (trimmed.startsWith("#")) continue;
|
|
15129
|
-
scope =
|
|
15070
|
+
scope = plainInline(trimmed);
|
|
15130
15071
|
break;
|
|
15131
15072
|
}
|
|
15132
15073
|
if (!title) {
|
|
@@ -15138,16 +15079,16 @@ function extractDocMeta(relPath, raw) {
|
|
|
15138
15079
|
function renderDocsIndex(entries) {
|
|
15139
15080
|
const lines = [
|
|
15140
15081
|
GENERATED_HEADER,
|
|
15082
|
+
"",
|
|
15141
15083
|
"# Documentation index",
|
|
15142
15084
|
"",
|
|
15143
15085
|
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
15144
15086
|
"can never diverge from the records it lists.",
|
|
15145
|
-
""
|
|
15146
|
-
"| Document | Scope |",
|
|
15147
|
-
"| --- | --- |"
|
|
15087
|
+
""
|
|
15148
15088
|
];
|
|
15149
15089
|
for (const entry of entries) {
|
|
15150
|
-
|
|
15090
|
+
const link = `[${plainInline(entry.title)}](${entry.path})`;
|
|
15091
|
+
lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
|
|
15151
15092
|
}
|
|
15152
15093
|
return lines.join("\n") + "\n";
|
|
15153
15094
|
}
|
|
@@ -15183,7 +15124,7 @@ function createDocsIndexDeps(repoRoot2) {
|
|
|
15183
15124
|
const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
|
|
15184
15125
|
const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
15185
15126
|
return {
|
|
15186
|
-
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(
|
|
15127
|
+
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
15187
15128
|
readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
|
|
15188
15129
|
readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
|
|
15189
15130
|
writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
|
|
@@ -15290,427 +15231,6 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
15290
15231
|
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
15291
15232
|
}
|
|
15292
15233
|
|
|
15293
|
-
// src/project-readiness.ts
|
|
15294
|
-
function stagesForTrack(meta) {
|
|
15295
|
-
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
15296
|
-
}
|
|
15297
|
-
function stageInTrack(meta, stage) {
|
|
15298
|
-
return stagesForTrack(meta).includes(stage);
|
|
15299
|
-
}
|
|
15300
|
-
function dnsErrorToResolution(code) {
|
|
15301
|
-
return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
|
|
15302
|
-
}
|
|
15303
|
-
var STAGES = ["dev", "rc", "main"];
|
|
15304
|
-
var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
15305
|
-
function boardRegistryGaps(meta) {
|
|
15306
|
-
if (!meta?.projectId) return [];
|
|
15307
|
-
if (meta.projectNumber != null) return [];
|
|
15308
|
-
return ["projectNumber"];
|
|
15309
|
-
}
|
|
15310
|
-
function previewRegistryMetaMerge(existing, patch) {
|
|
15311
|
-
const out = { ...existing ?? {} };
|
|
15312
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
15313
|
-
if (value === null) delete out[key];
|
|
15314
|
-
else out[key] = value;
|
|
15315
|
-
}
|
|
15316
|
-
return out;
|
|
15317
|
-
}
|
|
15318
|
-
function boardLinkWriteError(patch, existing) {
|
|
15319
|
-
const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
|
|
15320
|
-
const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
|
|
15321
|
-
if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
|
|
15322
|
-
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15323
|
-
}
|
|
15324
|
-
if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
|
|
15325
|
-
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15326
|
-
}
|
|
15327
|
-
return null;
|
|
15328
|
-
}
|
|
15329
|
-
function boardRegistryGapMessage(repo) {
|
|
15330
|
-
return `Board META incomplete for ${repo}: registry has projectId but no projectNumber \u2014 board claim and auto-add will fail until projectNumber is backfilled (re-run \`node infra/migrate/seed-registry.mjs\` or \`mmi-cli bootstrap apply --execute\` with board vars)`;
|
|
15331
|
-
}
|
|
15332
|
-
var slugOfRepo = slugOf;
|
|
15333
|
-
function repoFrom(repoOrSlug, slug) {
|
|
15334
|
-
return repoOrSlug.includes("/") ? repoOrSlug : `mutmutco/${slug}`;
|
|
15335
|
-
}
|
|
15336
|
-
function defaultSubdomain(slug) {
|
|
15337
|
-
return slug.replace(/^[a-z]+-/, "");
|
|
15338
|
-
}
|
|
15339
|
-
function projectRequiresGoogleOAuth(meta, model) {
|
|
15340
|
-
if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
|
|
15341
|
-
const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
|
|
15342
|
-
if (projectType !== "web-app") return false;
|
|
15343
|
-
return Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
15344
|
-
}
|
|
15345
|
-
function declaresNoPublicEdge(meta) {
|
|
15346
|
-
const ed = meta?.edgeDomains;
|
|
15347
|
-
return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
|
|
15348
|
-
}
|
|
15349
|
-
function declaresWorker(meta) {
|
|
15350
|
-
return meta?.projectType === "worker";
|
|
15351
|
-
}
|
|
15352
|
-
function isCentralContainerModel(model) {
|
|
15353
|
-
return model === "tenant-container" || model === "solo-container";
|
|
15354
|
-
}
|
|
15355
|
-
function isNoEdgeTenantWorker(meta, model) {
|
|
15356
|
-
return isCentralContainerModel(model) && (declaresNoPublicEdge(meta) || declaresWorker(meta));
|
|
15357
|
-
}
|
|
15358
|
-
function projectRequiresDeployCoords(model, stage, meta) {
|
|
15359
|
-
if (!isCentralContainerModel(model)) return false;
|
|
15360
|
-
if (stage && isNoEdgeTenantWorker(meta, model)) return stage === "main";
|
|
15361
|
-
return true;
|
|
15362
|
-
}
|
|
15363
|
-
function projectRequiresDeployState(model, stage) {
|
|
15364
|
-
return model === "hub-serverless" && stage !== "dev";
|
|
15365
|
-
}
|
|
15366
|
-
function stageRequiredSecrets(stage, meta) {
|
|
15367
|
-
const contract = meta.requiredRuntimeSecrets;
|
|
15368
|
-
const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage]) ? contract[stage] ?? [] : [];
|
|
15369
|
-
const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
|
|
15370
|
-
if (isNoEdgeTenantWorker(meta, model) && stage !== "main") return [];
|
|
15371
|
-
const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES2 : [];
|
|
15372
|
-
return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
|
|
15373
|
-
}
|
|
15374
|
-
function stageKey2(stage, key) {
|
|
15375
|
-
return key.includes("/") ? key : `${stage}/${key}`;
|
|
15376
|
-
}
|
|
15377
|
-
function materializedRuntimeSecretName(entry) {
|
|
15378
|
-
return entry.includes(":") ? entry.split(":").pop() : entry;
|
|
15379
|
-
}
|
|
15380
|
-
function runtimeSecretStreamGap(stage, meta, presentSecrets) {
|
|
15381
|
-
const streamed = new Set(
|
|
15382
|
-
(contractByStage(meta.requiredRuntimeSecrets)[stage] ?? []).map(materializedRuntimeSecretName)
|
|
15383
|
-
);
|
|
15384
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15385
|
-
const gap = [];
|
|
15386
|
-
for (const name of stageRequiredSecrets(stage, meta).map(materializedRuntimeSecretName)) {
|
|
15387
|
-
if (seen.has(name)) continue;
|
|
15388
|
-
seen.add(name);
|
|
15389
|
-
if (!streamed.has(name) && (presentSecrets.has(stageKey2(stage, name)) || presentSecrets.has(name))) gap.push(name);
|
|
15390
|
-
}
|
|
15391
|
-
return gap;
|
|
15392
|
-
}
|
|
15393
|
-
function hasRuntimeSecretContract(contract) {
|
|
15394
|
-
if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
|
|
15395
|
-
return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
|
|
15396
|
-
}
|
|
15397
|
-
function appAttestationOf(meta) {
|
|
15398
|
-
const a = meta?.appAttested;
|
|
15399
|
-
if (!a || typeof a !== "object" || Array.isArray(a)) return null;
|
|
15400
|
-
const { at, by } = a;
|
|
15401
|
-
return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
|
|
15402
|
-
}
|
|
15403
|
-
function attestedLine(att) {
|
|
15404
|
-
return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli org project attest\` after app-owned structural changes.`;
|
|
15405
|
-
}
|
|
15406
|
-
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: [] }).";
|
|
15407
|
-
function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
|
|
15408
|
-
const attested = appAttestationOf(meta);
|
|
15409
|
-
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";
|
|
15410
|
-
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
15411
|
-
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
15412
|
-
if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
15413
|
-
if (projectType === "desktop-app") {
|
|
15414
|
-
return [
|
|
15415
|
-
"Desktop app: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default; keep app-owned installer/packaging outside the tenant train.",
|
|
15416
|
-
"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."
|
|
15417
|
-
];
|
|
15418
|
-
}
|
|
15419
|
-
if (projectType === "desktop-game") {
|
|
15420
|
-
return [
|
|
15421
|
-
"Desktop game: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default; keep app-owned release packaging outside the tenant train.",
|
|
15422
|
-
"Keep README.md and architecture.md explicit about the game build/release path so readiness does not guess web infra."
|
|
15423
|
-
];
|
|
15424
|
-
}
|
|
15425
|
-
if (projectType === "non-deployable" || model === "none") {
|
|
15426
|
-
return [
|
|
15427
|
-
"Non-deployable repo: no Hub deploy coords, Google OAuth, DWD, or runtime tenant control by default.",
|
|
15428
|
-
"Keep README.md and architecture.md explicit about the repo purpose and any project-admin workflow it still needs."
|
|
15429
|
-
];
|
|
15430
|
-
}
|
|
15431
|
-
if (model === "hub-serverless") {
|
|
15432
|
-
return [
|
|
15433
|
-
"Hub serverless deploy stays on the Hub-owned workflows; deploy.yml stamps per-stage deploy state onto state-only DEPLOY# rows (no tenant coords).",
|
|
15434
|
-
"Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults."
|
|
15435
|
-
];
|
|
15436
|
-
}
|
|
15437
|
-
if (model === "serverless") {
|
|
15438
|
-
return [
|
|
15439
|
-
"Serverless deploys use the central serverless path; do not add tenant compose or box plumbing.",
|
|
15440
|
-
"Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
15441
|
-
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
15442
|
-
];
|
|
15443
|
-
}
|
|
15444
|
-
if (projectType === "cli-tool" || model === "registry-publish") {
|
|
15445
|
-
return [
|
|
15446
|
-
"Distributable CLI/plugin: ship a publishable package.json (name, version, bin/exports) \u2014 release publishes to npm (plugin-marketplace publish is future work when a plugin tenant lands), not a box deploy; no Hub deploy coords, edge, or OAuth.",
|
|
15447
|
-
"Keep the published version in lockstep with the release tag; make the publish idempotent (skip when the version is already on the registry).",
|
|
15448
|
-
"Keep app-owned README.md and architecture.md aligned with the registry-publish release path."
|
|
15449
|
-
];
|
|
15450
|
-
}
|
|
15451
|
-
const gaps = [
|
|
15452
|
-
'Ensure the compose is fileless (#2662) \u2014 drop env_file: and mark each runtime-secret service with the mmi.runtime-env: "true" label; the box injects coords + declared runtime secrets into the container process env at deploy (no .env on disk); the app reads plain environment variables and never self-loads SSM.',
|
|
15453
|
-
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
15454
|
-
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
15455
|
-
];
|
|
15456
|
-
if (isNoEdgeTenantWorker(meta, model)) {
|
|
15457
|
-
gaps.unshift("No public edge (worker/outbound-only tenant \u2014 `projectType: worker` or `edgeDomains: {}`): skip Cloudflare edge auto-heal, OAuth defaults, and dev/rc remote deploy coords unless META explicitly adds them.");
|
|
15458
|
-
}
|
|
15459
|
-
if (contractUndeclared) {
|
|
15460
|
-
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
15461
|
-
}
|
|
15462
|
-
if (slug === "mmi-katip") {
|
|
15463
|
-
gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
15464
|
-
}
|
|
15465
|
-
if (model === "solo-container" || model === "tenant-container") {
|
|
15466
|
-
if (typeof mainDeployFact?.port === "number") {
|
|
15467
|
-
gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${mainDeployFact.port}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202; port read from DEPLOY#main).`);
|
|
15468
|
-
} else if (!appAttestationOf(meta)) {
|
|
15469
|
-
gaps.push("Seed DEPLOY#main healthUrl from the DEPLOY#main edgeVhost.port during bootstrap (tenant reconcile/control health gate \u2014 #1202); doctor has no committed DEPLOY port to cite yet.");
|
|
15470
|
-
}
|
|
15471
|
-
}
|
|
15472
|
-
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
15473
|
-
return gaps;
|
|
15474
|
-
}
|
|
15475
|
-
function edgeDomainsByStage(meta) {
|
|
15476
|
-
const ed = meta?.edgeDomains;
|
|
15477
|
-
if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
|
|
15478
|
-
const out = {};
|
|
15479
|
-
for (const stage of STAGES) {
|
|
15480
|
-
const v = ed[stage];
|
|
15481
|
-
if (typeof v === "string" && v.trim()) out[stage] = v.trim();
|
|
15482
|
-
}
|
|
15483
|
-
return out;
|
|
15484
|
-
}
|
|
15485
|
-
async function probeEdgeDomains(meta, resolveDns) {
|
|
15486
|
-
const byStage = edgeDomainsByStage(meta);
|
|
15487
|
-
const results = await Promise.all(
|
|
15488
|
-
Object.entries(byStage).map(async ([stage, host]) => {
|
|
15489
|
-
let resolved;
|
|
15490
|
-
try {
|
|
15491
|
-
resolved = await resolveDns(host);
|
|
15492
|
-
} catch {
|
|
15493
|
-
resolved = void 0;
|
|
15494
|
-
}
|
|
15495
|
-
return resolved === false ? { stage, host } : void 0;
|
|
15496
|
-
})
|
|
15497
|
-
);
|
|
15498
|
-
return results.filter((r) => r !== void 0);
|
|
15499
|
-
}
|
|
15500
|
-
function contractByStage(contract) {
|
|
15501
|
-
return contract && !Array.isArray(contract) ? contract : {};
|
|
15502
|
-
}
|
|
15503
|
-
function ensureStageNames(names, required) {
|
|
15504
|
-
return [.../* @__PURE__ */ new Set([...names ?? [], ...required])];
|
|
15505
|
-
}
|
|
15506
|
-
function sameNames(a, b) {
|
|
15507
|
-
const left = new Set(a ?? []);
|
|
15508
|
-
const right = new Set(b ?? []);
|
|
15509
|
-
if (left.size !== right.size) return false;
|
|
15510
|
-
return [...left].every((name) => right.has(name));
|
|
15511
|
-
}
|
|
15512
|
-
function sameStageContract(a, b) {
|
|
15513
|
-
return STAGES.every((stage) => sameNames(a[stage], b[stage]));
|
|
15514
|
-
}
|
|
15515
|
-
function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
|
|
15516
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15517
|
-
const repo = repoFrom(repoOrSlug, slug);
|
|
15518
|
-
const sub = defaultSubdomain(slug);
|
|
15519
|
-
const patch = {};
|
|
15520
|
-
const confidentType = resolveProjectTypeConfident(meta, repo);
|
|
15521
|
-
const projectType = confidentType ?? resolveProjectType(meta, repo);
|
|
15522
|
-
const model = resolveDeployModel(meta, repo);
|
|
15523
|
-
if (confidentType && !meta?.class) patch.class = confidentType === "content" ? "content" : "deployable";
|
|
15524
|
-
if (confidentType) {
|
|
15525
|
-
if (!meta?.projectType) patch.projectType = confidentType;
|
|
15526
|
-
if (!meta?.deployModel) patch.deployModel = model;
|
|
15527
|
-
}
|
|
15528
|
-
if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
|
|
15529
|
-
if (confidentType && !meta?.edgeDomains && model === "tenant-container" && projectType !== "worker") {
|
|
15530
|
-
patch.edgeDomains = {
|
|
15531
|
-
dev: `dev.${sub}.mutatismutandis.co`,
|
|
15532
|
-
rc: `rc.${sub}.mutatismutandis.co`,
|
|
15533
|
-
main: `${sub}.mutatismutandis.co`
|
|
15534
|
-
};
|
|
15535
|
-
}
|
|
15536
|
-
if (meta && projectTypeClearsWebProfile(projectType, model)) {
|
|
15537
|
-
if (meta.oauth !== void 0) patch.oauth = null;
|
|
15538
|
-
if (meta.edgeDomains !== void 0) patch.edgeDomains = null;
|
|
15539
|
-
}
|
|
15540
|
-
if (slug === "mmi-katip") {
|
|
15541
|
-
const required = ["GOOGLE_IMPERSONATE_USER", "GOOGLE_ADMIN_USER"];
|
|
15542
|
-
const current = contractByStage(meta?.requiredRuntimeSecrets);
|
|
15543
|
-
const next = {
|
|
15544
|
-
dev: ensureStageNames(current.dev, required),
|
|
15545
|
-
rc: ensureStageNames(current.rc, required),
|
|
15546
|
-
main: ensureStageNames(current.main, required)
|
|
15547
|
-
};
|
|
15548
|
-
if (!sameStageContract(current, next)) {
|
|
15549
|
-
patch.requiredRuntimeSecrets = next;
|
|
15550
|
-
}
|
|
15551
|
-
}
|
|
15552
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli org 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).`];
|
|
15553
|
-
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
15554
|
-
return { slug, patch, appOwnedGaps };
|
|
15555
|
-
}
|
|
15556
|
-
async function runV2Heal(repoOrSlug, opts, deps) {
|
|
15557
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15558
|
-
const read = await deps.fetchProject(slug);
|
|
15559
|
-
if (!read.ok) {
|
|
15560
|
-
return { status: "aborted", slug, error: `registry read failed for ${slug} (${read.error}) \u2014 aborting; a failed read must not be treated as a missing project` };
|
|
15561
|
-
}
|
|
15562
|
-
const plan = buildV2HealPatch(repoOrSlug, read.project);
|
|
15563
|
-
if (!opts.apply) return { status: "planned", slug, patch: plan.patch, appOwnedGaps: plan.appOwnedGaps };
|
|
15564
|
-
if (!Object.keys(plan.patch).length) return { status: "noop", slug, appOwnedGaps: plan.appOwnedGaps };
|
|
15565
|
-
const write = await deps.upsertProject(slug, plan.patch);
|
|
15566
|
-
if (!write.ok) return { status: "write-failed", slug, write };
|
|
15567
|
-
return { status: "applied", slug, applied: Object.keys(plan.patch), appOwnedGaps: plan.appOwnedGaps, result: write.body };
|
|
15568
|
-
}
|
|
15569
|
-
async function buildV2Doctor(repoOrSlug, deps) {
|
|
15570
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15571
|
-
const repo = repoFrom(repoOrSlug, slug);
|
|
15572
|
-
const read = await deps.getProject(slug);
|
|
15573
|
-
if (!read.ok) {
|
|
15574
|
-
const degradedSecrets = {
|
|
15575
|
-
dev: { required: [], present: [], missing: [] },
|
|
15576
|
-
rc: { required: [], present: [], missing: [] },
|
|
15577
|
-
main: { required: [], present: [], missing: [] }
|
|
15578
|
-
};
|
|
15579
|
-
const degradedStage = {
|
|
15580
|
-
dev: { ok: false, required: false },
|
|
15581
|
-
rc: { ok: false, required: false },
|
|
15582
|
-
main: { ok: false, required: false }
|
|
15583
|
-
};
|
|
15584
|
-
return {
|
|
15585
|
-
ok: false,
|
|
15586
|
-
repo,
|
|
15587
|
-
slug,
|
|
15588
|
-
registryError: read.error,
|
|
15589
|
-
hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
|
|
15590
|
-
autoHealAvailable: [],
|
|
15591
|
-
appOwnedGaps: [`Hub registry read failed (${read.error}) \u2014 diagnosis degraded; nothing is known about META, coords, or gaps. Likely transient (cold start, network, or auth blip): retry \`mmi-cli org project doctor\` shortly.`]
|
|
15592
|
-
};
|
|
15593
|
-
}
|
|
15594
|
-
const meta = read.project;
|
|
15595
|
-
const projectType = resolveProjectType(meta, repo);
|
|
15596
|
-
const model = resolveDeployModel(meta, repo);
|
|
15597
|
-
const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
|
|
15598
|
-
const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
|
|
15599
|
-
if (!meta) {
|
|
15600
|
-
const emptySecrets = {
|
|
15601
|
-
dev: { required: [], present: [], missing: [] },
|
|
15602
|
-
rc: { required: [], present: [], missing: [] },
|
|
15603
|
-
main: { required: [], present: [], missing: [] }
|
|
15604
|
-
};
|
|
15605
|
-
const emptyCoords = {
|
|
15606
|
-
dev: { ok: false, required: true },
|
|
15607
|
-
rc: { ok: false, required: true },
|
|
15608
|
-
main: { ok: false, required: true }
|
|
15609
|
-
};
|
|
15610
|
-
const emptyState = Object.fromEntries(STAGES.map((stage) => {
|
|
15611
|
-
const required = projectRequiresDeployState(model, stage);
|
|
15612
|
-
return [stage, { required, ok: !required }];
|
|
15613
|
-
}));
|
|
15614
|
-
return {
|
|
15615
|
-
ok: false,
|
|
15616
|
-
repo,
|
|
15617
|
-
slug,
|
|
15618
|
-
hubOwned: { meta: { ok: false, missing: [`PROJECT#${slug}/META`] }, deployCoords: emptyCoords, deployState: emptyState, secrets: emptySecrets },
|
|
15619
|
-
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
15620
|
-
appOwnedGaps: autoHeal.appOwnedGaps
|
|
15621
|
-
};
|
|
15622
|
-
}
|
|
15623
|
-
let presentSecrets = /* @__PURE__ */ new Set();
|
|
15624
|
-
let secretsError;
|
|
15625
|
-
try {
|
|
15626
|
-
presentSecrets = new Set(await deps.listSecrets(repo));
|
|
15627
|
-
} catch (e) {
|
|
15628
|
-
secretsError = e?.message || "secrets list failed";
|
|
15629
|
-
}
|
|
15630
|
-
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
|
|
15631
|
-
const required = stageInTrack(meta, stage) && projectRequiresDeployCoords(model, stage, meta);
|
|
15632
|
-
return [stage, { required, ok: required ? await deps.hasDeployCoords(slug, stage) : true }];
|
|
15633
|
-
})));
|
|
15634
|
-
const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
|
|
15635
|
-
const required = stageInTrack(meta, stage) && projectRequiresDeployState(model, stage);
|
|
15636
|
-
return [stage, { required, ok: required ? await deps.hasDeployState(slug, stage) : true }];
|
|
15637
|
-
})));
|
|
15638
|
-
const secrets = Object.fromEntries(STAGES.map((stage) => {
|
|
15639
|
-
const names = stageInTrack(meta, stage) ? stageRequiredSecrets(stage, meta) : [];
|
|
15640
|
-
const satisfied = (key) => presentSecrets.has(stageKey2(stage, key)) || !key.includes("/") && presentSecrets.has(key);
|
|
15641
|
-
const required = names.map((key) => stageKey2(stage, key));
|
|
15642
|
-
const present = names.filter(satisfied).map((key) => stageKey2(stage, key));
|
|
15643
|
-
const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey2(stage, key));
|
|
15644
|
-
return [stage, { required, present, missing }];
|
|
15645
|
-
}));
|
|
15646
|
-
const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage) => [
|
|
15647
|
-
stage,
|
|
15648
|
-
stageInTrack(meta, stage) ? runtimeSecretStreamGap(stage, meta, presentSecrets) : []
|
|
15649
|
-
]));
|
|
15650
|
-
const runtimeSecretStreamWarningRows = STAGES.map((stage) => ({ stage, names: runtimeSecretStreamWarnings[stage] })).filter((row) => row.names.length > 0);
|
|
15651
|
-
const metaMissing = ["class", "projectType", "deployModel", "vaultPath"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
|
|
15652
|
-
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
|
|
15653
|
-
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
15654
|
-
return {
|
|
15655
|
-
ok,
|
|
15656
|
-
repo,
|
|
15657
|
-
slug,
|
|
15658
|
-
class: meta.class,
|
|
15659
|
-
projectType,
|
|
15660
|
-
deployModel: model,
|
|
15661
|
-
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
|
|
15662
|
-
secretsError,
|
|
15663
|
-
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
15664
|
-
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
15665
|
-
...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
|
|
15666
|
-
...runtimeSecretStreamWarningRows.length ? { runtimeSecretStreamWarnings: runtimeSecretStreamWarningRows } : {},
|
|
15667
|
-
appAttested: appAttestationOf(meta) ?? void 0
|
|
15668
|
-
};
|
|
15669
|
-
}
|
|
15670
|
-
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
15671
|
-
const start = "<!-- mmi-v2-readiness:start -->";
|
|
15672
|
-
const end = "<!-- mmi-v2-readiness:end -->";
|
|
15673
|
-
const stageLines = STAGES.map((stage) => {
|
|
15674
|
-
const coordState = report.hubOwned.deployCoords[stage];
|
|
15675
|
-
const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
|
|
15676
|
-
const state = report.hubOwned.deployState[stage];
|
|
15677
|
-
const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
|
|
15678
|
-
const missing = report.hubOwned.secrets[stage].missing;
|
|
15679
|
-
return `- ${stage}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
|
|
15680
|
-
});
|
|
15681
|
-
const section = [
|
|
15682
|
-
start,
|
|
15683
|
-
"## Hub v2 readiness",
|
|
15684
|
-
"",
|
|
15685
|
-
`Repo: ${report.repo}`,
|
|
15686
|
-
`Project type: ${report.projectType ?? "(unresolved)"}`,
|
|
15687
|
-
`Deploy model: ${report.deployModel ?? "(unresolved)"}`,
|
|
15688
|
-
`Overall: ${report.ok ? "ready" : "not ready"}`,
|
|
15689
|
-
"",
|
|
15690
|
-
"### Hub-owned diagnosis",
|
|
15691
|
-
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
15692
|
-
...stageLines,
|
|
15693
|
-
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
15694
|
-
...(report.edgeDomainWarnings ?? []).map(
|
|
15695
|
-
(w) => `- \u26A0 edge domain does not resolve in DNS (advisory): ${w.stage} \u2192 ${w.host}; verify the registry edgeDomains value against the live public host`
|
|
15696
|
-
),
|
|
15697
|
-
...(report.runtimeSecretStreamWarnings ?? []).map(
|
|
15698
|
-
(w) => `- \u26A0 required secrets provisioned but not in requiredRuntimeSecrets (advisory): ${w.stage} \u2192 ${w.names.join(", ")}; add them to the registry stream list or they will not be materialized into tenant.env`
|
|
15699
|
-
),
|
|
15700
|
-
"",
|
|
15701
|
-
"### Auto-heal applied / available",
|
|
15702
|
-
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
15703
|
-
"",
|
|
15704
|
-
"### App-owned implementation plan",
|
|
15705
|
-
...report.appOwnedGaps.map((x) => `- ${x}`),
|
|
15706
|
-
end
|
|
15707
|
-
].join("\n");
|
|
15708
|
-
const re = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
15709
|
-
return re.test(existingBody) ? existingBody.replace(re, () => section) : `${existingBody.trim()}
|
|
15710
|
-
|
|
15711
|
-
${section}`.trim();
|
|
15712
|
-
}
|
|
15713
|
-
|
|
15714
15234
|
// src/oauth.ts
|
|
15715
15235
|
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
15716
15236
|
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
@@ -15718,7 +15238,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
|
|
|
15718
15238
|
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
15719
15239
|
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
15720
15240
|
var uniq = (xs) => [...new Set(xs)];
|
|
15721
|
-
function
|
|
15241
|
+
function defaultSubdomain(slug) {
|
|
15722
15242
|
const i = slug.indexOf("-");
|
|
15723
15243
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
15724
15244
|
}
|
|
@@ -15771,7 +15291,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15771
15291
|
throw new Error("oauth must be an object when configured");
|
|
15772
15292
|
}
|
|
15773
15293
|
const raw = rawUnknown;
|
|
15774
|
-
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [
|
|
15294
|
+
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain(slug)];
|
|
15775
15295
|
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
15776
15296
|
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
15777
15297
|
if (!callbackPath.startsWith("/")) {
|
|
@@ -15782,7 +15302,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15782
15302
|
}
|
|
15783
15303
|
const meta = mmiConfig ?? {};
|
|
15784
15304
|
const rawFofuSub = raw.fofuSubdomain;
|
|
15785
|
-
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub :
|
|
15305
|
+
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
|
|
15786
15306
|
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
15787
15307
|
}
|
|
15788
15308
|
function probeRedirectUri(callbackPath, port = 9123) {
|
|
@@ -15810,6 +15330,26 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
|
15810
15330
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
15811
15331
|
var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
15812
15332
|
var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
|
|
15333
|
+
function previewRegistryMetaMerge(existing, patch) {
|
|
15334
|
+
const out = { ...existing ?? {} };
|
|
15335
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
15336
|
+
if (value === null) delete out[key];
|
|
15337
|
+
else out[key] = value;
|
|
15338
|
+
}
|
|
15339
|
+
return out;
|
|
15340
|
+
}
|
|
15341
|
+
function boardLinkWriteError(patch, existing) {
|
|
15342
|
+
const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
|
|
15343
|
+
const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
|
|
15344
|
+
if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
|
|
15345
|
+
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15346
|
+
}
|
|
15347
|
+
const preview = previewRegistryMetaMerge(existing, patch);
|
|
15348
|
+
if (patch.projectNumber === null && preview.projectId && preview.projectNumber == null) {
|
|
15349
|
+
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15350
|
+
}
|
|
15351
|
+
return null;
|
|
15352
|
+
}
|
|
15813
15353
|
function parseSecretsCatalogVar(raw) {
|
|
15814
15354
|
let parsed;
|
|
15815
15355
|
try {
|
|
@@ -16321,17 +15861,44 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
16321
15861
|
};
|
|
16322
15862
|
}
|
|
16323
15863
|
var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
|
|
15864
|
+
function filelessTransitionGuide(repo, stage) {
|
|
15865
|
+
const authority = stage === "main" ? "Authority: main is master-admin only." : "Authority: a project-admin may complete this for their own repo; master may also do it.";
|
|
15866
|
+
return [
|
|
15867
|
+
`Fileless transition for ${repo} (${stage}):`,
|
|
15868
|
+
` ${authority}`,
|
|
15869
|
+
` mmi-cli org access role ${repo}`,
|
|
15870
|
+
` mmi-cli secrets list --repo ${repo} # names/capabilities only`,
|
|
15871
|
+
` mmi-cli secrets preflight --repo ${repo} --stage ${stage}`,
|
|
15872
|
+
` mmi-cli org project set-deploy ${repo} --stage ${stage} --no-env-file true`,
|
|
15873
|
+
` mmi-cli runtime tenant reconcile ${repo} ${stage} --watch`,
|
|
15874
|
+
` mmi-cli runtime tenant redeploy ${repo} ${stage} --watch`,
|
|
15875
|
+
` mmi-cli runtime tenant control ${repo} ${stage} verify-secrets --watch`,
|
|
15876
|
+
` mmi-cli runtime tenant control ${repo} ${stage} verify-broker --watch # broker consumers`,
|
|
15877
|
+
" Missing/undeclared key (project-admin own project):",
|
|
15878
|
+
` mmi-cli secrets declare <KEY> --repo ${repo} --purpose "<purpose>" --group "<group>" --consumers runtime --stages ${stage}`,
|
|
15879
|
+
` mmi-cli secrets set ${stage}/<KEY> --repo ${repo} # value from secure prompt/stdin; never argv/stdout`,
|
|
15880
|
+
` mmi-cli secrets request <KEY> --repo ${repo} --reason "wire requiredRuntimeSecrets for ${stage}" # only if env injection needs master wiring`,
|
|
15881
|
+
" Runbook: https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/tenant-fileless-migration.md"
|
|
15882
|
+
].join("\n");
|
|
15883
|
+
}
|
|
16324
15884
|
function composeDeclaresEnvFile(composeText) {
|
|
16325
15885
|
return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
|
|
16326
15886
|
}
|
|
16327
15887
|
function composeHasRuntimeEnvLabel(composeText) {
|
|
16328
|
-
return /
|
|
15888
|
+
return composeText.split(/\r?\n/).some((raw) => {
|
|
15889
|
+
const line = raw.replace(/(^|\s)#.*$/, "$1");
|
|
15890
|
+
return /(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line) || /mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line);
|
|
15891
|
+
});
|
|
16329
15892
|
}
|
|
16330
15893
|
function evaluateFilelessComposeGuard(input) {
|
|
16331
15894
|
if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
|
|
16332
15895
|
if (input.composeText === null) {
|
|
16333
|
-
return { ok:
|
|
15896
|
+
return { ok: false, reason: `could not read docker-compose.yml on ${input.branch} \u2014 refusing to change noEnvFile without verifying the target branch compose. Run from the target repo checkout with origin/${input.branch} fetched, or use --force only after an independent verification.` };
|
|
15897
|
+
}
|
|
15898
|
+
if (input.noEnvFile === false && composeHasRuntimeEnvLabel(input.composeText)) {
|
|
15899
|
+
return { ok: false, reason: `the ${input.branch} branch marks a service mmi.runtime-env=true \u2014 setting noEnvFile=false would make the box take the legacy path and refuse this fileless compose (exit 78). Keep noEnvFile=true or land a verified legacy compose first.` };
|
|
16334
15900
|
}
|
|
15901
|
+
if (input.noEnvFile === false) return { ok: true };
|
|
16335
15902
|
if (composeDeclaresEnvFile(input.composeText)) {
|
|
16336
15903
|
return {
|
|
16337
15904
|
ok: false,
|
|
@@ -16393,15 +15960,16 @@ function evaluatePromotionFilelessGuard(input) {
|
|
|
16393
15960
|
return {
|
|
16394
15961
|
ok: false,
|
|
16395
15962
|
reason: `the ${branch} docker-compose.yml marks a service fileless (mmi.runtime-env: "true") but DEPLOY#${stage}.noEnvFile is not true \u2014 the ${stage} deploy would fail exit-78 ("compose is fileless but DEPLOY#${stage} noEnvFile is not true"). Fix BEFORE promoting, then re-run:
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
(or pass --skip-compose-guard to override this preflight)`
|
|
15963
|
+
${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
|
|
15964
|
+
Override only after independent verification: --skip-compose-guard`
|
|
16399
15965
|
};
|
|
16400
15966
|
}
|
|
16401
15967
|
if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
|
|
16402
15968
|
return {
|
|
16403
15969
|
ok: false,
|
|
16404
|
-
reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}
|
|
15970
|
+
reason: `DEPLOY#${stage}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}.
|
|
15971
|
+
${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
|
|
15972
|
+
Rollback only after independent verification: set-deploy --no-env-file false; preflight override: --skip-compose-guard.`
|
|
16405
15973
|
};
|
|
16406
15974
|
}
|
|
16407
15975
|
return { ok: true };
|
|
@@ -16490,6 +16058,27 @@ var import_node_fs17 = require("node:fs");
|
|
|
16490
16058
|
var import_node_path15 = require("node:path");
|
|
16491
16059
|
var import_node_os5 = require("node:os");
|
|
16492
16060
|
|
|
16061
|
+
// src/project-runtime.ts
|
|
16062
|
+
function hasRuntimeSecretContract(contract) {
|
|
16063
|
+
if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
|
|
16064
|
+
return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
|
|
16065
|
+
}
|
|
16066
|
+
function projectRequiresGoogleOAuth(meta, model) {
|
|
16067
|
+
if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
|
|
16068
|
+
const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
|
|
16069
|
+
return projectType === "web-app" && Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
16070
|
+
}
|
|
16071
|
+
function edgeDomainsByStage(meta) {
|
|
16072
|
+
const edgeDomains = meta?.edgeDomains;
|
|
16073
|
+
if (!edgeDomains || typeof edgeDomains !== "object" || Array.isArray(edgeDomains)) return {};
|
|
16074
|
+
const out = {};
|
|
16075
|
+
for (const stage of ["dev", "rc", "main"]) {
|
|
16076
|
+
const value = edgeDomains[stage];
|
|
16077
|
+
if (typeof value === "string" && value.trim()) out[stage] = value.trim();
|
|
16078
|
+
}
|
|
16079
|
+
return out;
|
|
16080
|
+
}
|
|
16081
|
+
|
|
16493
16082
|
// src/secrets-diff.ts
|
|
16494
16083
|
var TIMEOUT_MS2 = 8e3;
|
|
16495
16084
|
var OWNER3 = "mutmutco";
|
|
@@ -16661,13 +16250,13 @@ async function withSecrets(run) {
|
|
|
16661
16250
|
await run(makeSecretsDeps(cfg));
|
|
16662
16251
|
}
|
|
16663
16252
|
function registerSecretsCommands(program3) {
|
|
16664
|
-
const secrets = program3.command("secrets").description("
|
|
16253
|
+
const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
|
|
16665
16254
|
secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
16666
16255
|
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
16667
|
-
secrets.command("find <intent>").description("resolve a plain-language intent to
|
|
16256
|
+
secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
|
|
16668
16257
|
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
16669
16258
|
}));
|
|
16670
|
-
secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact
|
|
16259
|
+
secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
|
|
16671
16260
|
secrets.command("doctor").description("vault drift report \u2014 declared-missing / orphan / off-scheme / duplicate (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets(async (d) => {
|
|
16672
16261
|
if (!await secretsDoctor(d, o)) process.exitCode = 1;
|
|
16673
16262
|
}));
|
|
@@ -16736,7 +16325,7 @@ function registerSecretsCommands(program3) {
|
|
|
16736
16325
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
16737
16326
|
const fact = facts?.stages?.[stage] ?? null;
|
|
16738
16327
|
const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
|
|
16739
|
-
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false });
|
|
16328
|
+
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
|
|
16740
16329
|
if (!verdict.ok) {
|
|
16741
16330
|
d.err(`secrets preflight: ${verdict.reason}`);
|
|
16742
16331
|
filelessOk = false;
|
|
@@ -16778,7 +16367,7 @@ function registerSecretsCommands(program3) {
|
|
|
16778
16367
|
if (!ok) process.exitCode = 1;
|
|
16779
16368
|
});
|
|
16780
16369
|
secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
|
|
16781
|
-
secrets.command("copy").description("copy provider keys between vault
|
|
16370
|
+
secrets.command("copy").description("copy provider keys between stages in your own project vault (audit-logged; encryption keys blocked)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
16782
16371
|
const stages = ["dev", "rc", "main"];
|
|
16783
16372
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16784
16373
|
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
@@ -16818,13 +16407,13 @@ function registerSecretsCommands(program3) {
|
|
|
16818
16407
|
);
|
|
16819
16408
|
if (!ok) process.exitCode = 1;
|
|
16820
16409
|
}));
|
|
16821
|
-
secrets.command("rm <key>").description("remove a secret
|
|
16822
|
-
secrets.command("use <key> [command...]").description("consume a secret KEYLESS:
|
|
16410
|
+
secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
16411
|
+
secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
|
|
16823
16412
|
const ok = await secretsUse(d, key, { ...o, command });
|
|
16824
16413
|
if (ok === false) process.exitCode = 1;
|
|
16825
16414
|
}));
|
|
16826
|
-
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to
|
|
16827
|
-
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-
|
|
16415
|
+
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to one specific org-infra secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
16416
|
+
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
16828
16417
|
}
|
|
16829
16418
|
|
|
16830
16419
|
// src/app-actor.ts
|
|
@@ -17216,9 +16805,9 @@ function extractWorkflowCrons(yamlText) {
|
|
|
17216
16805
|
function workflowEntry(repo, workflowPath, yamlText) {
|
|
17217
16806
|
const crons = extractWorkflowCrons(yamlText);
|
|
17218
16807
|
if (!crons.length) return null;
|
|
17219
|
-
const
|
|
16808
|
+
const basename3 = workflowPath.split("/").pop() ?? workflowPath;
|
|
17220
16809
|
return {
|
|
17221
|
-
name: `${repo}/${
|
|
16810
|
+
name: `${repo}/${basename3.replace(/\.ya?ml$/, "")}`,
|
|
17222
16811
|
cadence: crons.join(" + "),
|
|
17223
16812
|
executor: "github-actions",
|
|
17224
16813
|
llm: llmFromHeader(yamlText),
|
|
@@ -18603,7 +18192,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18603
18192
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
18604
18193
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
18605
18194
|
});
|
|
18606
|
-
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(" | ")} (
|
|
18195
|
+
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(" | ")} (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) => {
|
|
18607
18196
|
const o = {
|
|
18608
18197
|
class: rawValue("--class", "deployable"),
|
|
18609
18198
|
projectType: rawValue("--project-type", ""),
|
|
@@ -18919,12 +18508,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
18919
18508
|
function shellFor(platform2 = process.platform) {
|
|
18920
18509
|
return platform2 === "win32" ? "powershell" : "bash";
|
|
18921
18510
|
}
|
|
18922
|
-
function
|
|
18511
|
+
function isCentralContainerModel(model) {
|
|
18923
18512
|
return model === "tenant-container" || model === "solo-container";
|
|
18924
18513
|
}
|
|
18925
18514
|
function deriveStageGap(inputs) {
|
|
18926
18515
|
const missing = [];
|
|
18927
|
-
if (!
|
|
18516
|
+
if (!isCentralContainerModel(inputs.deployModel)) {
|
|
18928
18517
|
return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
18929
18518
|
}
|
|
18930
18519
|
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
@@ -19396,7 +18985,7 @@ function registerBoardCommands(program3) {
|
|
|
19396
18985
|
expected: [...BOARD_STATUSES]
|
|
19397
18986
|
});
|
|
19398
18987
|
}
|
|
19399
|
-
async function runBulkMove(issueRefs, o, status
|
|
18988
|
+
async function runBulkMove(issueRefs, o, status) {
|
|
19400
18989
|
try {
|
|
19401
18990
|
const bulk = await moveBoardIssues({
|
|
19402
18991
|
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
@@ -19414,7 +19003,7 @@ function registerBoardCommands(program3) {
|
|
|
19414
19003
|
}
|
|
19415
19004
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
19416
19005
|
} catch (e) {
|
|
19417
|
-
return failGraceful(
|
|
19006
|
+
return failGraceful(`board move failed: ${e.message}`);
|
|
19418
19007
|
}
|
|
19419
19008
|
}
|
|
19420
19009
|
withExamples(mutating(
|
|
@@ -19436,7 +19025,7 @@ function registerBoardCommands(program3) {
|
|
|
19436
19025
|
}
|
|
19437
19026
|
return;
|
|
19438
19027
|
}
|
|
19439
|
-
await runBulkMove(issueRefs, o, canonicalStatus
|
|
19028
|
+
await runBulkMove(issueRefs, o, canonicalStatus);
|
|
19440
19029
|
}), [
|
|
19441
19030
|
'mmi-cli board move "In Progress" 2680',
|
|
19442
19031
|
"mmi-cli board move Done 2680 2681 2682"
|
|
@@ -19469,19 +19058,6 @@ function registerBoardCommands(program3) {
|
|
|
19469
19058
|
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19470
19059
|
}
|
|
19471
19060
|
});
|
|
19472
|
-
mutating(board.command("done <issues...>").description("set one or more board items' Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails")).action(async (issueRefs, o) => {
|
|
19473
|
-
if (issueRefs.length === 1) {
|
|
19474
|
-
try {
|
|
19475
|
-
const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
19476
|
-
if (o.json) return console.log(JSON.stringify(result));
|
|
19477
|
-
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
|
|
19478
|
-
} catch (e) {
|
|
19479
|
-
return failGraceful(`board done failed: ${e.message}`);
|
|
19480
|
-
}
|
|
19481
|
-
return;
|
|
19482
|
-
}
|
|
19483
|
-
await runBulkMove(issueRefs, o, "Done", "board done");
|
|
19484
|
-
});
|
|
19485
19061
|
board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
|
|
19486
19062
|
const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
|
|
19487
19063
|
try {
|
|
@@ -19986,7 +19562,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
19986
19562
|
}
|
|
19987
19563
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
19988
19564
|
function spawnDeferredGcSweep() {
|
|
19989
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19565
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19990
19566
|
}
|
|
19991
19567
|
async function createDeferredWorktreeStore() {
|
|
19992
19568
|
try {
|
|
@@ -20232,7 +19808,6 @@ function classifyStaleLeaks(input) {
|
|
|
20232
19808
|
const protectedBranches = input.protectedBranches ?? PROTECTED_BRANCHES2;
|
|
20233
19809
|
const leaks = [];
|
|
20234
19810
|
const worktreeBranches2 = new Set(input.worktrees.filter((w) => !w.primary).map((w) => w.branch));
|
|
20235
|
-
const worktreeByBranch = new Map(input.worktrees.map((w) => [w.branch, w.path]));
|
|
20236
19811
|
const stageByPath = new Map(input.stages.map((s) => [s.path, s.port]));
|
|
20237
19812
|
for (const wt of input.worktrees) {
|
|
20238
19813
|
if (wt.primary) continue;
|
|
@@ -20483,7 +20058,6 @@ async function editIssue(client, options, deps = {}) {
|
|
|
20483
20058
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
20484
20059
|
const patch = {};
|
|
20485
20060
|
let bodyChanged = false;
|
|
20486
|
-
let northStarSlug;
|
|
20487
20061
|
if (options.title !== void 0) patch.title = options.title;
|
|
20488
20062
|
if (options.body !== void 0 || options.bodyFile !== void 0) {
|
|
20489
20063
|
let body;
|
|
@@ -20493,29 +20067,14 @@ async function editIssue(client, options, deps = {}) {
|
|
|
20493
20067
|
} else {
|
|
20494
20068
|
body = options.body ?? "";
|
|
20495
20069
|
}
|
|
20496
|
-
if (options.northStar) {
|
|
20497
|
-
northStarSlug = normalizeNorthStarSlug(options.northStar);
|
|
20498
|
-
body = appendNorthStarLine(body, northStarSlug);
|
|
20499
|
-
}
|
|
20500
20070
|
patch.body = body;
|
|
20501
20071
|
bodyChanged = true;
|
|
20502
|
-
} else if (options.northStar) {
|
|
20503
|
-
northStarSlug = normalizeNorthStarSlug(options.northStar);
|
|
20504
|
-
const current = await client.rest("GET", `repos/${repo}/issues/${parsed.number}`);
|
|
20505
|
-
patch.body = appendNorthStarLine(current?.body ?? "", northStarSlug);
|
|
20506
|
-
bodyChanged = true;
|
|
20507
20072
|
}
|
|
20508
20073
|
if (patch.title !== void 0 || patch.body !== void 0) {
|
|
20509
20074
|
await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, { body: patch });
|
|
20510
20075
|
}
|
|
20511
20076
|
const labelsAdded = [];
|
|
20512
20077
|
const labelsRemoved = [];
|
|
20513
|
-
if (options.northStar && northStarSlug) {
|
|
20514
|
-
const nsLabel = northStarLabel(northStarSlug);
|
|
20515
|
-
if (!options.addLabel?.includes(nsLabel)) {
|
|
20516
|
-
(options.addLabel ??= []).push(nsLabel);
|
|
20517
|
-
}
|
|
20518
|
-
}
|
|
20519
20078
|
if (options.addLabel?.length) {
|
|
20520
20079
|
await client.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, { body: { labels: options.addLabel } });
|
|
20521
20080
|
labelsAdded.push(...options.addLabel);
|
|
@@ -20545,8 +20104,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
20545
20104
|
bodyChanged,
|
|
20546
20105
|
labelsAdded,
|
|
20547
20106
|
labelsRemoved,
|
|
20548
|
-
...parentResult ? { parent: parentResult } : {}
|
|
20549
|
-
...northStarSlug ? { northStarSlug } : {}
|
|
20107
|
+
...parentResult ? { parent: parentResult } : {}
|
|
20550
20108
|
};
|
|
20551
20109
|
}
|
|
20552
20110
|
async function closeIssue(client, options, deps = {}) {
|
|
@@ -20726,7 +20284,7 @@ ${spec.body ?? ""}`;
|
|
|
20726
20284
|
const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
20727
20285
|
return `${batchKey}:${hash}`;
|
|
20728
20286
|
}
|
|
20729
|
-
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "
|
|
20287
|
+
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "parent", "repo"]);
|
|
20730
20288
|
function validateBatchSpecs(specs) {
|
|
20731
20289
|
const errors = [];
|
|
20732
20290
|
const validated = [];
|
|
@@ -20766,28 +20324,6 @@ function validateBatchSpecs(specs) {
|
|
|
20766
20324
|
}
|
|
20767
20325
|
return { ok: errors.length === 0, errors, validated };
|
|
20768
20326
|
}
|
|
20769
|
-
var NORTH_STAR_LABEL_COLOR = "5319e7";
|
|
20770
|
-
function northStarLabelsForBatch(specs) {
|
|
20771
|
-
const labels = /* @__PURE__ */ new Set();
|
|
20772
|
-
for (const spec of specs) {
|
|
20773
|
-
if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
|
|
20774
|
-
for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
|
|
20775
|
-
}
|
|
20776
|
-
return [...labels].sort((a, b) => a.localeCompare(b));
|
|
20777
|
-
}
|
|
20778
|
-
async function ensureNorthStarLabels(client, repo, specs) {
|
|
20779
|
-
const minted = [];
|
|
20780
|
-
for (const name of northStarLabelsForBatch(specs)) {
|
|
20781
|
-
try {
|
|
20782
|
-
await client.rest("POST", `repos/${repo}/labels`, {
|
|
20783
|
-
body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
|
|
20784
|
-
});
|
|
20785
|
-
minted.push(name);
|
|
20786
|
-
} catch {
|
|
20787
|
-
}
|
|
20788
|
-
}
|
|
20789
|
-
return minted;
|
|
20790
|
-
}
|
|
20791
20327
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
20792
20328
|
const client = deps.client ?? defaultGitHubClient();
|
|
20793
20329
|
const validation = validateBatchSpecs(specs);
|
|
@@ -20801,14 +20337,6 @@ ${lines}`);
|
|
|
20801
20337
|
throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
|
|
20802
20338
|
}
|
|
20803
20339
|
const rowRepo = (spec) => spec.repo ?? defaultRepo;
|
|
20804
|
-
const byRepo = /* @__PURE__ */ new Map();
|
|
20805
|
-
for (const v of validation.validated) {
|
|
20806
|
-
const r = rowRepo(v.spec);
|
|
20807
|
-
byRepo.set(r, [...byRepo.get(r) ?? [], v.spec]);
|
|
20808
|
-
}
|
|
20809
|
-
for (const [r, specsForRepo] of byRepo) {
|
|
20810
|
-
await ensureNorthStarLabels(client, r, specsForRepo);
|
|
20811
|
-
}
|
|
20812
20340
|
const created = [];
|
|
20813
20341
|
const failures = [];
|
|
20814
20342
|
for (const entry of validation.validated) {
|
|
@@ -20823,12 +20351,6 @@ ${lines}`);
|
|
|
20823
20351
|
}
|
|
20824
20352
|
}
|
|
20825
20353
|
let body = spec.body ?? "";
|
|
20826
|
-
if (spec.northStar) {
|
|
20827
|
-
const slug = normalizeNorthStarSlug(spec.northStar);
|
|
20828
|
-
body = appendNorthStarLine(body, slug);
|
|
20829
|
-
const nsLabel = northStarLabel(slug);
|
|
20830
|
-
if (!spec.labels?.includes(nsLabel)) (spec.labels ??= []).push(nsLabel);
|
|
20831
|
-
}
|
|
20832
20354
|
if (rowKey) {
|
|
20833
20355
|
body = appendIdempotencyMarker(body, rowKey);
|
|
20834
20356
|
}
|
|
@@ -20882,7 +20404,7 @@ function registerIssueLifecycleCommands(program3) {
|
|
|
20882
20404
|
const issue2 = program3.commands.find((c) => c.name() === "issue");
|
|
20883
20405
|
if (!issue2) return;
|
|
20884
20406
|
mutating(
|
|
20885
|
-
issue2.command("edit <ref>").description("edit an issue title/body/labels
|
|
20407
|
+
issue2.command("edit <ref>").description("edit an issue title/body/labels or reparent it (--parent) \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
|
|
20886
20408
|
(opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
|
|
20887
20409
|
).action(async (ref, o) => {
|
|
20888
20410
|
try {
|
|
@@ -20895,8 +20417,7 @@ function registerIssueLifecycleCommands(program3) {
|
|
|
20895
20417
|
bodyFile: o.bodyFile,
|
|
20896
20418
|
addLabel: o.addLabel,
|
|
20897
20419
|
removeLabel: o.removeLabel,
|
|
20898
|
-
parent: o.parent
|
|
20899
|
-
northStar: o.northStar
|
|
20420
|
+
parent: o.parent
|
|
20900
20421
|
});
|
|
20901
20422
|
console.log(JSON.stringify(result));
|
|
20902
20423
|
} catch (e) {
|
|
@@ -21074,7 +20595,7 @@ function buildPluginGuardDecision(i) {
|
|
|
21074
20595
|
if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
|
|
21075
20596
|
return { state: "healthy" };
|
|
21076
20597
|
}
|
|
21077
|
-
function
|
|
20598
|
+
function buildPluginGuardLine(state, opts = {}) {
|
|
21078
20599
|
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
21079
20600
|
const recovery = opts.recovery ?? "mmi-cli plugin heal";
|
|
21080
20601
|
const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
|
|
@@ -21281,8 +20802,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
21281
20802
|
}
|
|
21282
20803
|
return true;
|
|
21283
20804
|
}
|
|
21284
|
-
async function runGuard(
|
|
21285
|
-
void opts;
|
|
20805
|
+
async function runGuard(readOrigin) {
|
|
21286
20806
|
try {
|
|
21287
20807
|
const surface = detectSurface(process.env);
|
|
21288
20808
|
if (surfaceToken(surface) !== "claude") {
|
|
@@ -21292,7 +20812,7 @@ async function runGuard(opts = {}, readOrigin) {
|
|
|
21292
20812
|
const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
|
|
21293
20813
|
const input = snapshotPluginGuardInput(surface, isOrgRepo);
|
|
21294
20814
|
const { state } = buildPluginGuardDecision(input);
|
|
21295
|
-
const { line, exitCode } =
|
|
20815
|
+
const { line, exitCode } = buildPluginGuardLine(state);
|
|
21296
20816
|
if (line) console.error(line);
|
|
21297
20817
|
process.exitCode = exitCode;
|
|
21298
20818
|
} catch {
|
|
@@ -21448,7 +20968,7 @@ function formatDeployStatus(r) {
|
|
|
21448
20968
|
}
|
|
21449
20969
|
|
|
21450
20970
|
// src/deploy-commands.ts
|
|
21451
|
-
var
|
|
20971
|
+
var STAGES = ["dev", "rc", "main"];
|
|
21452
20972
|
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
21453
20973
|
const controller = new AbortController();
|
|
21454
20974
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -21465,7 +20985,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
|
21465
20985
|
function registerDeployCommands(program3) {
|
|
21466
20986
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
21467
20987
|
deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
|
|
21468
|
-
if (!
|
|
20988
|
+
if (!STAGES.includes(stage)) {
|
|
21469
20989
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
21470
20990
|
}
|
|
21471
20991
|
try {
|
|
@@ -23026,7 +22546,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
23026
22546
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
23027
22547
|
}
|
|
23028
22548
|
});
|
|
23029
|
-
program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational
|
|
22549
|
+
program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational and internal depth").option("--json", "machine-readable command manifest").option("--primary", "with --json, emit only the compact primary tree and index").action((o) => {
|
|
23030
22550
|
const manifest = buildCommandManifest(program2);
|
|
23031
22551
|
if (o.json) {
|
|
23032
22552
|
consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
|
|
@@ -23189,13 +22709,8 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
23189
22709
|
});
|
|
23190
22710
|
});
|
|
23191
22711
|
}
|
|
23192
|
-
async function primaryCheckoutRoot(
|
|
23193
|
-
|
|
23194
|
-
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
23195
|
-
return out ? (0, import_node_path23.dirname)(out) : void 0;
|
|
23196
|
-
} catch {
|
|
23197
|
-
return void 0;
|
|
23198
|
-
}
|
|
22712
|
+
async function primaryCheckoutRoot(from) {
|
|
22713
|
+
return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
23199
22714
|
}
|
|
23200
22715
|
function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
23201
22716
|
return {
|
|
@@ -23259,7 +22774,7 @@ withExamples(mutating(
|
|
|
23259
22774
|
} else if (o.claim || o.for) {
|
|
23260
22775
|
return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
|
|
23261
22776
|
}
|
|
23262
|
-
const repoRoot2 =
|
|
22777
|
+
const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
|
|
23263
22778
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
|
|
23264
22779
|
const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
|
|
23265
22780
|
if (fetchBranch) {
|
|
@@ -23468,7 +22983,7 @@ async function reportWrite(label, res) {
|
|
|
23468
22983
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
23469
22984
|
}
|
|
23470
22985
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
23471
|
-
tenant.command("control <owner/repo> <stage> <action>").description("
|
|
22986
|
+
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker watch by default)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
23472
22987
|
try {
|
|
23473
22988
|
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
|
|
23474
22989
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
@@ -23476,6 +22991,10 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
23476
22991
|
const { lines, failure } = renderVerifySecrets(body);
|
|
23477
22992
|
for (const line of lines) printLine(line);
|
|
23478
22993
|
if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
|
|
22994
|
+
} else if (!o.json && action === "verify-broker" && result.broker) {
|
|
22995
|
+
const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
|
|
22996
|
+
for (const line of lines) printLine(line);
|
|
22997
|
+
if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
|
|
23479
22998
|
} else {
|
|
23480
22999
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
23481
23000
|
}
|
|
@@ -23486,6 +23005,15 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
23486
23005
|
return failGraceful(`runtime tenant control: ${e.message}`);
|
|
23487
23006
|
}
|
|
23488
23007
|
});
|
|
23008
|
+
tenant.command("reconcile <owner/repo> <stage>").description("re-render this tenant stage's generated box assets from registry truth; project-admin own dev/rc, master main; watches by default").option("--watch", "wait for the tenant-reconcile.yml run (default)").option("--no-watch", "return after dispatch; do not redeploy until the reconcile run succeeds").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
23009
|
+
try {
|
|
23010
|
+
const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
|
|
23011
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
|
|
23012
|
+
if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
|
|
23013
|
+
} catch (e) {
|
|
23014
|
+
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
23015
|
+
}
|
|
23016
|
+
});
|
|
23489
23017
|
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").action(async (repo, stage) => {
|
|
23490
23018
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
23491
23019
|
const cfg = await loadConfig();
|
|
@@ -23527,14 +23055,6 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
23527
23055
|
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
23528
23056
|
}
|
|
23529
23057
|
});
|
|
23530
|
-
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
23531
|
-
const { lookup } = await import("node:dns/promises");
|
|
23532
|
-
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
23533
|
-
const timeout = new Promise((resolve5) => {
|
|
23534
|
-
setTimeout(() => resolve5(void 0), timeoutMs).unref?.();
|
|
23535
|
-
});
|
|
23536
|
-
return Promise.race([probe, timeout]);
|
|
23537
|
-
}
|
|
23538
23058
|
async function probeHttpBounded2(url, timeoutMs = 5e3) {
|
|
23539
23059
|
const controller = new AbortController();
|
|
23540
23060
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -23565,68 +23085,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
|
23565
23085
|
lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
|
|
23566
23086
|
});
|
|
23567
23087
|
}
|
|
23568
|
-
|
|
23569
|
-
const reg = registryClientDeps(cfg);
|
|
23570
|
-
return {
|
|
23571
|
-
resolveDns: (host) => resolveDnsBounded(host),
|
|
23572
|
-
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
23573
|
-
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
23574
|
-
hasDeployCoords: async (slug, stage) => {
|
|
23575
|
-
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
23576
|
-
return Boolean(status?.stages[stage]);
|
|
23577
|
-
},
|
|
23578
|
-
hasDeployState: async (slug, stage) => {
|
|
23579
|
-
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
23580
|
-
return Boolean(status?.deployState[stage]);
|
|
23581
|
-
},
|
|
23582
|
-
getDeployFact: async (slug, stage) => {
|
|
23583
|
-
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
23584
|
-
const fact = facts?.stages[stage];
|
|
23585
|
-
return fact ? { port: fact.port, domain: fact.domain } : null;
|
|
23586
|
-
},
|
|
23587
|
-
listSecrets: async (targetRepo2) => {
|
|
23588
|
-
const apiUrl = cfg.sagaApiUrl;
|
|
23589
|
-
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
23590
|
-
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
23591
|
-
const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
23592
|
-
method: "GET",
|
|
23593
|
-
headers: await hubHeaders()
|
|
23594
|
-
}, { attempts: 2, timeoutMs: 5e3 });
|
|
23595
|
-
if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
|
|
23596
|
-
const body = await res.json();
|
|
23597
|
-
return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
|
|
23598
|
-
}
|
|
23599
|
-
};
|
|
23600
|
-
}
|
|
23601
|
-
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
23602
|
-
const title = "v2 readiness: central deploy + secrets alignment";
|
|
23603
|
-
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
23604
|
-
const list = await execFileP2("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
|
|
23605
|
-
const issues = JSON.parse(list.stdout || "[]");
|
|
23606
|
-
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
23607
|
-
if (!existing) {
|
|
23608
|
-
const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
|
|
23609
|
-
try {
|
|
23610
|
-
const created = await execFileP2("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23611
|
-
const url = created.stdout.trim();
|
|
23612
|
-
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
23613
|
-
return { number, url, action: "created" };
|
|
23614
|
-
} finally {
|
|
23615
|
-
await create.cleanup();
|
|
23616
|
-
}
|
|
23617
|
-
}
|
|
23618
|
-
const view = await execFileP2("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
23619
|
-
const current = JSON.parse(view.stdout || "{}");
|
|
23620
|
-
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
23621
|
-
const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
|
|
23622
|
-
try {
|
|
23623
|
-
await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23624
|
-
} finally {
|
|
23625
|
-
await edit.cleanup();
|
|
23626
|
-
}
|
|
23627
|
-
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
23628
|
-
}
|
|
23629
|
-
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
|
|
23088
|
+
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
23630
23089
|
async function projectTarget(commandName, explicitTarget) {
|
|
23631
23090
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
23632
23091
|
}
|
|
@@ -23686,69 +23145,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
23686
23145
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
23687
23146
|
console.log(JSON.stringify(payload));
|
|
23688
23147
|
});
|
|
23689
|
-
project.command("
|
|
23690
|
-
const cfg = await loadConfig();
|
|
23691
|
-
let target;
|
|
23692
|
-
try {
|
|
23693
|
-
target = await projectTarget("org project doctor", repo);
|
|
23694
|
-
} catch (e) {
|
|
23695
|
-
return fail(e.message);
|
|
23696
|
-
}
|
|
23697
|
-
const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
|
|
23698
|
-
console.log(JSON.stringify(report));
|
|
23699
|
-
if (!report.ok) process.exitCode = 1;
|
|
23700
|
-
});
|
|
23701
|
-
project.command("heal [owner/repo]").description("repair Hub-owned v2 readiness state only; never product repo files; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--apply", "apply the Hub-owned registry patch").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
23702
|
-
const cfg = await loadConfig();
|
|
23703
|
-
let target;
|
|
23704
|
-
try {
|
|
23705
|
-
target = await projectTarget("org project heal", repo);
|
|
23706
|
-
} catch (e) {
|
|
23707
|
-
return fail(e.message);
|
|
23708
|
-
}
|
|
23709
|
-
const reg = registryClientDeps(cfg);
|
|
23710
|
-
const out = await runV2Heal(target, { apply: o.apply }, {
|
|
23711
|
-
fetchProject: (s) => fetchProjectBySlugChecked(s, reg),
|
|
23712
|
-
upsertProject: (s, patch) => upsertProject(s, patch, reg)
|
|
23713
|
-
});
|
|
23714
|
-
if (out.status === "aborted") return failGraceful(`org project heal: ${out.error}`);
|
|
23715
|
-
if (out.status === "planned") {
|
|
23716
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, dryRun: true, patch: out.patch, appOwnedGaps: out.appOwnedGaps }));
|
|
23717
|
-
return;
|
|
23718
|
-
}
|
|
23719
|
-
if (out.status === "noop") {
|
|
23720
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, applied: [], appOwnedGaps: out.appOwnedGaps }));
|
|
23721
|
-
return;
|
|
23722
|
-
}
|
|
23723
|
-
if (out.status === "write-failed") return reportWrite("org project heal", out.write);
|
|
23724
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, applied: out.applied, appOwnedGaps: out.appOwnedGaps, result: out.result }));
|
|
23725
|
-
});
|
|
23726
|
-
project.command("readiness [owner/repo]").description("update the repo v2 readiness issue with Hub diagnosis and app-owned tasks; defaults to the current repo").option("--update-issue", "create/update the bounded v2 readiness issue section").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
23727
|
-
if (!o.updateIssue) return fail("org project readiness: pass --update-issue");
|
|
23728
|
-
const cfg = await loadConfig();
|
|
23729
|
-
let target;
|
|
23730
|
-
try {
|
|
23731
|
-
target = await projectTarget("org project readiness", repo);
|
|
23732
|
-
} catch (e) {
|
|
23733
|
-
return fail(e.message);
|
|
23734
|
-
}
|
|
23735
|
-
const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
|
|
23736
|
-
const issue2 = await updateV2ReadinessIssue(target, report, []);
|
|
23737
|
-
console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
|
|
23738
|
-
});
|
|
23739
|
-
project.command("attest [owner/repo]").description("attest this repo's app-owned v2 readiness work is done (project-admin of the repo or master) \u2014 clears doctor's static appOwnedGaps; re-running overwrites; defaults to the current repo").option("--json", "machine-readable output").action(async (repoOrSlug) => {
|
|
23740
|
-
const cfg = await loadConfig();
|
|
23741
|
-
let target;
|
|
23742
|
-
try {
|
|
23743
|
-
target = await projectTarget("org project attest", repoOrSlug);
|
|
23744
|
-
} catch (e) {
|
|
23745
|
-
return fail(e.message);
|
|
23746
|
-
}
|
|
23747
|
-
const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
|
|
23748
|
-
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
23749
|
-
return reportWrite("org project attest", res);
|
|
23750
|
-
});
|
|
23751
|
-
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) => {
|
|
23148
|
+
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(" | ")} (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) => {
|
|
23752
23149
|
const cfg = await loadConfig();
|
|
23753
23150
|
let target;
|
|
23754
23151
|
try {
|
|
@@ -23862,7 +23259,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
23862
23259
|
console.log(JSON.stringify(report, null, 2));
|
|
23863
23260
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
23864
23261
|
});
|
|
23865
|
-
project.command("set-deploy [owner/repo]").description("patch
|
|
23262
|
+
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
23866
23263
|
const cfg = await loadConfig();
|
|
23867
23264
|
let target;
|
|
23868
23265
|
try {
|
|
@@ -23876,17 +23273,23 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
|
|
|
23876
23273
|
if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
|
|
23877
23274
|
noEnvFile = v === "true";
|
|
23878
23275
|
}
|
|
23879
|
-
if (noEnvFile
|
|
23276
|
+
if (noEnvFile !== void 0) {
|
|
23880
23277
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
23881
23278
|
if (branch) {
|
|
23882
23279
|
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
23883
23280
|
const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
|
|
23884
23281
|
if (sameRepo) {
|
|
23885
|
-
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force) });
|
|
23886
|
-
if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
|
|
23282
|
+
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
|
|
23283
|
+
if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
|
|
23284
|
+
${filelessTransitionGuide(target, o.stage)}`);
|
|
23887
23285
|
if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
|
|
23286
|
+
} else if (!o.force) {
|
|
23287
|
+
return fail(
|
|
23288
|
+
`org project set-deploy: cannot verify ${target} ${branch} from this checkout \u2014 refusing to change noEnvFile. Run from the target repo with origin/${branch} fetched.
|
|
23289
|
+
${filelessTransitionGuide(target, o.stage)}`
|
|
23290
|
+
);
|
|
23888
23291
|
} else {
|
|
23889
|
-
console.error(`org project set-deploy: --
|
|
23292
|
+
console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
|
|
23890
23293
|
}
|
|
23891
23294
|
}
|
|
23892
23295
|
}
|
|
@@ -23906,6 +23309,7 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
|
|
|
23906
23309
|
clearAliases: o.clearAliases,
|
|
23907
23310
|
noEnvFile
|
|
23908
23311
|
});
|
|
23312
|
+
if (target.includes("/")) body.repo = target;
|
|
23909
23313
|
} catch (e) {
|
|
23910
23314
|
return fail(e.message);
|
|
23911
23315
|
}
|
|
@@ -24041,7 +23445,7 @@ function resolveCreateType(raw, command) {
|
|
|
24041
23445
|
}
|
|
24042
23446
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
24043
23447
|
withExamples(mutating(
|
|
24044
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--
|
|
23448
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
24045
23449
|
// --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
|
|
24046
23450
|
// ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
|
|
24047
23451
|
// (`--parent`) and title-source are validated by the action on a real run.
|
|
@@ -24056,23 +23460,14 @@ withExamples(mutating(
|
|
|
24056
23460
|
let body;
|
|
24057
23461
|
let title;
|
|
24058
23462
|
let issueType;
|
|
24059
|
-
let northStarSlug;
|
|
24060
23463
|
let extraLabels = [];
|
|
24061
23464
|
try {
|
|
24062
23465
|
issueType = resolveCreateType(o.type, "issue create");
|
|
24063
23466
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
|
|
24064
23467
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
|
|
24065
|
-
if (o.northStar !== void 0) {
|
|
24066
|
-
northStarSlug = normalizeNorthStarSlug(o.northStar);
|
|
24067
|
-
body = appendNorthStarLine(body, northStarSlug);
|
|
24068
|
-
}
|
|
24069
23468
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
24070
23469
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
24071
23470
|
extraLabels = [...o.label ?? []];
|
|
24072
|
-
if (northStarSlug) {
|
|
24073
|
-
const nsLabel = northStarLabel(northStarSlug);
|
|
24074
|
-
if (!extraLabels.includes(nsLabel)) extraLabels.push(nsLabel);
|
|
24075
|
-
}
|
|
24076
23471
|
args = buildIssueArgs({
|
|
24077
23472
|
type: issueType,
|
|
24078
23473
|
title,
|
|
@@ -24114,7 +23509,6 @@ withExamples(mutating(
|
|
|
24114
23509
|
priority,
|
|
24115
23510
|
projectItemId,
|
|
24116
23511
|
onBoard,
|
|
24117
|
-
...northStarSlug ? { northStarSlug } : {},
|
|
24118
23512
|
...parentLinkFields(parent, parentLinkError)
|
|
24119
23513
|
}));
|
|
24120
23514
|
}), [
|
|
@@ -24753,6 +24147,12 @@ function trainApplyDeps() {
|
|
|
24753
24147
|
const body = res.body;
|
|
24754
24148
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
24755
24149
|
},
|
|
24150
|
+
dispatchTenantReconcile: async ({ repo, stage }) => {
|
|
24151
|
+
const res = await tenantReconcile({ repo, stage }, registryClientDeps(await loadConfig()));
|
|
24152
|
+
if (res.ok) return { ok: true };
|
|
24153
|
+
const body = res.body;
|
|
24154
|
+
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
24155
|
+
},
|
|
24756
24156
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
24757
24157
|
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
24758
24158
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
@@ -25026,7 +24426,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
25026
24426
|
mmiDoctorDeps()
|
|
25027
24427
|
);
|
|
25028
24428
|
});
|
|
25029
|
-
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk
|
|
24429
|
+
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
|
|
25030
24430
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
25031
24431
|
function directoryBytes(path2) {
|
|
25032
24432
|
let total = 0;
|
|
@@ -25170,18 +24570,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
25170
24570
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
|
|
25171
24571
|
});
|
|
25172
24572
|
installProcessBackstop();
|
|
25173
|
-
program2.command("lane", { hidden: true }).action(() => {
|
|
25174
|
-
fail("lane orchestration lives in jerv-cli \u2014 try: jerv-cli lane --help");
|
|
25175
|
-
});
|
|
25176
|
-
program2.command("fusion", { hidden: true }).action(() => {
|
|
25177
|
-
fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
|
|
25178
|
-
});
|
|
25179
24573
|
consolidateCommandNamespaces(program2);
|
|
25180
24574
|
applyCommandTaxonomy(program2);
|
|
25181
|
-
|
|
25182
|
-
if (rewrittenArgv.warning) process.stderr.write(`${rewrittenArgv.warning}
|
|
25183
|
-
`);
|
|
25184
|
-
program2.parseAsync(rewrittenArgv.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
24575
|
+
program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
25185
24576
|
// Annotate the CommonJS export names for ESM import in node:
|
|
25186
24577
|
0 && (module.exports = {
|
|
25187
24578
|
DEFAULT_PRIORITY,
|