@mutmutco/cli 3.32.0 → 3.33.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 +306 -838
- 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;
|
|
@@ -10386,7 +10371,14 @@ var import_node_util6 = require("node:util");
|
|
|
10386
10371
|
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
|
|
10387
10372
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
10388
10373
|
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
10389
|
-
function
|
|
10374
|
+
function earlyExitGraceMs() {
|
|
10375
|
+
if (process.env.NODE_ENV === "test") {
|
|
10376
|
+
const testOverride = Number(process.env.MMI_CLI_TEST_EARLY_EXIT_GRACE_MS);
|
|
10377
|
+
if (Number.isFinite(testOverride) && testOverride >= 25) return testOverride;
|
|
10378
|
+
}
|
|
10379
|
+
return EARLY_EXIT_GRACE_MS;
|
|
10380
|
+
}
|
|
10381
|
+
function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
10390
10382
|
return new Promise((resolve5, reject) => {
|
|
10391
10383
|
let settled = false;
|
|
10392
10384
|
const finish = (fn) => {
|
|
@@ -11504,20 +11496,18 @@ for (const [helpGroup, names] of PRIMARY_GROUPS) {
|
|
|
11504
11496
|
for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
|
|
11505
11497
|
}
|
|
11506
11498
|
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
11499
|
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
11500
|
var PATH_OVERRIDES = {
|
|
11513
11501
|
"board slice-refresh": { category: "internal", discovery: "hidden", help_group: "Operations" },
|
|
11514
|
-
|
|
11515
|
-
|
|
11502
|
+
"secrets find": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11503
|
+
"secrets catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11504
|
+
"secrets doctor": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11505
|
+
"secrets org-catalog": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11506
|
+
"secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11507
|
+
"secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
|
|
11516
11508
|
};
|
|
11517
11509
|
var HIDDEN_OPTIONS = {
|
|
11518
11510
|
"worktree create": ["--base"],
|
|
11519
|
-
"org project doctor": ["--v2"],
|
|
11520
|
-
"org project heal": ["--v2"],
|
|
11521
11511
|
"org project set": ["--set"]
|
|
11522
11512
|
};
|
|
11523
11513
|
function primaryMetadata(name) {
|
|
@@ -11535,7 +11525,6 @@ function primaryMetadata(name) {
|
|
|
11535
11525
|
function topLevelMetadata(name) {
|
|
11536
11526
|
const primary = primaryMetadata(name);
|
|
11537
11527
|
if (primary) return primary;
|
|
11538
|
-
if (name === "lane" || name === "fusion") return PATH_OVERRIDES[name];
|
|
11539
11528
|
if (!OPERATIONAL_TOP_LEVEL.has(name)) {
|
|
11540
11529
|
throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
|
|
11541
11530
|
}
|
|
@@ -11605,113 +11594,7 @@ function commandHelpGroupRank(group) {
|
|
|
11605
11594
|
return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
|
|
11606
11595
|
}
|
|
11607
11596
|
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;
|
|
11597
|
+
return metadata.category !== "internal";
|
|
11715
11598
|
}
|
|
11716
11599
|
|
|
11717
11600
|
// src/command-manifest.ts
|
|
@@ -11780,63 +11663,6 @@ function buildCommand(cmd, path2) {
|
|
|
11780
11663
|
if (commonMistakes) out.common_mistakes = commonMistakes;
|
|
11781
11664
|
return out;
|
|
11782
11665
|
}
|
|
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
11666
|
function primaryCopy(node, root = false) {
|
|
11841
11667
|
if (!root && node.discovery !== "primary") return void 0;
|
|
11842
11668
|
const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
|
|
@@ -11856,7 +11682,6 @@ function collectLeaves(node, acc) {
|
|
|
11856
11682
|
}
|
|
11857
11683
|
function buildCommandManifest(program3) {
|
|
11858
11684
|
const tree = buildCommand(program3, "");
|
|
11859
|
-
if (hasConsolidatedCommandNamespaces(program3)) addVirtualShims(tree);
|
|
11860
11685
|
const index = [];
|
|
11861
11686
|
collectLeaves(tree, index);
|
|
11862
11687
|
const primaryTree = primaryCopy(tree, true);
|
|
@@ -11913,10 +11738,71 @@ function formatManifestHuman(manifest, options = {}) {
|
|
|
11913
11738
|
}
|
|
11914
11739
|
render(command, 1);
|
|
11915
11740
|
}
|
|
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
|
|
11741
|
+
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
11742
|
return lines.join("\n");
|
|
11918
11743
|
}
|
|
11919
11744
|
|
|
11745
|
+
// src/command-consolidation.ts
|
|
11746
|
+
function child(parent, name) {
|
|
11747
|
+
const found = parent.commands.find((command) => command.name() === name);
|
|
11748
|
+
if (!found) throw new Error(`command consolidation: missing "${parent.name()} ${name}"`);
|
|
11749
|
+
return found;
|
|
11750
|
+
}
|
|
11751
|
+
function detach(parent, command) {
|
|
11752
|
+
const commands = parent.commands;
|
|
11753
|
+
const index = commands.indexOf(command);
|
|
11754
|
+
if (index < 0) throw new Error(`command consolidation: "${command.name()}" is not a child of "${parent.name()}"`);
|
|
11755
|
+
commands.splice(index, 1);
|
|
11756
|
+
return command;
|
|
11757
|
+
}
|
|
11758
|
+
function move(parent, destination, name, nextName = name) {
|
|
11759
|
+
const command = detach(parent, child(parent, name));
|
|
11760
|
+
command.name(nextName);
|
|
11761
|
+
destination.addCommand(command);
|
|
11762
|
+
return command;
|
|
11763
|
+
}
|
|
11764
|
+
function remove(parent, name) {
|
|
11765
|
+
detach(parent, child(parent, name));
|
|
11766
|
+
}
|
|
11767
|
+
function consolidateCommandNamespaces(program3) {
|
|
11768
|
+
const org = program3.command("org").description("organization projects, access, rules, credentials, and schedules");
|
|
11769
|
+
const runtime = program3.command("runtime").description("tenant, deploy, box, and edge operations");
|
|
11770
|
+
const plugin = program3.command("plugin").description("plugin lifecycle and guard operations");
|
|
11771
|
+
move(program3, org, "project");
|
|
11772
|
+
move(program3, org, "oauth");
|
|
11773
|
+
move(program3, org, "access");
|
|
11774
|
+
move(program3, org, "schedules");
|
|
11775
|
+
const config = org.command("config").description("organization configuration");
|
|
11776
|
+
const registry2 = child(program3, "registry");
|
|
11777
|
+
move(registry2, config, "org", "get");
|
|
11778
|
+
remove(program3, "registry");
|
|
11779
|
+
const orgRules = org.command("rules").description("organization-managed repository rules");
|
|
11780
|
+
const rules2 = child(program3, "rules");
|
|
11781
|
+
move(rules2, orgRules, "gitignore");
|
|
11782
|
+
remove(program3, "rules");
|
|
11783
|
+
move(program3, runtime, "tenant");
|
|
11784
|
+
const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
|
|
11785
|
+
const deploy = child(program3, "deploy");
|
|
11786
|
+
move(deploy, runtimeDeploy, "status");
|
|
11787
|
+
remove(program3, "deploy");
|
|
11788
|
+
move(program3, runtime, "box");
|
|
11789
|
+
move(program3, runtime, "edge");
|
|
11790
|
+
move(program3, plugin, "guard");
|
|
11791
|
+
move(program3, plugin, "plugin-heal", "heal");
|
|
11792
|
+
move(program3, plugin, "plugin-prune", "prune");
|
|
11793
|
+
move(program3, plugin, "session-start");
|
|
11794
|
+
const docs2 = child(program3, "docs");
|
|
11795
|
+
move(program3, docs2, "docs-audit", "audit");
|
|
11796
|
+
const train = child(program3, "train");
|
|
11797
|
+
const fullTrack2 = child(program3, "full-track");
|
|
11798
|
+
move(fullTrack2, train, "readiness");
|
|
11799
|
+
remove(program3, "full-track");
|
|
11800
|
+
const worktree2 = child(program3, "worktree");
|
|
11801
|
+
move(program3, worktree2, "gc");
|
|
11802
|
+
const stage = child(program3, "stage");
|
|
11803
|
+
move(program3, stage, "port-range");
|
|
11804
|
+
}
|
|
11805
|
+
|
|
11920
11806
|
// src/version-lag.ts
|
|
11921
11807
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
11922
11808
|
var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
|
|
@@ -12449,6 +12335,14 @@ function parseVerifySecrets(stdout) {
|
|
|
12449
12335
|
}
|
|
12450
12336
|
return out;
|
|
12451
12337
|
}
|
|
12338
|
+
function parseVerifyBroker(stdout) {
|
|
12339
|
+
const out = [];
|
|
12340
|
+
for (const line of stdout.split("\n")) {
|
|
12341
|
+
const match = /^(\S+):\s*(reachable|missing|denied|error)\b/.exec(line.trim());
|
|
12342
|
+
if (match) out.push({ key: match[1], status: match[2] });
|
|
12343
|
+
}
|
|
12344
|
+
return out;
|
|
12345
|
+
}
|
|
12452
12346
|
|
|
12453
12347
|
// src/train-apply.ts
|
|
12454
12348
|
function resolveDeployModel2(meta, repo) {
|
|
@@ -12732,7 +12626,7 @@ function classifyProjectGetFailure(text) {
|
|
|
12732
12626
|
}
|
|
12733
12627
|
async function loadProjectMeta(deps, ctx) {
|
|
12734
12628
|
try {
|
|
12735
|
-
const out = await deps.runSelf(["project", "get", ctx.repo]);
|
|
12629
|
+
const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
|
|
12736
12630
|
const parsed = JSON.parse(out);
|
|
12737
12631
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12738
12632
|
return { status: "read-error", error: "invalid registry META JSON" };
|
|
@@ -12851,6 +12745,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
|
|
|
12851
12745
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
12852
12746
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
12853
12747
|
}
|
|
12748
|
+
function correlateReconcileRun(deps, since, titleIncludes) {
|
|
12749
|
+
return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
|
|
12750
|
+
}
|
|
12854
12751
|
async function correlateWorkflowRun(deps, args) {
|
|
12855
12752
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
12856
12753
|
}
|
|
@@ -13906,8 +13803,36 @@ async function runTenantRedeploy(deps, options) {
|
|
|
13906
13803
|
const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
|
|
13907
13804
|
return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
13908
13805
|
}
|
|
13806
|
+
async function runTenantReconcile(deps, options) {
|
|
13807
|
+
const parts = options.repo.split("/");
|
|
13808
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`repo must be owner/name, got ${options.repo}`);
|
|
13809
|
+
if (!["dev", "rc", "main"].includes(options.stage)) throw new Error(`stage must be dev, rc, or main, got ${options.stage}`);
|
|
13810
|
+
if (!deps.dispatchTenantReconcile) throw new Error("tenant reconcile dispatch is unavailable");
|
|
13811
|
+
const stage = options.stage;
|
|
13812
|
+
const base = { command: "tenant-reconcile", repo: options.repo, stage };
|
|
13813
|
+
const since = (deps.now ?? Date.now)();
|
|
13814
|
+
const dispatched = await deps.dispatchTenantReconcile({ repo: options.repo, stage });
|
|
13815
|
+
if (!dispatched.ok) {
|
|
13816
|
+
return {
|
|
13817
|
+
...base,
|
|
13818
|
+
dispatched: false,
|
|
13819
|
+
conclusion: "failure",
|
|
13820
|
+
note: `tenant reconcile rejected: ${dispatched.error ?? "request rejected by the Hub"}`
|
|
13821
|
+
};
|
|
13822
|
+
}
|
|
13823
|
+
const slug = parts[1].toLowerCase();
|
|
13824
|
+
const run = await correlateReconcileRun(deps, since, [slug, stage]);
|
|
13825
|
+
const conclusion = options.watch ?? true ? await watchTenantRun(deps, run.runId) : "pending";
|
|
13826
|
+
return {
|
|
13827
|
+
...base,
|
|
13828
|
+
dispatched: true,
|
|
13829
|
+
...run,
|
|
13830
|
+
conclusion,
|
|
13831
|
+
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"
|
|
13832
|
+
};
|
|
13833
|
+
}
|
|
13909
13834
|
function tenantControlWatches(action) {
|
|
13910
|
-
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
13835
|
+
return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
|
|
13911
13836
|
}
|
|
13912
13837
|
async function runTenantControl(deps, options) {
|
|
13913
13838
|
const { repo, stage, action } = options;
|
|
@@ -13931,13 +13856,16 @@ async function runTenantControl(deps, options) {
|
|
|
13931
13856
|
if (action === "retire") {
|
|
13932
13857
|
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
13933
13858
|
}
|
|
13934
|
-
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets")) {
|
|
13859
|
+
if (watch && runId != null && conclusion === "success" && (action === "status" || action === "verify-secrets" || action === "verify-broker")) {
|
|
13935
13860
|
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
13936
13861
|
if (action === "status") {
|
|
13937
13862
|
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
13938
|
-
} else {
|
|
13863
|
+
} else if (action === "verify-secrets") {
|
|
13939
13864
|
result.secrets = parseVerifySecrets(output);
|
|
13940
13865
|
result.secretsRaw = output;
|
|
13866
|
+
} else {
|
|
13867
|
+
result.broker = parseVerifyBroker(output);
|
|
13868
|
+
result.brokerRaw = output;
|
|
13941
13869
|
}
|
|
13942
13870
|
}
|
|
13943
13871
|
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 +13915,26 @@ function renderVerifySecrets(body) {
|
|
|
13987
13915
|
return { lines, failure: null };
|
|
13988
13916
|
}
|
|
13989
13917
|
|
|
13918
|
+
// src/tenant-verify-broker.ts
|
|
13919
|
+
function renderVerifyBroker(input) {
|
|
13920
|
+
if (input.broker.length === 0) {
|
|
13921
|
+
const raw = input.raw?.trim();
|
|
13922
|
+
if (raw?.startsWith("no broker-readable runtime secrets declared")) return { lines: [raw], failure: null };
|
|
13923
|
+
return {
|
|
13924
|
+
lines: raw ? raw.split("\n") : ["verify-broker: no per-key verdicts returned"],
|
|
13925
|
+
failure: "verify-broker returned no per-key verdicts, so broker access was not verified"
|
|
13926
|
+
};
|
|
13927
|
+
}
|
|
13928
|
+
const lines = input.broker.map((item) => `${item.key}: ${item.status}`);
|
|
13929
|
+
const reachable = input.broker.filter((item) => item.status === "reachable").length;
|
|
13930
|
+
const bad = input.broker.length - reachable;
|
|
13931
|
+
lines.push(`verify-broker: ${reachable} reachable, ${bad} failed`);
|
|
13932
|
+
return {
|
|
13933
|
+
lines,
|
|
13934
|
+
failure: bad > 0 ? `${bad} of ${input.broker.length} broker read(s) failed; redeploy refreshes an expired runtime token` : null
|
|
13935
|
+
};
|
|
13936
|
+
}
|
|
13937
|
+
|
|
13990
13938
|
// src/hotfix-coverage.ts
|
|
13991
13939
|
var import_node_child_process8 = require("node:child_process");
|
|
13992
13940
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
@@ -15009,14 +14957,10 @@ function loadAccessTargets(projectsJson) {
|
|
|
15009
14957
|
const seen = /* @__PURE__ */ new Set();
|
|
15010
14958
|
const targets = [];
|
|
15011
14959
|
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
14960
|
for (const repo of project2.repos ?? []) {
|
|
15016
14961
|
if (seen.has(repo)) continue;
|
|
15017
14962
|
seen.add(repo);
|
|
15018
|
-
const
|
|
15019
|
-
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
|
|
14963
|
+
const cls = project2.class === "content" || project2.deployModel === "content" ? "content" : "deployable";
|
|
15020
14964
|
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
15021
14965
|
targets.push({ repo, class: cls, releaseTrack });
|
|
15022
14966
|
}
|
|
@@ -15096,9 +15040,13 @@ function renderAccessReport(report) {
|
|
|
15096
15040
|
var import_node_fs16 = require("node:fs");
|
|
15097
15041
|
var import_node_path14 = require("node:path");
|
|
15098
15042
|
var DOCS_INDEX_PATH = "docs/index.md";
|
|
15043
|
+
function isRoutableDocsPath(relPath) {
|
|
15044
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
15045
|
+
return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
|
|
15046
|
+
}
|
|
15099
15047
|
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(
|
|
15048
|
+
function plainInline(text) {
|
|
15049
|
+
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
15050
|
}
|
|
15103
15051
|
function extractDocMeta(relPath, raw) {
|
|
15104
15052
|
const lines = raw.split(/\r?\n/);
|
|
@@ -15126,7 +15074,7 @@ function extractDocMeta(relPath, raw) {
|
|
|
15126
15074
|
break;
|
|
15127
15075
|
}
|
|
15128
15076
|
if (trimmed.startsWith("#")) continue;
|
|
15129
|
-
scope =
|
|
15077
|
+
scope = plainInline(trimmed);
|
|
15130
15078
|
break;
|
|
15131
15079
|
}
|
|
15132
15080
|
if (!title) {
|
|
@@ -15138,16 +15086,16 @@ function extractDocMeta(relPath, raw) {
|
|
|
15138
15086
|
function renderDocsIndex(entries) {
|
|
15139
15087
|
const lines = [
|
|
15140
15088
|
GENERATED_HEADER,
|
|
15089
|
+
"",
|
|
15141
15090
|
"# Documentation index",
|
|
15142
15091
|
"",
|
|
15143
15092
|
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
15144
15093
|
"can never diverge from the records it lists.",
|
|
15145
|
-
""
|
|
15146
|
-
"| Document | Scope |",
|
|
15147
|
-
"| --- | --- |"
|
|
15094
|
+
""
|
|
15148
15095
|
];
|
|
15149
15096
|
for (const entry of entries) {
|
|
15150
|
-
|
|
15097
|
+
const link = `[${plainInline(entry.title)}](${entry.path})`;
|
|
15098
|
+
lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
|
|
15151
15099
|
}
|
|
15152
15100
|
return lines.join("\n") + "\n";
|
|
15153
15101
|
}
|
|
@@ -15183,7 +15131,7 @@ function createDocsIndexDeps(repoRoot2) {
|
|
|
15183
15131
|
const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
|
|
15184
15132
|
const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
15185
15133
|
return {
|
|
15186
|
-
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(
|
|
15134
|
+
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
15187
15135
|
readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
|
|
15188
15136
|
readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
|
|
15189
15137
|
writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
|
|
@@ -15290,427 +15238,6 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
15290
15238
|
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
15291
15239
|
}
|
|
15292
15240
|
|
|
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
15241
|
// src/oauth.ts
|
|
15715
15242
|
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
15716
15243
|
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
@@ -15718,7 +15245,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
|
|
|
15718
15245
|
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
15719
15246
|
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
15720
15247
|
var uniq = (xs) => [...new Set(xs)];
|
|
15721
|
-
function
|
|
15248
|
+
function defaultSubdomain(slug) {
|
|
15722
15249
|
const i = slug.indexOf("-");
|
|
15723
15250
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
15724
15251
|
}
|
|
@@ -15771,7 +15298,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15771
15298
|
throw new Error("oauth must be an object when configured");
|
|
15772
15299
|
}
|
|
15773
15300
|
const raw = rawUnknown;
|
|
15774
|
-
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [
|
|
15301
|
+
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain(slug)];
|
|
15775
15302
|
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
15776
15303
|
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
15777
15304
|
if (!callbackPath.startsWith("/")) {
|
|
@@ -15782,7 +15309,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15782
15309
|
}
|
|
15783
15310
|
const meta = mmiConfig ?? {};
|
|
15784
15311
|
const rawFofuSub = raw.fofuSubdomain;
|
|
15785
|
-
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub :
|
|
15312
|
+
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
|
|
15786
15313
|
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
15787
15314
|
}
|
|
15788
15315
|
function probeRedirectUri(callbackPath, port = 9123) {
|
|
@@ -15810,6 +15337,26 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
|
15810
15337
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
15811
15338
|
var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
15812
15339
|
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)$/;
|
|
15340
|
+
function previewRegistryMetaMerge(existing, patch) {
|
|
15341
|
+
const out = { ...existing ?? {} };
|
|
15342
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
15343
|
+
if (value === null) delete out[key];
|
|
15344
|
+
else out[key] = value;
|
|
15345
|
+
}
|
|
15346
|
+
return out;
|
|
15347
|
+
}
|
|
15348
|
+
function boardLinkWriteError(patch, existing) {
|
|
15349
|
+
const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
|
|
15350
|
+
const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
|
|
15351
|
+
if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
|
|
15352
|
+
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15353
|
+
}
|
|
15354
|
+
const preview = previewRegistryMetaMerge(existing, patch);
|
|
15355
|
+
if (patch.projectNumber === null && preview.projectId && preview.projectNumber == null) {
|
|
15356
|
+
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
15357
|
+
}
|
|
15358
|
+
return null;
|
|
15359
|
+
}
|
|
15813
15360
|
function parseSecretsCatalogVar(raw) {
|
|
15814
15361
|
let parsed;
|
|
15815
15362
|
try {
|
|
@@ -16321,17 +15868,44 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
16321
15868
|
};
|
|
16322
15869
|
}
|
|
16323
15870
|
var DEPLOY_STAGE_BRANCH = { dev: "development", rc: "rc", main: "main" };
|
|
15871
|
+
function filelessTransitionGuide(repo, stage) {
|
|
15872
|
+
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.";
|
|
15873
|
+
return [
|
|
15874
|
+
`Fileless transition for ${repo} (${stage}):`,
|
|
15875
|
+
` ${authority}`,
|
|
15876
|
+
` mmi-cli org access role ${repo}`,
|
|
15877
|
+
` mmi-cli secrets list --repo ${repo} # names/capabilities only`,
|
|
15878
|
+
` mmi-cli secrets preflight --repo ${repo} --stage ${stage}`,
|
|
15879
|
+
` mmi-cli org project set-deploy ${repo} --stage ${stage} --no-env-file true`,
|
|
15880
|
+
` mmi-cli runtime tenant reconcile ${repo} ${stage} --watch`,
|
|
15881
|
+
` mmi-cli runtime tenant redeploy ${repo} ${stage} --watch`,
|
|
15882
|
+
` mmi-cli runtime tenant control ${repo} ${stage} verify-secrets --watch`,
|
|
15883
|
+
` mmi-cli runtime tenant control ${repo} ${stage} verify-broker --watch # broker consumers`,
|
|
15884
|
+
" Missing/undeclared key (project-admin own project):",
|
|
15885
|
+
` mmi-cli secrets declare <KEY> --repo ${repo} --purpose "<purpose>" --group "<group>" --consumers runtime --stages ${stage}`,
|
|
15886
|
+
` mmi-cli secrets set ${stage}/<KEY> --repo ${repo} # value from secure prompt/stdin; never argv/stdout`,
|
|
15887
|
+
` mmi-cli secrets request <KEY> --repo ${repo} --reason "wire requiredRuntimeSecrets for ${stage}" # only if env injection needs master wiring`,
|
|
15888
|
+
" Runbook: https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/tenant-fileless-migration.md"
|
|
15889
|
+
].join("\n");
|
|
15890
|
+
}
|
|
16324
15891
|
function composeDeclaresEnvFile(composeText) {
|
|
16325
15892
|
return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
|
|
16326
15893
|
}
|
|
16327
15894
|
function composeHasRuntimeEnvLabel(composeText) {
|
|
16328
|
-
return /
|
|
15895
|
+
return composeText.split(/\r?\n/).some((raw) => {
|
|
15896
|
+
const line = raw.replace(/(^|\s)#.*$/, "$1");
|
|
15897
|
+
return /(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line) || /mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line);
|
|
15898
|
+
});
|
|
16329
15899
|
}
|
|
16330
15900
|
function evaluateFilelessComposeGuard(input) {
|
|
16331
15901
|
if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
|
|
16332
15902
|
if (input.composeText === null) {
|
|
16333
|
-
return { ok:
|
|
15903
|
+
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.` };
|
|
16334
15904
|
}
|
|
15905
|
+
if (input.noEnvFile === false && composeHasRuntimeEnvLabel(input.composeText)) {
|
|
15906
|
+
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.` };
|
|
15907
|
+
}
|
|
15908
|
+
if (input.noEnvFile === false) return { ok: true };
|
|
16335
15909
|
if (composeDeclaresEnvFile(input.composeText)) {
|
|
16336
15910
|
return {
|
|
16337
15911
|
ok: false,
|
|
@@ -16393,15 +15967,16 @@ function evaluatePromotionFilelessGuard(input) {
|
|
|
16393
15967
|
return {
|
|
16394
15968
|
ok: false,
|
|
16395
15969
|
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)`
|
|
15970
|
+
${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
|
|
15971
|
+
Override only after independent verification: --skip-compose-guard`
|
|
16399
15972
|
};
|
|
16400
15973
|
}
|
|
16401
15974
|
if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
|
|
16402
15975
|
return {
|
|
16403
15976
|
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}
|
|
15977
|
+
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}.
|
|
15978
|
+
${filelessTransitionGuide(input.repo ?? "<owner/repo>", stage)}
|
|
15979
|
+
Rollback only after independent verification: set-deploy --no-env-file false; preflight override: --skip-compose-guard.`
|
|
16405
15980
|
};
|
|
16406
15981
|
}
|
|
16407
15982
|
return { ok: true };
|
|
@@ -16490,6 +16065,27 @@ var import_node_fs17 = require("node:fs");
|
|
|
16490
16065
|
var import_node_path15 = require("node:path");
|
|
16491
16066
|
var import_node_os5 = require("node:os");
|
|
16492
16067
|
|
|
16068
|
+
// src/project-runtime.ts
|
|
16069
|
+
function hasRuntimeSecretContract(contract) {
|
|
16070
|
+
if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
|
|
16071
|
+
return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
|
|
16072
|
+
}
|
|
16073
|
+
function projectRequiresGoogleOAuth(meta, model) {
|
|
16074
|
+
if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
|
|
16075
|
+
const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
|
|
16076
|
+
return projectType === "web-app" && Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
16077
|
+
}
|
|
16078
|
+
function edgeDomainsByStage(meta) {
|
|
16079
|
+
const edgeDomains = meta?.edgeDomains;
|
|
16080
|
+
if (!edgeDomains || typeof edgeDomains !== "object" || Array.isArray(edgeDomains)) return {};
|
|
16081
|
+
const out = {};
|
|
16082
|
+
for (const stage of ["dev", "rc", "main"]) {
|
|
16083
|
+
const value = edgeDomains[stage];
|
|
16084
|
+
if (typeof value === "string" && value.trim()) out[stage] = value.trim();
|
|
16085
|
+
}
|
|
16086
|
+
return out;
|
|
16087
|
+
}
|
|
16088
|
+
|
|
16493
16089
|
// src/secrets-diff.ts
|
|
16494
16090
|
var TIMEOUT_MS2 = 8e3;
|
|
16495
16091
|
var OWNER3 = "mutmutco";
|
|
@@ -16661,13 +16257,13 @@ async function withSecrets(run) {
|
|
|
16661
16257
|
await run(makeSecretsDeps(cfg));
|
|
16662
16258
|
}
|
|
16663
16259
|
function registerSecretsCommands(program3) {
|
|
16664
|
-
const secrets = program3.command("secrets").description("
|
|
16260
|
+
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
16261
|
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
16262
|
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
|
|
16263
|
+
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
16264
|
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
16669
16265
|
}));
|
|
16670
|
-
secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact
|
|
16266
|
+
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
16267
|
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
16268
|
if (!await secretsDoctor(d, o)) process.exitCode = 1;
|
|
16673
16269
|
}));
|
|
@@ -16736,7 +16332,7 @@ function registerSecretsCommands(program3) {
|
|
|
16736
16332
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
16737
16333
|
const fact = facts?.stages?.[stage] ?? null;
|
|
16738
16334
|
const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
|
|
16739
|
-
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false });
|
|
16335
|
+
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage, fact, sameRepo: Boolean(sameRepo), force: false, repo });
|
|
16740
16336
|
if (!verdict.ok) {
|
|
16741
16337
|
d.err(`secrets preflight: ${verdict.reason}`);
|
|
16742
16338
|
filelessOk = false;
|
|
@@ -16778,7 +16374,7 @@ function registerSecretsCommands(program3) {
|
|
|
16778
16374
|
if (!ok) process.exitCode = 1;
|
|
16779
16375
|
});
|
|
16780
16376
|
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
|
|
16377
|
+
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
16378
|
const stages = ["dev", "rc", "main"];
|
|
16783
16379
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16784
16380
|
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
@@ -16818,13 +16414,13 @@ function registerSecretsCommands(program3) {
|
|
|
16818
16414
|
);
|
|
16819
16415
|
if (!ok) process.exitCode = 1;
|
|
16820
16416
|
}));
|
|
16821
|
-
secrets.command("rm <key>").description("remove a secret
|
|
16822
|
-
secrets.command("use <key> [command...]").description("consume a secret KEYLESS:
|
|
16417
|
+
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)));
|
|
16418
|
+
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
16419
|
const ok = await secretsUse(d, key, { ...o, command });
|
|
16824
16420
|
if (ok === false) process.exitCode = 1;
|
|
16825
16421
|
}));
|
|
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-
|
|
16422
|
+
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, {})));
|
|
16423
|
+
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
16424
|
}
|
|
16829
16425
|
|
|
16830
16426
|
// src/app-actor.ts
|
|
@@ -18603,7 +18199,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18603
18199
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
18604
18200
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
18605
18201
|
});
|
|
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(" | ")} (
|
|
18202
|
+
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
18203
|
const o = {
|
|
18608
18204
|
class: rawValue("--class", "deployable"),
|
|
18609
18205
|
projectType: rawValue("--project-type", ""),
|
|
@@ -18919,12 +18515,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
18919
18515
|
function shellFor(platform2 = process.platform) {
|
|
18920
18516
|
return platform2 === "win32" ? "powershell" : "bash";
|
|
18921
18517
|
}
|
|
18922
|
-
function
|
|
18518
|
+
function isCentralContainerModel(model) {
|
|
18923
18519
|
return model === "tenant-container" || model === "solo-container";
|
|
18924
18520
|
}
|
|
18925
18521
|
function deriveStageGap(inputs) {
|
|
18926
18522
|
const missing = [];
|
|
18927
|
-
if (!
|
|
18523
|
+
if (!isCentralContainerModel(inputs.deployModel)) {
|
|
18928
18524
|
return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
18929
18525
|
}
|
|
18930
18526
|
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
@@ -19396,7 +18992,7 @@ function registerBoardCommands(program3) {
|
|
|
19396
18992
|
expected: [...BOARD_STATUSES]
|
|
19397
18993
|
});
|
|
19398
18994
|
}
|
|
19399
|
-
async function runBulkMove(issueRefs, o, status
|
|
18995
|
+
async function runBulkMove(issueRefs, o, status) {
|
|
19400
18996
|
try {
|
|
19401
18997
|
const bulk = await moveBoardIssues({
|
|
19402
18998
|
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
@@ -19414,7 +19010,7 @@ function registerBoardCommands(program3) {
|
|
|
19414
19010
|
}
|
|
19415
19011
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
19416
19012
|
} catch (e) {
|
|
19417
|
-
return failGraceful(
|
|
19013
|
+
return failGraceful(`board move failed: ${e.message}`);
|
|
19418
19014
|
}
|
|
19419
19015
|
}
|
|
19420
19016
|
withExamples(mutating(
|
|
@@ -19436,7 +19032,7 @@ function registerBoardCommands(program3) {
|
|
|
19436
19032
|
}
|
|
19437
19033
|
return;
|
|
19438
19034
|
}
|
|
19439
|
-
await runBulkMove(issueRefs, o, canonicalStatus
|
|
19035
|
+
await runBulkMove(issueRefs, o, canonicalStatus);
|
|
19440
19036
|
}), [
|
|
19441
19037
|
'mmi-cli board move "In Progress" 2680',
|
|
19442
19038
|
"mmi-cli board move Done 2680 2681 2682"
|
|
@@ -19469,19 +19065,6 @@ function registerBoardCommands(program3) {
|
|
|
19469
19065
|
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19470
19066
|
}
|
|
19471
19067
|
});
|
|
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
19068
|
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
19069
|
const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
|
|
19487
19070
|
try {
|
|
@@ -19986,7 +19569,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
19986
19569
|
}
|
|
19987
19570
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
19988
19571
|
function spawnDeferredGcSweep() {
|
|
19989
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19572
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
19990
19573
|
}
|
|
19991
19574
|
async function createDeferredWorktreeStore() {
|
|
19992
19575
|
try {
|
|
@@ -21074,7 +20657,7 @@ function buildPluginGuardDecision(i) {
|
|
|
21074
20657
|
if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
|
|
21075
20658
|
return { state: "healthy" };
|
|
21076
20659
|
}
|
|
21077
|
-
function
|
|
20660
|
+
function buildPluginGuardLine(state, opts = {}) {
|
|
21078
20661
|
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
21079
20662
|
const recovery = opts.recovery ?? "mmi-cli plugin heal";
|
|
21080
20663
|
const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
|
|
@@ -21281,8 +20864,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
21281
20864
|
}
|
|
21282
20865
|
return true;
|
|
21283
20866
|
}
|
|
21284
|
-
async function runGuard(
|
|
21285
|
-
void opts;
|
|
20867
|
+
async function runGuard(readOrigin) {
|
|
21286
20868
|
try {
|
|
21287
20869
|
const surface = detectSurface(process.env);
|
|
21288
20870
|
if (surfaceToken(surface) !== "claude") {
|
|
@@ -21292,7 +20874,7 @@ async function runGuard(opts = {}, readOrigin) {
|
|
|
21292
20874
|
const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
|
|
21293
20875
|
const input = snapshotPluginGuardInput(surface, isOrgRepo);
|
|
21294
20876
|
const { state } = buildPluginGuardDecision(input);
|
|
21295
|
-
const { line, exitCode } =
|
|
20877
|
+
const { line, exitCode } = buildPluginGuardLine(state);
|
|
21296
20878
|
if (line) console.error(line);
|
|
21297
20879
|
process.exitCode = exitCode;
|
|
21298
20880
|
} catch {
|
|
@@ -21448,7 +21030,7 @@ function formatDeployStatus(r) {
|
|
|
21448
21030
|
}
|
|
21449
21031
|
|
|
21450
21032
|
// src/deploy-commands.ts
|
|
21451
|
-
var
|
|
21033
|
+
var STAGES = ["dev", "rc", "main"];
|
|
21452
21034
|
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
21453
21035
|
const controller = new AbortController();
|
|
21454
21036
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -21465,7 +21047,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
|
21465
21047
|
function registerDeployCommands(program3) {
|
|
21466
21048
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
21467
21049
|
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 (!
|
|
21050
|
+
if (!STAGES.includes(stage)) {
|
|
21469
21051
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
21470
21052
|
}
|
|
21471
21053
|
try {
|
|
@@ -23026,7 +22608,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
23026
22608
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
23027
22609
|
}
|
|
23028
22610
|
});
|
|
23029
|
-
program2.command("commands").description("print the grouped agentic-coding map; use --all for operational depth").option("--all", "include operational
|
|
22611
|
+
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
22612
|
const manifest = buildCommandManifest(program2);
|
|
23031
22613
|
if (o.json) {
|
|
23032
22614
|
consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
|
|
@@ -23468,7 +23050,7 @@ async function reportWrite(label, res) {
|
|
|
23468
23050
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
23469
23051
|
}
|
|
23470
23052
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
23471
|
-
tenant.command("control <owner/repo> <stage> <action>").description("
|
|
23053
|
+
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
23054
|
try {
|
|
23473
23055
|
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
|
|
23474
23056
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
@@ -23476,6 +23058,10 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
23476
23058
|
const { lines, failure } = renderVerifySecrets(body);
|
|
23477
23059
|
for (const line of lines) printLine(line);
|
|
23478
23060
|
if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}`);
|
|
23061
|
+
} else if (!o.json && action === "verify-broker" && result.broker) {
|
|
23062
|
+
const { lines, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
|
|
23063
|
+
for (const line of lines) printLine(line);
|
|
23064
|
+
if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}`);
|
|
23479
23065
|
} else {
|
|
23480
23066
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
23481
23067
|
}
|
|
@@ -23486,6 +23072,15 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
23486
23072
|
return failGraceful(`runtime tenant control: ${e.message}`);
|
|
23487
23073
|
}
|
|
23488
23074
|
});
|
|
23075
|
+
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) => {
|
|
23076
|
+
try {
|
|
23077
|
+
const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
|
|
23078
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
|
|
23079
|
+
if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
|
|
23080
|
+
} catch (e) {
|
|
23081
|
+
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
23082
|
+
}
|
|
23083
|
+
});
|
|
23489
23084
|
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
23085
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
23491
23086
|
const cfg = await loadConfig();
|
|
@@ -23527,14 +23122,6 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
23527
23122
|
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
23528
23123
|
}
|
|
23529
23124
|
});
|
|
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
23125
|
async function probeHttpBounded2(url, timeoutMs = 5e3) {
|
|
23539
23126
|
const controller = new AbortController();
|
|
23540
23127
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -23565,68 +23152,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
|
23565
23152
|
lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
|
|
23566
23153
|
});
|
|
23567
23154
|
}
|
|
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");
|
|
23155
|
+
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
23630
23156
|
async function projectTarget(commandName, explicitTarget) {
|
|
23631
23157
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
23632
23158
|
}
|
|
@@ -23686,69 +23212,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
23686
23212
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
23687
23213
|
console.log(JSON.stringify(payload));
|
|
23688
23214
|
});
|
|
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) => {
|
|
23215
|
+
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
23216
|
const cfg = await loadConfig();
|
|
23753
23217
|
let target;
|
|
23754
23218
|
try {
|
|
@@ -23862,7 +23326,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
23862
23326
|
console.log(JSON.stringify(report, null, 2));
|
|
23863
23327
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
23864
23328
|
});
|
|
23865
|
-
project.command("set-deploy [owner/repo]").description("patch
|
|
23329
|
+
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
23330
|
const cfg = await loadConfig();
|
|
23867
23331
|
let target;
|
|
23868
23332
|
try {
|
|
@@ -23876,17 +23340,23 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
|
|
|
23876
23340
|
if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
|
|
23877
23341
|
noEnvFile = v === "true";
|
|
23878
23342
|
}
|
|
23879
|
-
if (noEnvFile
|
|
23343
|
+
if (noEnvFile !== void 0) {
|
|
23880
23344
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
23881
23345
|
if (branch) {
|
|
23882
23346
|
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
23883
23347
|
const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
|
|
23884
23348
|
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}
|
|
23349
|
+
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force), noEnvFile });
|
|
23350
|
+
if (!verdict.ok) return fail(`org project set-deploy: ${verdict.reason}
|
|
23351
|
+
${filelessTransitionGuide(target, o.stage)}`);
|
|
23887
23352
|
if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
|
|
23353
|
+
} else if (!o.force) {
|
|
23354
|
+
return fail(
|
|
23355
|
+
`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.
|
|
23356
|
+
${filelessTransitionGuide(target, o.stage)}`
|
|
23357
|
+
);
|
|
23888
23358
|
} else {
|
|
23889
|
-
console.error(`org project set-deploy: --
|
|
23359
|
+
console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
|
|
23890
23360
|
}
|
|
23891
23361
|
}
|
|
23892
23362
|
}
|
|
@@ -23906,6 +23376,7 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
|
|
|
23906
23376
|
clearAliases: o.clearAliases,
|
|
23907
23377
|
noEnvFile
|
|
23908
23378
|
});
|
|
23379
|
+
if (target.includes("/")) body.repo = target;
|
|
23909
23380
|
} catch (e) {
|
|
23910
23381
|
return fail(e.message);
|
|
23911
23382
|
}
|
|
@@ -24753,6 +24224,12 @@ function trainApplyDeps() {
|
|
|
24753
24224
|
const body = res.body;
|
|
24754
24225
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
24755
24226
|
},
|
|
24227
|
+
dispatchTenantReconcile: async ({ repo, stage }) => {
|
|
24228
|
+
const res = await tenantReconcile({ repo, stage }, registryClientDeps(await loadConfig()));
|
|
24229
|
+
if (res.ok) return { ok: true };
|
|
24230
|
+
const body = res.body;
|
|
24231
|
+
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
24232
|
+
},
|
|
24756
24233
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
24757
24234
|
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
24758
24235
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
@@ -25026,7 +24503,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
25026
24503
|
mmiDoctorDeps()
|
|
25027
24504
|
);
|
|
25028
24505
|
});
|
|
25029
|
-
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk
|
|
24506
|
+
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
|
|
25030
24507
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
25031
24508
|
function directoryBytes(path2) {
|
|
25032
24509
|
let total = 0;
|
|
@@ -25170,18 +24647,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
25170
24647
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
|
|
25171
24648
|
});
|
|
25172
24649
|
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
24650
|
consolidateCommandNamespaces(program2);
|
|
25180
24651
|
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));
|
|
24652
|
+
program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
25185
24653
|
// Annotate the CommonJS export names for ESM import in node:
|
|
25186
24654
|
0 && (module.exports = {
|
|
25187
24655
|
DEFAULT_PRIORITY,
|