@mutmutco/cli 3.31.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 +10 -10
- package/dist/main.cjs +809 -1001
- 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(
|
|
@@ -4337,7 +4340,7 @@ function formatScratchGcPlan(plan, applied, applyResult) {
|
|
|
4337
4340
|
function scratchGcBannerLine(safeAutoPruned, advisory) {
|
|
4338
4341
|
const parts = [];
|
|
4339
4342
|
if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} scratch item(s)`);
|
|
4340
|
-
if (advisory > 0) parts.push(`${advisory} kept scratch item(s) need review \u2014 \`mmi-cli gc --scratch --dry-run\``);
|
|
4343
|
+
if (advisory > 0) parts.push(`${advisory} kept scratch item(s) need review \u2014 \`mmi-cli worktree gc --scratch --dry-run\``);
|
|
4341
4344
|
return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
|
|
4342
4345
|
}
|
|
4343
4346
|
function isLinkLike(path2, symbolic = false) {
|
|
@@ -4359,9 +4362,9 @@ function treeOlderThan(root, now, floor) {
|
|
|
4359
4362
|
if (now - st.mtimeMs <= floor) return false;
|
|
4360
4363
|
if (!st.isDirectory()) continue;
|
|
4361
4364
|
for (const ent of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
|
|
4362
|
-
const
|
|
4363
|
-
if (isLinkLike(
|
|
4364
|
-
stack.push(
|
|
4365
|
+
const child2 = (0, import_node_path4.join)(current, ent.name);
|
|
4366
|
+
if (isLinkLike(child2, ent.isSymbolicLink())) return false;
|
|
4367
|
+
stack.push(child2);
|
|
4365
4368
|
}
|
|
4366
4369
|
}
|
|
4367
4370
|
return true;
|
|
@@ -4481,16 +4484,16 @@ function rootScratchDirSnapshot(root, readdir, stat2) {
|
|
|
4481
4484
|
while (stack.length) {
|
|
4482
4485
|
const current = stack.pop();
|
|
4483
4486
|
for (const ent of readdir(current, { withFileTypes: true })) {
|
|
4484
|
-
const
|
|
4487
|
+
const child2 = (0, import_node_path4.join)(current, ent.name);
|
|
4485
4488
|
if (ent.isSymbolicLink?.()) return null;
|
|
4486
4489
|
try {
|
|
4487
|
-
(0, import_node_fs6.readlinkSync)(
|
|
4490
|
+
(0, import_node_fs6.readlinkSync)(child2);
|
|
4488
4491
|
return null;
|
|
4489
4492
|
} catch {
|
|
4490
4493
|
}
|
|
4491
|
-
const st = stat2(
|
|
4494
|
+
const st = stat2(child2);
|
|
4492
4495
|
mtimeMs = Math.max(mtimeMs, st.mtimeMs);
|
|
4493
|
-
if (ent.isDirectory()) stack.push(
|
|
4496
|
+
if (ent.isDirectory()) stack.push(child2);
|
|
4494
4497
|
else if (ent.isFile()) bytes += st.size;
|
|
4495
4498
|
}
|
|
4496
4499
|
}
|
|
@@ -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
|
}
|
|
@@ -5156,7 +5143,7 @@ var import_node_fs10 = require("node:fs");
|
|
|
5156
5143
|
|
|
5157
5144
|
// src/gc.ts
|
|
5158
5145
|
var import_node_path7 = require("node:path");
|
|
5159
|
-
var DEFERRED_SWEEP_COMMAND = "mmi-cli gc sweep-deferred";
|
|
5146
|
+
var DEFERRED_SWEEP_COMMAND = "mmi-cli worktree gc sweep-deferred";
|
|
5160
5147
|
var DEFERRED_NOTE = "Worktree cleanup queued (IDE lock). Detached sweep will retry automatically \u2014 no human action required.";
|
|
5161
5148
|
var PRESERVED_WORKTREE_CONFIG = "mmi.preservedWorktreeBranch";
|
|
5162
5149
|
var WORKTREE_LOCK_RE = /EPERM|EBUSY|EACCES|ENOTEMPTY|permission denied|access is denied|used by another process|resource busy|directory not empty/i;
|
|
@@ -5858,11 +5845,11 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
|
|
|
5858
5845
|
return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
|
|
5859
5846
|
}
|
|
5860
5847
|
function isPathUnderDirectory(childPath, parentPath) {
|
|
5861
|
-
const
|
|
5848
|
+
const child2 = normPath(childPath);
|
|
5862
5849
|
const parent = normPath(parentPath);
|
|
5863
|
-
if (!
|
|
5864
|
-
if (
|
|
5865
|
-
return
|
|
5850
|
+
if (!child2 || !parent) return false;
|
|
5851
|
+
if (child2 === parent) return true;
|
|
5852
|
+
return child2.startsWith(`${parent}/`);
|
|
5866
5853
|
}
|
|
5867
5854
|
function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
|
|
5868
5855
|
if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
|
|
@@ -6113,7 +6100,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
6113
6100
|
return report;
|
|
6114
6101
|
}
|
|
6115
6102
|
function formatGcPlan(plan, apply) {
|
|
6116
|
-
const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
|
|
6103
|
+
const lines = [`mmi-cli worktree gc: ${apply ? "apply" : "dry-run"}`];
|
|
6117
6104
|
if (!plan.branches.length && !plan.trackingRefs.length && !plan.worktreeDirs.length) lines.push("nothing to clean");
|
|
6118
6105
|
if (plan.branches.length) {
|
|
6119
6106
|
lines.push("local branches:");
|
|
@@ -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";
|
|
@@ -6579,10 +6563,10 @@ async function secretsCapabilities(deps, opts) {
|
|
|
6579
6563
|
} catch (e) {
|
|
6580
6564
|
const message = e.message;
|
|
6581
6565
|
if (isTimeoutError(e)) {
|
|
6582
|
-
deps.err(`access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
|
|
6566
|
+
deps.err(`org access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
|
|
6583
6567
|
return;
|
|
6584
6568
|
}
|
|
6585
|
-
deps.err(`access capabilities: ${message}`);
|
|
6569
|
+
deps.err(`org access capabilities: ${message}`);
|
|
6586
6570
|
return;
|
|
6587
6571
|
}
|
|
6588
6572
|
if (!res.ok) {
|
|
@@ -6591,10 +6575,10 @@ async function secretsCapabilities(deps, opts) {
|
|
|
6591
6575
|
deps.err(upgrade);
|
|
6592
6576
|
} else if (res.status === 404) {
|
|
6593
6577
|
deps.err(
|
|
6594
|
-
"access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
|
|
6578
|
+
"org access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
|
|
6595
6579
|
);
|
|
6596
6580
|
} else {
|
|
6597
|
-
deps.err(`access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
|
|
6581
|
+
deps.err(`org access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
|
|
6598
6582
|
}
|
|
6599
6583
|
return;
|
|
6600
6584
|
}
|
|
@@ -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;
|
|
@@ -7644,9 +7629,9 @@ function resolveBoardConfig(cfg) {
|
|
|
7644
7629
|
const gap = diagnoseBoardConfigGap(cfg);
|
|
7645
7630
|
if (gap) {
|
|
7646
7631
|
throw gap.kind === "no-project-detected" ? new Error(
|
|
7647
|
-
"no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli project get <owner/repo>` to check registration"
|
|
7632
|
+
"no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli org project get <owner/repo>` to check registration"
|
|
7648
7633
|
) : new Error(
|
|
7649
|
-
`Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
|
|
7634
|
+
`Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli org project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
|
|
7650
7635
|
);
|
|
7651
7636
|
}
|
|
7652
7637
|
return {
|
|
@@ -9120,7 +9105,7 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
|
|
|
9120
9105
|
message: [
|
|
9121
9106
|
`${staleNote} and are not green (${checks}).`,
|
|
9122
9107
|
"Fix the checks or use `mmi-cli pr land` so the CLI waits.",
|
|
9123
|
-
"Then correct registry META: `mmi-cli project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
|
|
9108
|
+
"Then correct registry META: `mmi-cli org project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
|
|
9124
9109
|
].join(" ")
|
|
9125
9110
|
};
|
|
9126
9111
|
}
|
|
@@ -9961,7 +9946,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
9961
9946
|
ok: false,
|
|
9962
9947
|
label: "registry no-ci contradicts PR workflows",
|
|
9963
9948
|
detail: `registry META declares no-ci but pull_request workflows emit [${emitted.join(", ")}]`,
|
|
9964
|
-
remediation: `Fix the workflows or correct registry META: mmi-cli project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
|
|
9949
|
+
remediation: `Fix the workflows or correct registry META: mmi-cli org project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
|
|
9965
9950
|
});
|
|
9966
9951
|
}
|
|
9967
9952
|
}
|
|
@@ -10022,7 +10007,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
10022
10007
|
ok: explicitNoCi,
|
|
10023
10008
|
label: "registry META declares intentional no-ci",
|
|
10024
10009
|
detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
|
|
10025
|
-
remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
|
|
10010
|
+
remediation: `mmi-cli org project set ${repo} --var ci=none --var requiredChecks=[]`
|
|
10026
10011
|
});
|
|
10027
10012
|
} else if (deployableGated) {
|
|
10028
10013
|
checks.push({
|
|
@@ -10060,7 +10045,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
10060
10045
|
ok: trackExpectsRc === rcPresent,
|
|
10061
10046
|
label: "release track matches branch topology",
|
|
10062
10047
|
detail: trackExpectsRc === rcPresent ? void 0 : `registry resolves '${effectiveTrack}' but the rc branch ${rcPresent ? "exists" : "is absent"} \u2014 set release track '${wantTrack}'`,
|
|
10063
|
-
remediation: trackExpectsRc === rcPresent ? void 0 : `mmi-cli project set ${repo} --release-track ${wantTrack}`
|
|
10048
|
+
remediation: trackExpectsRc === rcPresent ? void 0 : `mmi-cli org project set ${repo} --release-track ${wantTrack}`
|
|
10064
10049
|
});
|
|
10065
10050
|
}
|
|
10066
10051
|
}
|
|
@@ -10185,7 +10170,7 @@ async function seedGateYml(repo, deps, meta, result) {
|
|
|
10185
10170
|
}
|
|
10186
10171
|
const gate = meta?.gate;
|
|
10187
10172
|
if (!gate || typeof gate === "object" && Object.keys(gate).length === 0) {
|
|
10188
|
-
result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli project set ${repo} --var gate={...}\` first, then re-run reconcile`);
|
|
10173
|
+
result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli org project set ${repo} --var gate={...}\` first, then re-run reconcile`);
|
|
10189
10174
|
return;
|
|
10190
10175
|
}
|
|
10191
10176
|
const derivedVars = withDerivedRepoVars({ ...gateConfigToVars(gate), REPO_SLUG: parsed.slug }, parsed, "deployable", releaseTrack);
|
|
@@ -10386,20 +10371,27 @@ 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) => {
|
|
10393
10385
|
if (settled) return;
|
|
10394
10386
|
settled = true;
|
|
10395
10387
|
clearTimeout(timer);
|
|
10396
|
-
|
|
10397
|
-
|
|
10388
|
+
child2.removeAllListeners("error");
|
|
10389
|
+
child2.removeAllListeners("exit");
|
|
10398
10390
|
fn();
|
|
10399
10391
|
};
|
|
10400
10392
|
const timer = setTimeout(() => finish(resolve5), graceMs);
|
|
10401
|
-
|
|
10402
|
-
|
|
10393
|
+
child2.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
|
|
10394
|
+
child2.on("exit", (code, signal) => {
|
|
10403
10395
|
const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
|
|
10404
10396
|
finish(() => reject(new Error(`stage process exited before health check (${detail})`)));
|
|
10405
10397
|
});
|
|
@@ -10534,9 +10526,9 @@ function parseStagePortFlag(raw) {
|
|
|
10534
10526
|
return port;
|
|
10535
10527
|
}
|
|
10536
10528
|
function pathUnder(childPath, parentPath) {
|
|
10537
|
-
const
|
|
10529
|
+
const child2 = normPath2(childPath);
|
|
10538
10530
|
const parent = normPath2(parentPath);
|
|
10539
|
-
return Boolean(
|
|
10531
|
+
return Boolean(child2 && parent && (child2 === parent || child2.startsWith(`${parent}/`)));
|
|
10540
10532
|
}
|
|
10541
10533
|
function stageStateMatchesRequiredCwd(state, requiredCwd) {
|
|
10542
10534
|
if (!state) return false;
|
|
@@ -10890,7 +10882,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10890
10882
|
let up = sub(config.up.trim());
|
|
10891
10883
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
10892
10884
|
const identity = await resolveStageIdentity(cwd);
|
|
10893
|
-
const
|
|
10885
|
+
const child2 = (0, import_node_child_process7.spawn)(up, {
|
|
10894
10886
|
cwd,
|
|
10895
10887
|
shell: true,
|
|
10896
10888
|
// POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
|
|
@@ -10903,7 +10895,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10903
10895
|
env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
|
|
10904
10896
|
});
|
|
10905
10897
|
const state = {
|
|
10906
|
-
pid:
|
|
10898
|
+
pid: child2.pid ?? 0,
|
|
10907
10899
|
command: up,
|
|
10908
10900
|
cwd,
|
|
10909
10901
|
statePath,
|
|
@@ -10917,7 +10909,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10917
10909
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, state);
|
|
10918
10910
|
try {
|
|
10919
10911
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
10920
|
-
else await waitForProcessStability(
|
|
10912
|
+
else await waitForProcessStability(child2);
|
|
10921
10913
|
} catch (e) {
|
|
10922
10914
|
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd);
|
|
10923
10915
|
throw e;
|
|
@@ -10932,7 +10924,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
10932
10924
|
message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}`
|
|
10933
10925
|
};
|
|
10934
10926
|
opts.onReady?.(result);
|
|
10935
|
-
|
|
10927
|
+
child2.unref();
|
|
10936
10928
|
return result;
|
|
10937
10929
|
}
|
|
10938
10930
|
async function runStage(config = {}, opts = {}) {
|
|
@@ -11149,7 +11141,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
11149
11141
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
11150
11142
|
} };
|
|
11151
11143
|
const write = deps.write ?? import_promises.writeFile;
|
|
11152
|
-
const
|
|
11144
|
+
const remove2 = deps.remove ?? import_promises.unlink;
|
|
11153
11145
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
11154
11146
|
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
11155
11147
|
const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
@@ -11160,7 +11152,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
11160
11152
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
11161
11153
|
cleanup: async () => {
|
|
11162
11154
|
try {
|
|
11163
|
-
await
|
|
11155
|
+
await remove2(file);
|
|
11164
11156
|
} catch {
|
|
11165
11157
|
}
|
|
11166
11158
|
}
|
|
@@ -11375,9 +11367,9 @@ async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
|
11375
11367
|
}
|
|
11376
11368
|
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
11377
11369
|
const parent = parseIssueRef(parentRef);
|
|
11378
|
-
const
|
|
11370
|
+
const child2 = parseIssueRef(childRef);
|
|
11379
11371
|
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
11380
|
-
const subIssueId = await resolveIssueNodeId(runGh,
|
|
11372
|
+
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
11381
11373
|
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
11382
11374
|
const result = parseAddSubIssueResult(stdout);
|
|
11383
11375
|
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
@@ -11412,9 +11404,9 @@ function parseRemoveSubIssueResult(stdout) {
|
|
|
11412
11404
|
}
|
|
11413
11405
|
async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
11414
11406
|
const parent = parseIssueRef(parentRef);
|
|
11415
|
-
const
|
|
11407
|
+
const child2 = parseIssueRef(childRef);
|
|
11416
11408
|
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
11417
|
-
const subIssueId = await resolveIssueNodeId(runGh,
|
|
11409
|
+
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
11418
11410
|
const stdout = await runGh(buildRemoveSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
11419
11411
|
const result = parseRemoveSubIssueResult(stdout);
|
|
11420
11412
|
if (!result) throw new Error(`removeSubIssue returned an unexpected response:
|
|
@@ -11485,6 +11477,126 @@ function applyChecklistCheck(body, query, checked) {
|
|
|
11485
11477
|
return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
|
|
11486
11478
|
}
|
|
11487
11479
|
|
|
11480
|
+
// src/command-taxonomy.ts
|
|
11481
|
+
var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata");
|
|
11482
|
+
var PRIMARY_GROUPS = [
|
|
11483
|
+
["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
|
|
11484
|
+
["Plan and work", ["board", "issue", "worktree", "stage"]],
|
|
11485
|
+
["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
|
|
11486
|
+
["Setup and support", ["bootstrap", "secrets", "docs"]],
|
|
11487
|
+
["Coordinate and improve", ["wave", "report", "skill-lesson"]]
|
|
11488
|
+
];
|
|
11489
|
+
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
|
|
11490
|
+
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "wave", "report", "skill-lesson"]);
|
|
11491
|
+
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
11492
|
+
var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
|
|
11493
|
+
var topLevelPosition = 0;
|
|
11494
|
+
for (const [helpGroup, names] of PRIMARY_GROUPS) {
|
|
11495
|
+
HELP_GROUP_ORDER.set(helpGroup, HELP_GROUP_ORDER.size);
|
|
11496
|
+
for (const name of names) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
|
|
11497
|
+
}
|
|
11498
|
+
HELP_GROUP_ORDER.set("Operations", HELP_GROUP_ORDER.size);
|
|
11499
|
+
for (const name of OPERATIONAL_TOP_LEVEL) TOP_LEVEL_ORDER.set(name, topLevelPosition++);
|
|
11500
|
+
var PATH_OVERRIDES = {
|
|
11501
|
+
"board slice-refresh": { category: "internal", discovery: "hidden", help_group: "Operations" },
|
|
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" }
|
|
11508
|
+
};
|
|
11509
|
+
var HIDDEN_OPTIONS = {
|
|
11510
|
+
"worktree create": ["--base"],
|
|
11511
|
+
"org project set": ["--set"]
|
|
11512
|
+
};
|
|
11513
|
+
function primaryMetadata(name) {
|
|
11514
|
+
for (const [helpGroup, names] of PRIMARY_GROUPS) {
|
|
11515
|
+
if (names.includes(name)) {
|
|
11516
|
+
return {
|
|
11517
|
+
category: SUPPORT_PRIMARY.has(name) ? "support" : "core",
|
|
11518
|
+
discovery: "primary",
|
|
11519
|
+
help_group: helpGroup
|
|
11520
|
+
};
|
|
11521
|
+
}
|
|
11522
|
+
}
|
|
11523
|
+
return void 0;
|
|
11524
|
+
}
|
|
11525
|
+
function topLevelMetadata(name) {
|
|
11526
|
+
const primary = primaryMetadata(name);
|
|
11527
|
+
if (primary) return primary;
|
|
11528
|
+
if (!OPERATIONAL_TOP_LEVEL.has(name)) {
|
|
11529
|
+
throw new Error(`command taxonomy: unclassified top-level command "${name}"`);
|
|
11530
|
+
}
|
|
11531
|
+
return {
|
|
11532
|
+
category: name === "plugin" ? "internal" : "admin",
|
|
11533
|
+
discovery: "all-only",
|
|
11534
|
+
help_group: "Operations"
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11537
|
+
function setCommandMetadata(command, metadata) {
|
|
11538
|
+
command[COMMAND_METADATA] = metadata;
|
|
11539
|
+
}
|
|
11540
|
+
function commandMetadata(command) {
|
|
11541
|
+
return command[COMMAND_METADATA];
|
|
11542
|
+
}
|
|
11543
|
+
function classifyTree(command, path2, inherited) {
|
|
11544
|
+
const metadata = PATH_OVERRIDES[path2] ?? inherited;
|
|
11545
|
+
setCommandMetadata(command, metadata);
|
|
11546
|
+
if (metadata.discovery !== "primary") {
|
|
11547
|
+
command._hidden = true;
|
|
11548
|
+
}
|
|
11549
|
+
for (const option of command.options) {
|
|
11550
|
+
if ((HIDDEN_OPTIONS[path2] ?? []).includes(option.long ?? option.short ?? "")) option.hideHelp();
|
|
11551
|
+
}
|
|
11552
|
+
for (const child2 of command.commands) {
|
|
11553
|
+
const childPath = path2 ? `${path2} ${child2.name()}` : child2.name();
|
|
11554
|
+
classifyTree(child2, childPath, PATH_OVERRIDES[childPath] ?? metadata);
|
|
11555
|
+
}
|
|
11556
|
+
}
|
|
11557
|
+
function applyCommandTaxonomy(program3) {
|
|
11558
|
+
for (const command of program3.commands) {
|
|
11559
|
+
const metadata = topLevelMetadata(command.name());
|
|
11560
|
+
command.helpGroup(metadata.help_group);
|
|
11561
|
+
classifyTree(command, command.name(), metadata);
|
|
11562
|
+
}
|
|
11563
|
+
program3.configureHelp({
|
|
11564
|
+
visibleCommands(command) {
|
|
11565
|
+
const visible = command.commands.filter((child2) => !child2._hidden);
|
|
11566
|
+
const helpCommand = command._getHelpCommand();
|
|
11567
|
+
if (helpCommand && !helpCommand._hidden) visible.push(helpCommand);
|
|
11568
|
+
if (command === program3) visible.sort((a, b) => commandTaxonomyRank(a.name()) - commandTaxonomyRank(b.name()));
|
|
11569
|
+
return visible;
|
|
11570
|
+
},
|
|
11571
|
+
groupItems(unsortedItems, visibleItems, getGroup) {
|
|
11572
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11573
|
+
for (const item of unsortedItems) {
|
|
11574
|
+
const group = getGroup(item);
|
|
11575
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
11576
|
+
}
|
|
11577
|
+
for (const item of visibleItems) {
|
|
11578
|
+
const group = getGroup(item);
|
|
11579
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
11580
|
+
groups.get(group).push(item);
|
|
11581
|
+
}
|
|
11582
|
+
return new Map([...groups].sort(([left], [right]) => {
|
|
11583
|
+
const leftRank = HELP_GROUP_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER;
|
|
11584
|
+
const rightRank = HELP_GROUP_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER;
|
|
11585
|
+
return leftRank - rightRank;
|
|
11586
|
+
}));
|
|
11587
|
+
}
|
|
11588
|
+
});
|
|
11589
|
+
}
|
|
11590
|
+
function commandTaxonomyRank(name) {
|
|
11591
|
+
return TOP_LEVEL_ORDER.get(name) ?? Number.MAX_SAFE_INTEGER;
|
|
11592
|
+
}
|
|
11593
|
+
function commandHelpGroupRank(group) {
|
|
11594
|
+
return HELP_GROUP_ORDER.get(group) ?? Number.MAX_SAFE_INTEGER;
|
|
11595
|
+
}
|
|
11596
|
+
function isCanonicalSuggestion(metadata) {
|
|
11597
|
+
return metadata.category !== "internal";
|
|
11598
|
+
}
|
|
11599
|
+
|
|
11488
11600
|
// src/command-manifest.ts
|
|
11489
11601
|
var EXAMPLES = /* @__PURE__ */ Symbol.for("mmi.commandManifest.examples");
|
|
11490
11602
|
var COMMON_MISTAKES = /* @__PURE__ */ Symbol.for("mmi.commandManifest.commonMistakes");
|
|
@@ -11524,17 +11636,24 @@ function buildOption(opt) {
|
|
|
11524
11636
|
if (opt.description) out.description = opt.description;
|
|
11525
11637
|
if (opt.defaultValue !== void 0) out.default = opt.defaultValue;
|
|
11526
11638
|
if (opt.argChoices && opt.argChoices.length) out.enum = [...opt.argChoices];
|
|
11639
|
+
if (opt.hidden) out.discovery = "all-only";
|
|
11527
11640
|
return out;
|
|
11528
11641
|
}
|
|
11529
11642
|
function buildCommand(cmd, path2) {
|
|
11643
|
+
const metadata = commandMetadata(cmd) ?? {
|
|
11644
|
+
category: "core",
|
|
11645
|
+
discovery: "primary",
|
|
11646
|
+
help_group: "Plan and work"
|
|
11647
|
+
};
|
|
11530
11648
|
const out = {
|
|
11531
11649
|
name: cmd.name(),
|
|
11532
11650
|
path: path2,
|
|
11533
11651
|
arguments: cmd.registeredArguments.map(buildArgument),
|
|
11534
11652
|
options: cmd.options.map(buildOption),
|
|
11535
11653
|
subcommands: cmd.commands.map(
|
|
11536
|
-
(
|
|
11537
|
-
)
|
|
11654
|
+
(child2) => buildCommand(child2, path2 ? `${path2} ${child2.name()}` : child2.name())
|
|
11655
|
+
),
|
|
11656
|
+
...metadata
|
|
11538
11657
|
};
|
|
11539
11658
|
const description = cmd.description();
|
|
11540
11659
|
if (description) out.description = description;
|
|
@@ -11544,42 +11663,146 @@ function buildCommand(cmd, path2) {
|
|
|
11544
11663
|
if (commonMistakes) out.common_mistakes = commonMistakes;
|
|
11545
11664
|
return out;
|
|
11546
11665
|
}
|
|
11666
|
+
function primaryCopy(node, root = false) {
|
|
11667
|
+
if (!root && node.discovery !== "primary") return void 0;
|
|
11668
|
+
const subcommands = node.subcommands.map((child2) => primaryCopy(child2)).filter((child2) => Boolean(child2));
|
|
11669
|
+
if (root) subcommands.sort((a, b) => commandTaxonomyRank(a.name) - commandTaxonomyRank(b.name));
|
|
11670
|
+
return {
|
|
11671
|
+
...node,
|
|
11672
|
+
options: node.options.filter((option) => option.discovery !== "all-only" && option.discovery !== "hidden"),
|
|
11673
|
+
subcommands
|
|
11674
|
+
};
|
|
11675
|
+
}
|
|
11547
11676
|
function collectLeaves(node, acc) {
|
|
11548
11677
|
if (node.subcommands.length === 0) {
|
|
11549
11678
|
if (node.path) acc.push(node);
|
|
11550
11679
|
return;
|
|
11551
11680
|
}
|
|
11552
|
-
for (const
|
|
11681
|
+
for (const child2 of node.subcommands) collectLeaves(child2, acc);
|
|
11553
11682
|
}
|
|
11554
11683
|
function buildCommandManifest(program3) {
|
|
11555
11684
|
const tree = buildCommand(program3, "");
|
|
11556
11685
|
const index = [];
|
|
11557
11686
|
collectLeaves(tree, index);
|
|
11558
|
-
const
|
|
11687
|
+
const primaryTree = primaryCopy(tree, true);
|
|
11688
|
+
const primaryIndex = [];
|
|
11689
|
+
collectLeaves(primaryTree, primaryIndex);
|
|
11690
|
+
const manifest = {
|
|
11691
|
+
schema_version: 2,
|
|
11692
|
+
scope: "all",
|
|
11693
|
+
name: tree.name,
|
|
11694
|
+
tree,
|
|
11695
|
+
index,
|
|
11696
|
+
primary_tree: primaryTree,
|
|
11697
|
+
primary_index: primaryIndex,
|
|
11698
|
+
error_codes: ERROR_CODE_REFERENCE
|
|
11699
|
+
};
|
|
11559
11700
|
const version = program3.version();
|
|
11560
11701
|
if (version) manifest.version = version;
|
|
11561
11702
|
return manifest;
|
|
11562
11703
|
}
|
|
11704
|
+
function primaryCommandManifest(manifest) {
|
|
11705
|
+
return {
|
|
11706
|
+
schema_version: manifest.schema_version,
|
|
11707
|
+
scope: "primary",
|
|
11708
|
+
name: manifest.name,
|
|
11709
|
+
...manifest.version ? { version: manifest.version } : {},
|
|
11710
|
+
tree: manifest.primary_tree,
|
|
11711
|
+
index: manifest.primary_index,
|
|
11712
|
+
error_codes: manifest.error_codes
|
|
11713
|
+
};
|
|
11714
|
+
}
|
|
11563
11715
|
function argSignature(arg) {
|
|
11564
11716
|
const inner = arg.variadic ? `${arg.name}...` : arg.name;
|
|
11565
11717
|
return arg.required ? `<${inner}>` : `[${inner}]`;
|
|
11566
11718
|
}
|
|
11567
|
-
function formatManifestHuman(manifest) {
|
|
11568
|
-
const
|
|
11719
|
+
function formatManifestHuman(manifest, options = {}) {
|
|
11720
|
+
const root = options.all ? manifest.tree : manifest.primary_tree;
|
|
11721
|
+
const lines = [`${manifest.name}${manifest.version ? ` v${manifest.version}` : ""} \u2014 ${root.description ?? ""}`];
|
|
11569
11722
|
const render = (node, depth) => {
|
|
11570
11723
|
const indent = " ".repeat(depth);
|
|
11571
11724
|
const args = node.arguments.map(argSignature).join(" ");
|
|
11572
|
-
const head =
|
|
11725
|
+
const head = node.name + (args ? ` ${args}` : "");
|
|
11573
11726
|
lines.push(node.description ? `${indent}${head} \u2014 ${node.description}` : `${indent}${head}`);
|
|
11574
|
-
for (const opt of node.options) {
|
|
11727
|
+
for (const opt of options.all ? node.options : []) {
|
|
11575
11728
|
lines.push(`${indent} ${opt.flags}${opt.description ? ` ${opt.description}` : ""}`);
|
|
11576
11729
|
}
|
|
11577
|
-
for (const
|
|
11730
|
+
if (options.all) for (const child2 of node.subcommands) render(child2, depth + 1);
|
|
11578
11731
|
};
|
|
11579
|
-
|
|
11732
|
+
let currentGroup;
|
|
11733
|
+
const rootCommands = [...root.subcommands].sort((a, b) => commandHelpGroupRank(a.help_group) - commandHelpGroupRank(b.help_group) || commandTaxonomyRank(a.name) - commandTaxonomyRank(b.name));
|
|
11734
|
+
for (const command of rootCommands) {
|
|
11735
|
+
if (command.help_group !== currentGroup) {
|
|
11736
|
+
currentGroup = command.help_group;
|
|
11737
|
+
lines.push("", `${currentGroup}:`);
|
|
11738
|
+
}
|
|
11739
|
+
render(command, 1);
|
|
11740
|
+
}
|
|
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.");
|
|
11580
11742
|
return lines.join("\n");
|
|
11581
11743
|
}
|
|
11582
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
|
+
|
|
11583
11806
|
// src/version-lag.ts
|
|
11584
11807
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
11585
11808
|
var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
|
|
@@ -11722,12 +11945,12 @@ function newestMtimeMs(path2, listDir, mtimeOf) {
|
|
|
11722
11945
|
return newest;
|
|
11723
11946
|
}
|
|
11724
11947
|
for (const e of entries) {
|
|
11725
|
-
const
|
|
11948
|
+
const child2 = `${path2}/${e.name}`;
|
|
11726
11949
|
let m = 0;
|
|
11727
|
-
if (e.isDirectory) m = newestMtimeMs(
|
|
11950
|
+
if (e.isDirectory) m = newestMtimeMs(child2, listDir, mtimeOf);
|
|
11728
11951
|
else {
|
|
11729
11952
|
try {
|
|
11730
|
-
m = mtimeOf(
|
|
11953
|
+
m = mtimeOf(child2);
|
|
11731
11954
|
} catch {
|
|
11732
11955
|
m = 0;
|
|
11733
11956
|
}
|
|
@@ -11803,12 +12026,12 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
|
11803
12026
|
const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
|
|
11804
12027
|
return { cacheRoot, present: true, keep, prune, bytes, running, ...stagingFields };
|
|
11805
12028
|
}
|
|
11806
|
-
function applyPluginCachePlan(plan,
|
|
12029
|
+
function applyPluginCachePlan(plan, remove2, stagingGuard) {
|
|
11807
12030
|
const removed = [];
|
|
11808
12031
|
const failed = [];
|
|
11809
12032
|
for (const version of plan.prune) {
|
|
11810
12033
|
try {
|
|
11811
|
-
|
|
12034
|
+
remove2(`${plan.cacheRoot}/${version}`);
|
|
11812
12035
|
removed.push(version);
|
|
11813
12036
|
} catch (e) {
|
|
11814
12037
|
failed.push({ version, error: e.message });
|
|
@@ -11836,7 +12059,7 @@ function applyPluginCachePlan(plan, remove, stagingGuard) {
|
|
|
11836
12059
|
}
|
|
11837
12060
|
}
|
|
11838
12061
|
try {
|
|
11839
|
-
|
|
12062
|
+
remove2(`${plan.stagingRoot}/${s.name}`);
|
|
11840
12063
|
removedStaging.push(s.name);
|
|
11841
12064
|
} catch (e) {
|
|
11842
12065
|
failedStaging.push({ name: s.name, error: e.message });
|
|
@@ -11856,7 +12079,7 @@ var CONCURRENT_SESSION_WARNING = "only the version THIS session runs from is pro
|
|
|
11856
12079
|
function renderPluginCachePlan(plan, applied) {
|
|
11857
12080
|
const mb = (b) => `${(b / 1e6).toFixed(1)} MB`;
|
|
11858
12081
|
if (!plan.present && plan.staging.length === 0) {
|
|
11859
|
-
return `plugin
|
|
12082
|
+
return `plugin prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
|
|
11860
12083
|
}
|
|
11861
12084
|
const lines = [];
|
|
11862
12085
|
if (plan.present) {
|
|
@@ -12005,11 +12228,11 @@ function trainPlan(command, options = {}) {
|
|
|
12005
12228
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
12006
12229
|
{ label: "verify current branch is development", gated: true },
|
|
12007
12230
|
rcandVersionStep(options),
|
|
12008
|
-
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
12231
|
+
{ label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
|
|
12009
12232
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
12010
12233
|
{ label: "merge development to rc", gated: true },
|
|
12011
12234
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push, correlate/watch that run", gated: true },
|
|
12012
|
-
{ label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
|
|
12235
|
+
{ label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli runtime tenant redeploy <owner/repo> rc --watch", gated: true }
|
|
12013
12236
|
];
|
|
12014
12237
|
}
|
|
12015
12238
|
if (command === "release") {
|
|
@@ -12017,7 +12240,7 @@ function trainPlan(command, options = {}) {
|
|
|
12017
12240
|
return [
|
|
12018
12241
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
12019
12242
|
{ label: "verify current branch is development", gated: true },
|
|
12020
|
-
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
12243
|
+
{ label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
|
|
12021
12244
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
12022
12245
|
{ label: "merge development to main", gated: true },
|
|
12023
12246
|
{ label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
@@ -12031,20 +12254,20 @@ function trainPlan(command, options = {}) {
|
|
|
12031
12254
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
12032
12255
|
{ label: "verify current branch is development", gated: true },
|
|
12033
12256
|
{ label: "guard: refuse if origin/rc carries content not in development (a dev -> main release would drop it)", command: "git rev-list --count --right-only --cherry-pick --no-merges origin/development...origin/rc", gated: true },
|
|
12034
|
-
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
12257
|
+
{ label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
|
|
12035
12258
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
12036
12259
|
{ label: "merge development to main (rc skipped)", gated: true },
|
|
12037
12260
|
{ label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
12038
12261
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
12039
12262
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
|
|
12040
|
-
{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli tenant control <owner/repo> rc retire", gated: true },
|
|
12263
|
+
{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli runtime tenant control <owner/repo> rc retire", gated: true },
|
|
12041
12264
|
{ label: "roll development forward and align rc to the released main", gated: true }
|
|
12042
12265
|
];
|
|
12043
12266
|
}
|
|
12044
12267
|
return [
|
|
12045
12268
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
12046
12269
|
{ label: "verify current branch is rc", gated: true },
|
|
12047
|
-
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
12270
|
+
{ label: "verify registry META for this project", command: "mmi-cli org project get <owner/repo>", gated: true },
|
|
12048
12271
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
12049
12272
|
{ label: "verify every main-only hotfix commit is covered by the rc candidate (the guard runs automatically inside the apply step below; --ack <sha> overrides a verified, trailer-less port)", command: "mmi-cli release --apply [--ack <sha>]", gated: true },
|
|
12050
12273
|
{ label: "merge rc to main", gated: true },
|
|
@@ -12112,6 +12335,14 @@ function parseVerifySecrets(stdout) {
|
|
|
12112
12335
|
}
|
|
12113
12336
|
return out;
|
|
12114
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
|
+
}
|
|
12115
12346
|
|
|
12116
12347
|
// src/train-apply.ts
|
|
12117
12348
|
function resolveDeployModel2(meta, repo) {
|
|
@@ -12395,7 +12626,7 @@ function classifyProjectGetFailure(text) {
|
|
|
12395
12626
|
}
|
|
12396
12627
|
async function loadProjectMeta(deps, ctx) {
|
|
12397
12628
|
try {
|
|
12398
|
-
const out = await deps.runSelf(["project", "get", ctx.repo]);
|
|
12629
|
+
const out = await deps.runSelf(["org", "project", "get", ctx.repo]);
|
|
12399
12630
|
const parsed = JSON.parse(out);
|
|
12400
12631
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12401
12632
|
return { status: "read-error", error: "invalid registry META JSON" };
|
|
@@ -12415,7 +12646,7 @@ function requireProjectMetaForTrain(load, repo) {
|
|
|
12415
12646
|
}
|
|
12416
12647
|
if (load.status === "missing") {
|
|
12417
12648
|
throw new Error(
|
|
12418
|
-
`${label}: no registry META for ${repo} \u2014 train apply refused; bootstrap the project or verify META with \`mmi-cli project get\` before promoting.`
|
|
12649
|
+
`${label}: no registry META for ${repo} \u2014 train apply refused; bootstrap the project or verify META with \`mmi-cli org project get\` before promoting.`
|
|
12419
12650
|
);
|
|
12420
12651
|
}
|
|
12421
12652
|
return load.meta;
|
|
@@ -12514,6 +12745,9 @@ function correlatePublishRun(deps, since, titleIncludes) {
|
|
|
12514
12745
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
12515
12746
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
12516
12747
|
}
|
|
12748
|
+
function correlateReconcileRun(deps, since, titleIncludes) {
|
|
12749
|
+
return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
|
|
12750
|
+
}
|
|
12517
12751
|
async function correlateWorkflowRun(deps, args) {
|
|
12518
12752
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
12519
12753
|
}
|
|
@@ -12729,7 +12963,7 @@ function partialTrainRecoveryError(cause, input) {
|
|
|
12729
12963
|
partial train state: tag ${input.tag} is already pushed; origin/${branch} has not been pushed; ${releaseState}; deploy not dispatched.
|
|
12730
12964
|
Recovery sequence:
|
|
12731
12965
|
1. git push origin ${branch}` + releaseCommand + `
|
|
12732
|
-
${deployStep}. mmi-cli tenant redeploy ${input.repo} ${input.stage} --watch
|
|
12966
|
+
${deployStep}. mmi-cli runtime tenant redeploy ${input.repo} ${input.stage} --watch
|
|
12733
12967
|
Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
|
|
12734
12968
|
);
|
|
12735
12969
|
}
|
|
@@ -12911,7 +13145,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
12911
13145
|
if (dispatchFailure === "throw") throw e;
|
|
12912
13146
|
const msg = e instanceof Error ? e.message : String(e);
|
|
12913
13147
|
return {
|
|
12914
|
-
note: `tenant-deploy dispatch FAILED: ${msg}. The promotion itself landed (merge/tag/Release pushed before the dispatch) \u2014 recover the deploy with \`mmi-cli tenant redeploy ${ctx.repo} ${stage}\`; never re-tag.`,
|
|
13148
|
+
note: `tenant-deploy dispatch FAILED: ${msg}. The promotion itself landed (merge/tag/Release pushed before the dispatch) \u2014 recover the deploy with \`mmi-cli runtime tenant redeploy ${ctx.repo} ${stage}\`; never re-tag.`,
|
|
12915
13149
|
deployStatus: "failure"
|
|
12916
13150
|
};
|
|
12917
13151
|
}
|
|
@@ -13498,14 +13732,14 @@ async function attemptRetire(deps, ctx) {
|
|
|
13498
13732
|
return {
|
|
13499
13733
|
status: "retired",
|
|
13500
13734
|
category: "retired",
|
|
13501
|
-
note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
13735
|
+
note: `rc runtime retired (tenant-control.yml${r.runUrl ? `, ${r.runUrl}` : ""}) \u2014 registry coords kept; /rcand or runtime tenant redeploy recreates rc next cycle`
|
|
13502
13736
|
};
|
|
13503
13737
|
}
|
|
13504
13738
|
if (r.category === "wait-timeout") {
|
|
13505
13739
|
return {
|
|
13506
13740
|
status: "failed",
|
|
13507
13741
|
category: "wait-timeout",
|
|
13508
|
-
note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
|
|
13742
|
+
note: `rc retire dispatched but the run could not be observed \u2014 verify with: mmi-cli runtime tenant control ${ctx.repo} rc status${r.runUrl ? ` (run ${r.runUrl})` : ""}`
|
|
13509
13743
|
};
|
|
13510
13744
|
}
|
|
13511
13745
|
return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
|
|
@@ -13520,7 +13754,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
13520
13754
|
if (deployStatus !== "success") {
|
|
13521
13755
|
return {
|
|
13522
13756
|
status: "skipped",
|
|
13523
|
-
note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli tenant control ${ctx.repo} rc retire`
|
|
13757
|
+
note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli runtime tenant control ${ctx.repo} rc retire`
|
|
13524
13758
|
};
|
|
13525
13759
|
}
|
|
13526
13760
|
try {
|
|
@@ -13542,7 +13776,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
13542
13776
|
}
|
|
13543
13777
|
const f = last;
|
|
13544
13778
|
if (f.category === "wait-timeout") return f;
|
|
13545
|
-
const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli tenant sweep-rc --retire --yes)`;
|
|
13779
|
+
const note = `rc retirement failed (the release itself succeeded): ${f.note}. The rc runtime may be orphaned on the box \u2014 retire it with: mmi-cli runtime tenant control ${ctx.repo} rc retire (or sweep all: mmi-cli runtime tenant sweep-rc --retire --yes)`;
|
|
13546
13780
|
return { status: "failed", category: f.category, note };
|
|
13547
13781
|
} catch (e) {
|
|
13548
13782
|
const err = e;
|
|
@@ -13569,8 +13803,36 @@ async function runTenantRedeploy(deps, options) {
|
|
|
13569
13803
|
const d = await dispatchDeploy(deps, ctx, stage, ref, deployModel, watch);
|
|
13570
13804
|
return { ...ctx, command: "tenant-redeploy", stage, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
13571
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
|
+
}
|
|
13572
13834
|
function tenantControlWatches(action) {
|
|
13573
|
-
return action === "status" || action === "retire" || action === "verify-secrets";
|
|
13835
|
+
return action === "status" || action === "retire" || action === "verify-secrets" || action === "verify-broker";
|
|
13574
13836
|
}
|
|
13575
13837
|
async function runTenantControl(deps, options) {
|
|
13576
13838
|
const { repo, stage, action } = options;
|
|
@@ -13585,7 +13847,7 @@ async function runTenantControl(deps, options) {
|
|
|
13585
13847
|
dispatched: false,
|
|
13586
13848
|
conclusion: "failure",
|
|
13587
13849
|
category: action === "retire" ? transport ? "transport-failed" : "dispatch-rejected" : void 0,
|
|
13588
|
-
note: transport ? `tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
|
|
13850
|
+
note: transport ? `runtime tenant control ${action} dispatch failed (transport) \u2014 safe to retry` : `runtime tenant control ${action} rejected: ${d.error ?? "request rejected by the Hub"}`
|
|
13589
13851
|
};
|
|
13590
13852
|
}
|
|
13591
13853
|
const { runId, runUrl } = await correlateControlRun(deps, since, [stage, action]);
|
|
@@ -13594,20 +13856,23 @@ async function runTenantControl(deps, options) {
|
|
|
13594
13856
|
if (action === "retire") {
|
|
13595
13857
|
result.category = conclusion === "success" ? "retired" : conclusion === "failure" ? "control-run-failed" : "wait-timeout";
|
|
13596
13858
|
}
|
|
13597
|
-
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")) {
|
|
13598
13860
|
const output = extractControlOutputFromLog(await fetchControlRunLog(deps, runId));
|
|
13599
13861
|
if (action === "status") {
|
|
13600
13862
|
result.serviceState = parseStatusSnippet(output).serviceState;
|
|
13601
|
-
} else {
|
|
13863
|
+
} else if (action === "verify-secrets") {
|
|
13602
13864
|
result.secrets = parseVerifySecrets(output);
|
|
13603
13865
|
result.secretsRaw = output;
|
|
13866
|
+
} else {
|
|
13867
|
+
result.broker = parseVerifyBroker(output);
|
|
13868
|
+
result.brokerRaw = output;
|
|
13604
13869
|
}
|
|
13605
13870
|
}
|
|
13606
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`;
|
|
13607
13872
|
return result;
|
|
13608
13873
|
}
|
|
13609
13874
|
function renderTenantControl(r) {
|
|
13610
|
-
const head = `tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
|
|
13875
|
+
const head = `runtime tenant control ${r.repo} ${r.stage} ${r.action}: ${r.conclusion}${r.category ? ` (${r.category})` : ""}`;
|
|
13611
13876
|
const lines = [head];
|
|
13612
13877
|
if (r.runUrl) lines.push(` run: ${r.runUrl}`);
|
|
13613
13878
|
if (r.serviceState) lines.push(` serviceState: ${r.serviceState}`);
|
|
@@ -13650,6 +13915,26 @@ function renderVerifySecrets(body) {
|
|
|
13650
13915
|
return { lines, failure: null };
|
|
13651
13916
|
}
|
|
13652
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
|
+
|
|
13653
13938
|
// src/hotfix-coverage.ts
|
|
13654
13939
|
var import_node_child_process8 = require("node:child_process");
|
|
13655
13940
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
@@ -13768,7 +14053,7 @@ function pickRepo(p) {
|
|
|
13768
14053
|
}
|
|
13769
14054
|
async function sweepRcOrphans(deps, opts) {
|
|
13770
14055
|
const projects = await deps.listProjects();
|
|
13771
|
-
if (!projects) throw new Error("project list unavailable \u2014 Hub unreachable or this repo is not bootstrapped");
|
|
14056
|
+
if (!projects) throw new Error("org project list unavailable \u2014 Hub unreachable or this repo is not bootstrapped");
|
|
13772
14057
|
const targets = projects.filter(isRcBearingTenant);
|
|
13773
14058
|
const stages = [];
|
|
13774
14059
|
for (const p of targets) {
|
|
@@ -13805,7 +14090,7 @@ async function sweepRcOrphans(deps, opts) {
|
|
|
13805
14090
|
};
|
|
13806
14091
|
}
|
|
13807
14092
|
function renderSweep(r) {
|
|
13808
|
-
const lines = [`tenant sweep-rc: scanned ${r.scanned} tenant-container(s); ${r.running} rc runtime(s) running`];
|
|
14093
|
+
const lines = [`runtime tenant sweep-rc: scanned ${r.scanned} tenant-container(s); ${r.running} rc runtime(s) running`];
|
|
13809
14094
|
for (const s of r.stages) {
|
|
13810
14095
|
let line = ` ${s.repo} rc: ${s.orphanCandidate ? "RUNNING" : s.serviceState}`;
|
|
13811
14096
|
if (s.retired) line += ` -> ${s.retired}${s.category ? ` (${s.category}${s.reason ? `/${s.reason}` : ""})` : ""}`;
|
|
@@ -13813,7 +14098,7 @@ function renderSweep(r) {
|
|
|
13813
14098
|
lines.push(line);
|
|
13814
14099
|
}
|
|
13815
14100
|
if (r.running > 0 && !r.retireAttempted) {
|
|
13816
|
-
lines.push("Retire an orphan with: mmi-cli tenant control <owner/repo> rc retire (or sweep all running: mmi-cli tenant sweep-rc --retire --yes)");
|
|
14101
|
+
lines.push("Retire an orphan with: mmi-cli runtime tenant control <owner/repo> rc retire (or sweep all running: mmi-cli runtime tenant sweep-rc --retire --yes)");
|
|
13817
14102
|
}
|
|
13818
14103
|
const undetermined = r.stages.filter((s) => s.serviceState === "unknown").length;
|
|
13819
14104
|
if (undetermined > 0) {
|
|
@@ -14634,8 +14919,8 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
|
14634
14919
|
repo: OWNER2,
|
|
14635
14920
|
kind: "empty-target-inventory",
|
|
14636
14921
|
severity: "high",
|
|
14637
|
-
detail: "access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
|
|
14638
|
-
remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
|
|
14922
|
+
detail: "org access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
|
|
14923
|
+
remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli org access audit\`; or audit one repo explicitly with \`mmi-cli org access audit --repo <owner/repo>\``
|
|
14639
14924
|
}],
|
|
14640
14925
|
repos: []
|
|
14641
14926
|
};
|
|
@@ -14650,7 +14935,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
|
14650
14935
|
kind: "owner-baseline-unavailable",
|
|
14651
14936
|
severity: "medium",
|
|
14652
14937
|
detail: `could not resolve org owners (role=admin) from the GitHub API (${e.message}); the overgrant audit needs this baseline and was skipped to avoid false HIGH findings \u2014 re-run when the members API is reachable`,
|
|
14653
|
-
remediation: `confirm the token can read GET /orgs/${OWNER2}/members?role=admin, then re-run \`mmi-cli access audit\``
|
|
14938
|
+
remediation: `confirm the token can read GET /orgs/${OWNER2}/members?role=admin, then re-run \`mmi-cli org access audit\``
|
|
14654
14939
|
});
|
|
14655
14940
|
return { ok: degraded.every((f) => f.severity !== "high"), owners: [], orgFindings: degraded, repos: [] };
|
|
14656
14941
|
}
|
|
@@ -14672,14 +14957,10 @@ function loadAccessTargets(projectsJson) {
|
|
|
14672
14957
|
const seen = /* @__PURE__ */ new Set();
|
|
14673
14958
|
const targets = [];
|
|
14674
14959
|
for (const project2 of projects) {
|
|
14675
|
-
const embeddedContent = new Set(
|
|
14676
|
-
(project2.fanoutTargets ?? []).filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase())
|
|
14677
|
-
);
|
|
14678
14960
|
for (const repo of project2.repos ?? []) {
|
|
14679
14961
|
if (seen.has(repo)) continue;
|
|
14680
14962
|
seen.add(repo);
|
|
14681
|
-
const
|
|
14682
|
-
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";
|
|
14683
14964
|
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
14684
14965
|
targets.push({ repo, class: cls, releaseTrack });
|
|
14685
14966
|
}
|
|
@@ -14695,7 +14976,7 @@ function resolveOrgAuditTargets(registryProjects) {
|
|
|
14695
14976
|
kind: "registry-unreadable",
|
|
14696
14977
|
severity: "high",
|
|
14697
14978
|
detail: "the project registry SSOT (Hub API) was unreachable or unconfigured \u2014 the audit target set is unknown; auditing the stale committed projects.json instead could pass green while missing live repos",
|
|
14698
|
-
remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
|
|
14979
|
+
remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli org access audit\`; or audit one repo explicitly with \`mmi-cli org access audit --repo <owner/repo>\``
|
|
14699
14980
|
}
|
|
14700
14981
|
};
|
|
14701
14982
|
}
|
|
@@ -14740,7 +15021,7 @@ function dataAccessContractsFromProjects(projects) {
|
|
|
14740
15021
|
return { consumers };
|
|
14741
15022
|
}
|
|
14742
15023
|
function renderAccessReport(report) {
|
|
14743
|
-
const lines = [`mmi-cli access audit: ${report.ok ? "OK" : "CHECK"} (owners: ${report.owners.map((o) => "@" + o).join(", ") || "none"})`];
|
|
15024
|
+
const lines = [`mmi-cli org access audit: ${report.ok ? "OK" : "CHECK"} (owners: ${report.owners.map((o) => "@" + o).join(", ") || "none"})`];
|
|
14744
15025
|
for (const finding of report.orgFindings ?? []) {
|
|
14745
15026
|
lines.push(` [${finding.severity}] ${finding.kind}: ${finding.detail}`);
|
|
14746
15027
|
if (finding.remediation) lines.push(` ${finding.remediation}`);
|
|
@@ -14759,9 +15040,13 @@ function renderAccessReport(report) {
|
|
|
14759
15040
|
var import_node_fs16 = require("node:fs");
|
|
14760
15041
|
var import_node_path14 = require("node:path");
|
|
14761
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
|
+
}
|
|
14762
15047
|
var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
|
|
14763
|
-
function
|
|
14764
|
-
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();
|
|
14765
15050
|
}
|
|
14766
15051
|
function extractDocMeta(relPath, raw) {
|
|
14767
15052
|
const lines = raw.split(/\r?\n/);
|
|
@@ -14789,7 +15074,7 @@ function extractDocMeta(relPath, raw) {
|
|
|
14789
15074
|
break;
|
|
14790
15075
|
}
|
|
14791
15076
|
if (trimmed.startsWith("#")) continue;
|
|
14792
|
-
scope =
|
|
15077
|
+
scope = plainInline(trimmed);
|
|
14793
15078
|
break;
|
|
14794
15079
|
}
|
|
14795
15080
|
if (!title) {
|
|
@@ -14801,16 +15086,16 @@ function extractDocMeta(relPath, raw) {
|
|
|
14801
15086
|
function renderDocsIndex(entries) {
|
|
14802
15087
|
const lines = [
|
|
14803
15088
|
GENERATED_HEADER,
|
|
15089
|
+
"",
|
|
14804
15090
|
"# Documentation index",
|
|
14805
15091
|
"",
|
|
14806
15092
|
"Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
|
|
14807
15093
|
"can never diverge from the records it lists.",
|
|
14808
|
-
""
|
|
14809
|
-
"| Document | Scope |",
|
|
14810
|
-
"| --- | --- |"
|
|
15094
|
+
""
|
|
14811
15095
|
];
|
|
14812
15096
|
for (const entry of entries) {
|
|
14813
|
-
|
|
15097
|
+
const link = `[${plainInline(entry.title)}](${entry.path})`;
|
|
15098
|
+
lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
|
|
14814
15099
|
}
|
|
14815
15100
|
return lines.join("\n") + "\n";
|
|
14816
15101
|
}
|
|
@@ -14846,7 +15131,7 @@ function createDocsIndexDeps(repoRoot2) {
|
|
|
14846
15131
|
const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
|
|
14847
15132
|
const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
14848
15133
|
return {
|
|
14849
|
-
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(
|
|
15134
|
+
listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
|
|
14850
15135
|
readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
|
|
14851
15136
|
readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
|
|
14852
15137
|
writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
|
|
@@ -14880,20 +15165,20 @@ function docsAuditRecord(input) {
|
|
|
14880
15165
|
const date = input.date.trim();
|
|
14881
15166
|
const shaRange = input.shaRange.trim();
|
|
14882
15167
|
const checkerVendor = input.checkerVendor.trim();
|
|
14883
|
-
if (!repo) throw new Error("docs
|
|
14884
|
-
if (!date) throw new Error("docs
|
|
14885
|
-
if (!isValidIsoDate(date)) throw new Error(`docs
|
|
14886
|
-
if (!shaRange) throw new Error("docs
|
|
14887
|
-
if (!checkerVendor) throw new Error("docs
|
|
15168
|
+
if (!repo) throw new Error("docs audit record: repo is required");
|
|
15169
|
+
if (!date) throw new Error("docs audit record: date is required");
|
|
15170
|
+
if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
|
|
15171
|
+
if (!shaRange) throw new Error("docs audit record: shaRange is required");
|
|
15172
|
+
if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
|
|
14888
15173
|
if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
|
|
14889
|
-
throw new Error("docs
|
|
15174
|
+
throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
|
|
14890
15175
|
}
|
|
14891
15176
|
if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
|
|
14892
|
-
throw new Error("docs
|
|
15177
|
+
throw new Error("docs audit record: a `failed` outcome must carry a reason");
|
|
14893
15178
|
}
|
|
14894
15179
|
const outcome = serializeOutcome(input.outcome);
|
|
14895
15180
|
if (!isValidOutcomeWire(outcome)) {
|
|
14896
|
-
throw new Error(`docs
|
|
15181
|
+
throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
|
|
14897
15182
|
}
|
|
14898
15183
|
return { repo, date, shaRange, outcome, checkerVendor };
|
|
14899
15184
|
}
|
|
@@ -14904,13 +15189,13 @@ function ageInDays(verdictDate, today) {
|
|
|
14904
15189
|
}
|
|
14905
15190
|
function docsAuditStatus(fetch2, opts) {
|
|
14906
15191
|
if ("notArmed" in fetch2) {
|
|
14907
|
-
return { ok: true, state: "not-armed", line: `docs
|
|
15192
|
+
return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
|
|
14908
15193
|
}
|
|
14909
15194
|
if (!fetch2.ok) {
|
|
14910
|
-
return { ok: false, state: "error", line: `docs
|
|
15195
|
+
return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
14911
15196
|
}
|
|
14912
15197
|
if (fetch2.verdict === null) {
|
|
14913
|
-
return { ok: false, state: "missing", line: `docs
|
|
15198
|
+
return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
14914
15199
|
}
|
|
14915
15200
|
const verdict = fetch2.verdict;
|
|
14916
15201
|
for (const field of ["repo", "shaRange", "checkerVendor"]) {
|
|
@@ -14920,7 +15205,7 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
14920
15205
|
return {
|
|
14921
15206
|
ok: false,
|
|
14922
15207
|
state: "malformed",
|
|
14923
|
-
line: `docs
|
|
15208
|
+
line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
|
|
14924
15209
|
};
|
|
14925
15210
|
}
|
|
14926
15211
|
}
|
|
@@ -14928,450 +15213,29 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
14928
15213
|
return {
|
|
14929
15214
|
ok: false,
|
|
14930
15215
|
state: "malformed",
|
|
14931
|
-
line: `docs
|
|
15216
|
+
line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
|
|
14932
15217
|
};
|
|
14933
15218
|
}
|
|
14934
15219
|
if (!isValidOutcomeWire(verdict.outcome)) {
|
|
14935
15220
|
return {
|
|
14936
15221
|
ok: false,
|
|
14937
15222
|
state: "malformed",
|
|
14938
|
-
line: `docs
|
|
15223
|
+
line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
14939
15224
|
};
|
|
14940
15225
|
}
|
|
14941
15226
|
if (!isValidIsoDate(opts.today)) {
|
|
14942
|
-
throw new Error(`docs
|
|
15227
|
+
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
14943
15228
|
}
|
|
14944
15229
|
const cadence = opts.cadenceDays ?? 7;
|
|
14945
15230
|
const grace = opts.graceDays ?? 3;
|
|
14946
15231
|
const age = ageInDays(verdict.date, opts.today);
|
|
14947
15232
|
if (age > cadence + grace) {
|
|
14948
|
-
return { ok: false, state: "stale", line: `docs
|
|
15233
|
+
return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
|
|
14949
15234
|
}
|
|
14950
15235
|
if (verdict.outcome === "failed") {
|
|
14951
|
-
return { ok: false, state: "failed", line: `docs
|
|
14952
|
-
}
|
|
14953
|
-
return { ok: true, state: "clean", line: `docs-audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
14954
|
-
}
|
|
14955
|
-
|
|
14956
|
-
// src/project-readiness.ts
|
|
14957
|
-
function stagesForTrack(meta) {
|
|
14958
|
-
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
14959
|
-
}
|
|
14960
|
-
function stageInTrack(meta, stage) {
|
|
14961
|
-
return stagesForTrack(meta).includes(stage);
|
|
14962
|
-
}
|
|
14963
|
-
function dnsErrorToResolution(code) {
|
|
14964
|
-
return code === "ENOTFOUND" || code === "EAI_NONAME" ? false : void 0;
|
|
14965
|
-
}
|
|
14966
|
-
var STAGES = ["dev", "rc", "main"];
|
|
14967
|
-
var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
14968
|
-
function boardRegistryGaps(meta) {
|
|
14969
|
-
if (!meta?.projectId) return [];
|
|
14970
|
-
if (meta.projectNumber != null) return [];
|
|
14971
|
-
return ["projectNumber"];
|
|
14972
|
-
}
|
|
14973
|
-
function previewRegistryMetaMerge(existing, patch) {
|
|
14974
|
-
const out = { ...existing ?? {} };
|
|
14975
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
14976
|
-
if (value === null) delete out[key];
|
|
14977
|
-
else out[key] = value;
|
|
14978
|
-
}
|
|
14979
|
-
return out;
|
|
14980
|
-
}
|
|
14981
|
-
function boardLinkWriteError(patch, existing) {
|
|
14982
|
-
const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
|
|
14983
|
-
const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
|
|
14984
|
-
if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
|
|
14985
|
-
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
14986
|
-
}
|
|
14987
|
-
if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
|
|
14988
|
-
return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
|
|
14989
|
-
}
|
|
14990
|
-
return null;
|
|
14991
|
-
}
|
|
14992
|
-
function boardRegistryGapMessage(repo) {
|
|
14993
|
-
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)`;
|
|
14994
|
-
}
|
|
14995
|
-
var slugOfRepo = slugOf;
|
|
14996
|
-
function repoFrom(repoOrSlug, slug) {
|
|
14997
|
-
return repoOrSlug.includes("/") ? repoOrSlug : `mutmutco/${slug}`;
|
|
14998
|
-
}
|
|
14999
|
-
function defaultSubdomain(slug) {
|
|
15000
|
-
return slug.replace(/^[a-z]+-/, "");
|
|
15001
|
-
}
|
|
15002
|
-
function projectRequiresGoogleOAuth(meta, model) {
|
|
15003
|
-
if (!meta || model === "content" || model === "none" || meta.deployModel === "content" || meta.deployModel === "none" || meta.class === "content") return false;
|
|
15004
|
-
const projectType = resolveProjectType(meta, meta.repos?.[0] ?? "");
|
|
15005
|
-
if (projectType !== "web-app") return false;
|
|
15006
|
-
return Boolean(meta.oauth && typeof meta.oauth === "object");
|
|
15007
|
-
}
|
|
15008
|
-
function declaresNoPublicEdge(meta) {
|
|
15009
|
-
const ed = meta?.edgeDomains;
|
|
15010
|
-
return Boolean(ed && typeof ed === "object" && !Array.isArray(ed) && Object.keys(ed).length === 0);
|
|
15011
|
-
}
|
|
15012
|
-
function declaresWorker(meta) {
|
|
15013
|
-
return meta?.projectType === "worker";
|
|
15014
|
-
}
|
|
15015
|
-
function isCentralContainerModel(model) {
|
|
15016
|
-
return model === "tenant-container" || model === "solo-container";
|
|
15017
|
-
}
|
|
15018
|
-
function isNoEdgeTenantWorker(meta, model) {
|
|
15019
|
-
return isCentralContainerModel(model) && (declaresNoPublicEdge(meta) || declaresWorker(meta));
|
|
15020
|
-
}
|
|
15021
|
-
function projectRequiresDeployCoords(model, stage, meta) {
|
|
15022
|
-
if (!isCentralContainerModel(model)) return false;
|
|
15023
|
-
if (stage && isNoEdgeTenantWorker(meta, model)) return stage === "main";
|
|
15024
|
-
return true;
|
|
15025
|
-
}
|
|
15026
|
-
function projectRequiresDeployState(model, stage) {
|
|
15027
|
-
return model === "hub-serverless" && stage !== "dev";
|
|
15028
|
-
}
|
|
15029
|
-
function stageRequiredSecrets(stage, meta) {
|
|
15030
|
-
const contract = meta.requiredRuntimeSecrets;
|
|
15031
|
-
const extra = !Array.isArray(contract) && Array.isArray(contract?.[stage]) ? contract[stage] ?? [] : [];
|
|
15032
|
-
const model = resolveDeployModel(meta, meta.repos?.[0] ?? "");
|
|
15033
|
-
if (isNoEdgeTenantWorker(meta, model) && stage !== "main") return [];
|
|
15034
|
-
const defaults = projectRequiresGoogleOAuth(meta, model) ? DEFAULT_RUNTIME_SECRET_NAMES2 : [];
|
|
15035
|
-
return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
|
|
15036
|
-
}
|
|
15037
|
-
function stageKey2(stage, key) {
|
|
15038
|
-
return key.includes("/") ? key : `${stage}/${key}`;
|
|
15039
|
-
}
|
|
15040
|
-
function materializedRuntimeSecretName(entry) {
|
|
15041
|
-
return entry.includes(":") ? entry.split(":").pop() : entry;
|
|
15042
|
-
}
|
|
15043
|
-
function runtimeSecretStreamGap(stage, meta, presentSecrets) {
|
|
15044
|
-
const streamed = new Set(
|
|
15045
|
-
(contractByStage(meta.requiredRuntimeSecrets)[stage] ?? []).map(materializedRuntimeSecretName)
|
|
15046
|
-
);
|
|
15047
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15048
|
-
const gap = [];
|
|
15049
|
-
for (const name of stageRequiredSecrets(stage, meta).map(materializedRuntimeSecretName)) {
|
|
15050
|
-
if (seen.has(name)) continue;
|
|
15051
|
-
seen.add(name);
|
|
15052
|
-
if (!streamed.has(name) && (presentSecrets.has(stageKey2(stage, name)) || presentSecrets.has(name))) gap.push(name);
|
|
15053
|
-
}
|
|
15054
|
-
return gap;
|
|
15055
|
-
}
|
|
15056
|
-
function hasRuntimeSecretContract(contract) {
|
|
15057
|
-
if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
|
|
15058
|
-
return ["dev", "rc", "main"].some((stage) => Array.isArray(contract[stage]));
|
|
15059
|
-
}
|
|
15060
|
-
function appAttestationOf(meta) {
|
|
15061
|
-
const a = meta?.appAttested;
|
|
15062
|
-
if (!a || typeof a !== "object" || Array.isArray(a)) return null;
|
|
15063
|
-
const { at, by } = a;
|
|
15064
|
-
return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
|
|
15065
|
-
}
|
|
15066
|
-
function attestedLine(att) {
|
|
15067
|
-
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 project attest\` after app-owned structural changes.`;
|
|
15068
|
-
}
|
|
15069
|
-
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: [] }).";
|
|
15070
|
-
function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
|
|
15071
|
-
const attested = appAttestationOf(meta);
|
|
15072
|
-
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";
|
|
15073
|
-
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
15074
|
-
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
15075
|
-
if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
15076
|
-
if (projectType === "desktop-app") {
|
|
15077
|
-
return [
|
|
15078
|
-
"Desktop app: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned installer/packaging outside the tenant train.",
|
|
15079
|
-
"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."
|
|
15080
|
-
];
|
|
15081
|
-
}
|
|
15082
|
-
if (projectType === "desktop-game") {
|
|
15083
|
-
return [
|
|
15084
|
-
"Desktop game: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned release packaging outside the tenant train.",
|
|
15085
|
-
"Keep README.md and architecture.md explicit about the game build/release path so readiness does not guess web infra."
|
|
15086
|
-
];
|
|
15087
|
-
}
|
|
15088
|
-
if (projectType === "non-deployable" || model === "none") {
|
|
15089
|
-
return [
|
|
15090
|
-
"Non-deployable repo: no Hub deploy coords, Google OAuth, DWD, or tenant control by default.",
|
|
15091
|
-
"Keep README.md and architecture.md explicit about the repo purpose and any project-admin workflow it still needs."
|
|
15092
|
-
];
|
|
15093
|
-
}
|
|
15094
|
-
if (model === "hub-serverless") {
|
|
15095
|
-
return [
|
|
15096
|
-
"Hub serverless deploy stays on the Hub-owned workflows; deploy.yml stamps per-stage deploy state onto state-only DEPLOY# rows (no tenant coords).",
|
|
15097
|
-
"Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults."
|
|
15098
|
-
];
|
|
15099
|
-
}
|
|
15100
|
-
if (model === "serverless") {
|
|
15101
|
-
return [
|
|
15102
|
-
"Serverless deploys use the central serverless path; do not add tenant compose or box plumbing.",
|
|
15103
|
-
"Make runtime config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
15104
|
-
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
15105
|
-
];
|
|
15106
|
-
}
|
|
15107
|
-
if (projectType === "cli-tool" || model === "registry-publish") {
|
|
15108
|
-
return [
|
|
15109
|
-
"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.",
|
|
15110
|
-
"Keep the published version in lockstep with the release tag; make the publish idempotent (skip when the version is already on the registry).",
|
|
15111
|
-
"Keep app-owned README.md and architecture.md aligned with the registry-publish release path."
|
|
15112
|
-
];
|
|
15113
|
-
}
|
|
15114
|
-
const gaps = [
|
|
15115
|
-
'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.',
|
|
15116
|
-
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
15117
|
-
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
15118
|
-
];
|
|
15119
|
-
if (isNoEdgeTenantWorker(meta, model)) {
|
|
15120
|
-
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.");
|
|
15121
|
-
}
|
|
15122
|
-
if (contractUndeclared) {
|
|
15123
|
-
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
15124
|
-
}
|
|
15125
|
-
if (slug === "mmi-katip") {
|
|
15126
|
-
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.");
|
|
15127
|
-
}
|
|
15128
|
-
if (model === "solo-container" || model === "tenant-container") {
|
|
15129
|
-
if (typeof mainDeployFact?.port === "number") {
|
|
15130
|
-
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).`);
|
|
15131
|
-
} else if (!appAttestationOf(meta)) {
|
|
15132
|
-
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.");
|
|
15133
|
-
}
|
|
15134
|
-
}
|
|
15135
|
-
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
15136
|
-
return gaps;
|
|
15137
|
-
}
|
|
15138
|
-
function edgeDomainsByStage(meta) {
|
|
15139
|
-
const ed = meta?.edgeDomains;
|
|
15140
|
-
if (!ed || typeof ed !== "object" || Array.isArray(ed)) return {};
|
|
15141
|
-
const out = {};
|
|
15142
|
-
for (const stage of STAGES) {
|
|
15143
|
-
const v = ed[stage];
|
|
15144
|
-
if (typeof v === "string" && v.trim()) out[stage] = v.trim();
|
|
15145
|
-
}
|
|
15146
|
-
return out;
|
|
15147
|
-
}
|
|
15148
|
-
async function probeEdgeDomains(meta, resolveDns) {
|
|
15149
|
-
const byStage = edgeDomainsByStage(meta);
|
|
15150
|
-
const results = await Promise.all(
|
|
15151
|
-
Object.entries(byStage).map(async ([stage, host]) => {
|
|
15152
|
-
let resolved;
|
|
15153
|
-
try {
|
|
15154
|
-
resolved = await resolveDns(host);
|
|
15155
|
-
} catch {
|
|
15156
|
-
resolved = void 0;
|
|
15157
|
-
}
|
|
15158
|
-
return resolved === false ? { stage, host } : void 0;
|
|
15159
|
-
})
|
|
15160
|
-
);
|
|
15161
|
-
return results.filter((r) => r !== void 0);
|
|
15162
|
-
}
|
|
15163
|
-
function contractByStage(contract) {
|
|
15164
|
-
return contract && !Array.isArray(contract) ? contract : {};
|
|
15165
|
-
}
|
|
15166
|
-
function ensureStageNames(names, required) {
|
|
15167
|
-
return [.../* @__PURE__ */ new Set([...names ?? [], ...required])];
|
|
15168
|
-
}
|
|
15169
|
-
function sameNames(a, b) {
|
|
15170
|
-
const left = new Set(a ?? []);
|
|
15171
|
-
const right = new Set(b ?? []);
|
|
15172
|
-
if (left.size !== right.size) return false;
|
|
15173
|
-
return [...left].every((name) => right.has(name));
|
|
15174
|
-
}
|
|
15175
|
-
function sameStageContract(a, b) {
|
|
15176
|
-
return STAGES.every((stage) => sameNames(a[stage], b[stage]));
|
|
15177
|
-
}
|
|
15178
|
-
function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
|
|
15179
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15180
|
-
const repo = repoFrom(repoOrSlug, slug);
|
|
15181
|
-
const sub = defaultSubdomain(slug);
|
|
15182
|
-
const patch = {};
|
|
15183
|
-
const confidentType = resolveProjectTypeConfident(meta, repo);
|
|
15184
|
-
const projectType = confidentType ?? resolveProjectType(meta, repo);
|
|
15185
|
-
const model = resolveDeployModel(meta, repo);
|
|
15186
|
-
if (confidentType && !meta?.class) patch.class = confidentType === "content" ? "content" : "deployable";
|
|
15187
|
-
if (confidentType) {
|
|
15188
|
-
if (!meta?.projectType) patch.projectType = confidentType;
|
|
15189
|
-
if (!meta?.deployModel) patch.deployModel = model;
|
|
15190
|
-
}
|
|
15191
|
-
if (!meta?.vaultPath) patch.vaultPath = `/mmi-future/${slug}`;
|
|
15192
|
-
if (confidentType && !meta?.edgeDomains && model === "tenant-container" && projectType !== "worker") {
|
|
15193
|
-
patch.edgeDomains = {
|
|
15194
|
-
dev: `dev.${sub}.mutatismutandis.co`,
|
|
15195
|
-
rc: `rc.${sub}.mutatismutandis.co`,
|
|
15196
|
-
main: `${sub}.mutatismutandis.co`
|
|
15197
|
-
};
|
|
15236
|
+
return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
|
|
15198
15237
|
}
|
|
15199
|
-
|
|
15200
|
-
if (meta.oauth !== void 0) patch.oauth = null;
|
|
15201
|
-
if (meta.edgeDomains !== void 0) patch.edgeDomains = null;
|
|
15202
|
-
}
|
|
15203
|
-
if (slug === "mmi-katip") {
|
|
15204
|
-
const required = ["GOOGLE_IMPERSONATE_USER", "GOOGLE_ADMIN_USER"];
|
|
15205
|
-
const current = contractByStage(meta?.requiredRuntimeSecrets);
|
|
15206
|
-
const next = {
|
|
15207
|
-
dev: ensureStageNames(current.dev, required),
|
|
15208
|
-
rc: ensureStageNames(current.rc, required),
|
|
15209
|
-
main: ensureStageNames(current.main, required)
|
|
15210
|
-
};
|
|
15211
|
-
if (!sameStageContract(current, next)) {
|
|
15212
|
-
patch.requiredRuntimeSecrets = next;
|
|
15213
|
-
}
|
|
15214
|
-
}
|
|
15215
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-app|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
15216
|
-
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
15217
|
-
return { slug, patch, appOwnedGaps };
|
|
15218
|
-
}
|
|
15219
|
-
async function runV2Heal(repoOrSlug, opts, deps) {
|
|
15220
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15221
|
-
const read = await deps.fetchProject(slug);
|
|
15222
|
-
if (!read.ok) {
|
|
15223
|
-
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` };
|
|
15224
|
-
}
|
|
15225
|
-
const plan = buildV2HealPatch(repoOrSlug, read.project);
|
|
15226
|
-
if (!opts.apply) return { status: "planned", slug, patch: plan.patch, appOwnedGaps: plan.appOwnedGaps };
|
|
15227
|
-
if (!Object.keys(plan.patch).length) return { status: "noop", slug, appOwnedGaps: plan.appOwnedGaps };
|
|
15228
|
-
const write = await deps.upsertProject(slug, plan.patch);
|
|
15229
|
-
if (!write.ok) return { status: "write-failed", slug, write };
|
|
15230
|
-
return { status: "applied", slug, applied: Object.keys(plan.patch), appOwnedGaps: plan.appOwnedGaps, result: write.body };
|
|
15231
|
-
}
|
|
15232
|
-
async function buildV2Doctor(repoOrSlug, deps) {
|
|
15233
|
-
const slug = slugOfRepo(repoOrSlug);
|
|
15234
|
-
const repo = repoFrom(repoOrSlug, slug);
|
|
15235
|
-
const read = await deps.getProject(slug);
|
|
15236
|
-
if (!read.ok) {
|
|
15237
|
-
const degradedSecrets = {
|
|
15238
|
-
dev: { required: [], present: [], missing: [] },
|
|
15239
|
-
rc: { required: [], present: [], missing: [] },
|
|
15240
|
-
main: { required: [], present: [], missing: [] }
|
|
15241
|
-
};
|
|
15242
|
-
const degradedStage = {
|
|
15243
|
-
dev: { ok: false, required: false },
|
|
15244
|
-
rc: { ok: false, required: false },
|
|
15245
|
-
main: { ok: false, required: false }
|
|
15246
|
-
};
|
|
15247
|
-
return {
|
|
15248
|
-
ok: false,
|
|
15249
|
-
repo,
|
|
15250
|
-
slug,
|
|
15251
|
-
registryError: read.error,
|
|
15252
|
-
hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
|
|
15253
|
-
autoHealAvailable: [],
|
|
15254
|
-
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 project doctor\` shortly.`]
|
|
15255
|
-
};
|
|
15256
|
-
}
|
|
15257
|
-
const meta = read.project;
|
|
15258
|
-
const projectType = resolveProjectType(meta, repo);
|
|
15259
|
-
const model = resolveDeployModel(meta, repo);
|
|
15260
|
-
const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
|
|
15261
|
-
const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
|
|
15262
|
-
if (!meta) {
|
|
15263
|
-
const emptySecrets = {
|
|
15264
|
-
dev: { required: [], present: [], missing: [] },
|
|
15265
|
-
rc: { required: [], present: [], missing: [] },
|
|
15266
|
-
main: { required: [], present: [], missing: [] }
|
|
15267
|
-
};
|
|
15268
|
-
const emptyCoords = {
|
|
15269
|
-
dev: { ok: false, required: true },
|
|
15270
|
-
rc: { ok: false, required: true },
|
|
15271
|
-
main: { ok: false, required: true }
|
|
15272
|
-
};
|
|
15273
|
-
const emptyState = Object.fromEntries(STAGES.map((stage) => {
|
|
15274
|
-
const required = projectRequiresDeployState(model, stage);
|
|
15275
|
-
return [stage, { required, ok: !required }];
|
|
15276
|
-
}));
|
|
15277
|
-
return {
|
|
15278
|
-
ok: false,
|
|
15279
|
-
repo,
|
|
15280
|
-
slug,
|
|
15281
|
-
hubOwned: { meta: { ok: false, missing: [`PROJECT#${slug}/META`] }, deployCoords: emptyCoords, deployState: emptyState, secrets: emptySecrets },
|
|
15282
|
-
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
15283
|
-
appOwnedGaps: autoHeal.appOwnedGaps
|
|
15284
|
-
};
|
|
15285
|
-
}
|
|
15286
|
-
let presentSecrets = /* @__PURE__ */ new Set();
|
|
15287
|
-
let secretsError;
|
|
15288
|
-
try {
|
|
15289
|
-
presentSecrets = new Set(await deps.listSecrets(repo));
|
|
15290
|
-
} catch (e) {
|
|
15291
|
-
secretsError = e?.message || "secrets list failed";
|
|
15292
|
-
}
|
|
15293
|
-
const deployCoords = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
|
|
15294
|
-
const required = stageInTrack(meta, stage) && projectRequiresDeployCoords(model, stage, meta);
|
|
15295
|
-
return [stage, { required, ok: required ? await deps.hasDeployCoords(slug, stage) : true }];
|
|
15296
|
-
})));
|
|
15297
|
-
const deployState = Object.fromEntries(await Promise.all(STAGES.map(async (stage) => {
|
|
15298
|
-
const required = stageInTrack(meta, stage) && projectRequiresDeployState(model, stage);
|
|
15299
|
-
return [stage, { required, ok: required ? await deps.hasDeployState(slug, stage) : true }];
|
|
15300
|
-
})));
|
|
15301
|
-
const secrets = Object.fromEntries(STAGES.map((stage) => {
|
|
15302
|
-
const names = stageInTrack(meta, stage) ? stageRequiredSecrets(stage, meta) : [];
|
|
15303
|
-
const satisfied = (key) => presentSecrets.has(stageKey2(stage, key)) || !key.includes("/") && presentSecrets.has(key);
|
|
15304
|
-
const required = names.map((key) => stageKey2(stage, key));
|
|
15305
|
-
const present = names.filter(satisfied).map((key) => stageKey2(stage, key));
|
|
15306
|
-
const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey2(stage, key));
|
|
15307
|
-
return [stage, { required, present, missing }];
|
|
15308
|
-
}));
|
|
15309
|
-
const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage) => [
|
|
15310
|
-
stage,
|
|
15311
|
-
stageInTrack(meta, stage) ? runtimeSecretStreamGap(stage, meta, presentSecrets) : []
|
|
15312
|
-
]));
|
|
15313
|
-
const runtimeSecretStreamWarningRows = STAGES.map((stage) => ({ stage, names: runtimeSecretStreamWarnings[stage] })).filter((row) => row.names.length > 0);
|
|
15314
|
-
const metaMissing = ["class", "projectType", "deployModel", "vaultPath"].filter((key) => meta[key] === void 0).concat(boardRegistryGaps(meta));
|
|
15315
|
-
const ok = !secretsError && metaMissing.length === 0 && Object.values(deployCoords).every((v) => v.ok) && Object.values(secrets).every((v) => v.missing.length === 0);
|
|
15316
|
-
const edgeDomainWarnings = deps.resolveDns ? await probeEdgeDomains(meta, deps.resolveDns) : [];
|
|
15317
|
-
return {
|
|
15318
|
-
ok,
|
|
15319
|
-
repo,
|
|
15320
|
-
slug,
|
|
15321
|
-
class: meta.class,
|
|
15322
|
-
projectType,
|
|
15323
|
-
deployModel: model,
|
|
15324
|
-
hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets },
|
|
15325
|
-
secretsError,
|
|
15326
|
-
autoHealAvailable: Object.keys(autoHeal.patch),
|
|
15327
|
-
appOwnedGaps: autoHeal.appOwnedGaps,
|
|
15328
|
-
...edgeDomainWarnings.length ? { edgeDomainWarnings } : {},
|
|
15329
|
-
...runtimeSecretStreamWarningRows.length ? { runtimeSecretStreamWarnings: runtimeSecretStreamWarningRows } : {},
|
|
15330
|
-
appAttested: appAttestationOf(meta) ?? void 0
|
|
15331
|
-
};
|
|
15332
|
-
}
|
|
15333
|
-
function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
15334
|
-
const start = "<!-- mmi-v2-readiness:start -->";
|
|
15335
|
-
const end = "<!-- mmi-v2-readiness:end -->";
|
|
15336
|
-
const stageLines = STAGES.map((stage) => {
|
|
15337
|
-
const coordState = report.hubOwned.deployCoords[stage];
|
|
15338
|
-
const coords = !coordState.required ? "coords n/a" : coordState.ok ? "coords ok" : "coords missing/unverified";
|
|
15339
|
-
const state = report.hubOwned.deployState[stage];
|
|
15340
|
-
const statePart = !state.required ? [] : [state.ok ? "deploy state stamped" : "deploy state not stamped"];
|
|
15341
|
-
const missing = report.hubOwned.secrets[stage].missing;
|
|
15342
|
-
return `- ${stage}: ${[coords, ...statePart].join("; ")}; ${missing.length ? `missing ${missing.join(", ")}` : "required secret names present"}`;
|
|
15343
|
-
});
|
|
15344
|
-
const section = [
|
|
15345
|
-
start,
|
|
15346
|
-
"## Hub v2 readiness",
|
|
15347
|
-
"",
|
|
15348
|
-
`Repo: ${report.repo}`,
|
|
15349
|
-
`Project type: ${report.projectType ?? "(unresolved)"}`,
|
|
15350
|
-
`Deploy model: ${report.deployModel ?? "(unresolved)"}`,
|
|
15351
|
-
`Overall: ${report.ok ? "ready" : "not ready"}`,
|
|
15352
|
-
"",
|
|
15353
|
-
"### Hub-owned diagnosis",
|
|
15354
|
-
`- META: ${report.hubOwned.meta.ok ? "ok" : `missing ${report.hubOwned.meta.missing.join(", ")}`}`,
|
|
15355
|
-
...stageLines,
|
|
15356
|
-
...report.secretsError ? [`- secrets UNVERIFIED (treated as not ready): ${report.secretsError}`] : [],
|
|
15357
|
-
...(report.edgeDomainWarnings ?? []).map(
|
|
15358
|
-
(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`
|
|
15359
|
-
),
|
|
15360
|
-
...(report.runtimeSecretStreamWarnings ?? []).map(
|
|
15361
|
-
(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`
|
|
15362
|
-
),
|
|
15363
|
-
"",
|
|
15364
|
-
"### Auto-heal applied / available",
|
|
15365
|
-
...opts.healed?.length ? opts.healed.map((x) => `- ${x}`) : report.autoHealAvailable.map((x) => `- ${x}`),
|
|
15366
|
-
"",
|
|
15367
|
-
"### App-owned implementation plan",
|
|
15368
|
-
...report.appOwnedGaps.map((x) => `- ${x}`),
|
|
15369
|
-
end
|
|
15370
|
-
].join("\n");
|
|
15371
|
-
const re = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
15372
|
-
return re.test(existingBody) ? existingBody.replace(re, () => section) : `${existingBody.trim()}
|
|
15373
|
-
|
|
15374
|
-
${section}`.trim();
|
|
15238
|
+
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
15375
15239
|
}
|
|
15376
15240
|
|
|
15377
15241
|
// src/oauth.ts
|
|
@@ -15381,7 +15245,7 @@ var ENV_PREFIXES = ["", "dev", "rc"];
|
|
|
15381
15245
|
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
15382
15246
|
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
15383
15247
|
var uniq = (xs) => [...new Set(xs)];
|
|
15384
|
-
function
|
|
15248
|
+
function defaultSubdomain(slug) {
|
|
15385
15249
|
const i = slug.indexOf("-");
|
|
15386
15250
|
return i === -1 ? slug : slug.slice(i + 1);
|
|
15387
15251
|
}
|
|
@@ -15434,7 +15298,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15434
15298
|
throw new Error("oauth must be an object when configured");
|
|
15435
15299
|
}
|
|
15436
15300
|
const raw = rawUnknown;
|
|
15437
|
-
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)];
|
|
15438
15302
|
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
15439
15303
|
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
15440
15304
|
if (!callbackPath.startsWith("/")) {
|
|
@@ -15445,7 +15309,7 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
15445
15309
|
}
|
|
15446
15310
|
const meta = mmiConfig ?? {};
|
|
15447
15311
|
const rawFofuSub = raw.fofuSubdomain;
|
|
15448
|
-
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub :
|
|
15312
|
+
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
|
|
15449
15313
|
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
15450
15314
|
}
|
|
15451
15315
|
function probeRedirectUri(callbackPath, port = 9123) {
|
|
@@ -15473,42 +15337,62 @@ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
|
15473
15337
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
15474
15338
|
var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
15475
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
|
+
}
|
|
15476
15360
|
function parseSecretsCatalogVar(raw) {
|
|
15477
15361
|
let parsed;
|
|
15478
15362
|
try {
|
|
15479
15363
|
parsed = JSON.parse(raw);
|
|
15480
15364
|
} catch {
|
|
15481
|
-
throw new Error('project set: secrets must be JSON keyed by KEY, e.g. {"SCRAPER_API_KEY":{"key":"SCRAPER_API_KEY","purpose":"scrape provider key","group":"scraper","owner":"oguz","stages":[],"consumers":["box"]}}');
|
|
15365
|
+
throw new Error('org project set: secrets must be JSON keyed by KEY, e.g. {"SCRAPER_API_KEY":{"key":"SCRAPER_API_KEY","purpose":"scrape provider key","group":"scraper","owner":"oguz","stages":[],"consumers":["box"]}}');
|
|
15482
15366
|
}
|
|
15483
15367
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15484
|
-
throw new Error("project set: secrets must be a map keyed by canonical KEY");
|
|
15368
|
+
throw new Error("org project set: secrets must be a map keyed by canonical KEY");
|
|
15485
15369
|
}
|
|
15486
15370
|
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
15487
15371
|
const overrideMapKey = /^([A-Z_][A-Z0-9_]*)@(dev|rc|main)$/;
|
|
15488
15372
|
for (const [mapKey, value] of Object.entries(parsed)) {
|
|
15489
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`project set: secrets["${mapKey}"] must be an object`);
|
|
15373
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`org project set: secrets["${mapKey}"] must be an object`);
|
|
15490
15374
|
const e = value;
|
|
15491
15375
|
if (typeof e.key !== "string" || !SECRET_ENV_NAME_RE.test(e.key)) {
|
|
15492
|
-
throw new Error(`project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
|
|
15376
|
+
throw new Error(`org project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
|
|
15493
15377
|
}
|
|
15494
15378
|
const override = overrideMapKey.exec(mapKey);
|
|
15495
15379
|
if (override) {
|
|
15496
15380
|
if (e.key !== override[1] || !Array.isArray(e.stages) || e.stages.length !== 1 || e.stages[0] !== override[2]) {
|
|
15497
|
-
throw new Error(`project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
|
|
15381
|
+
throw new Error(`org project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
|
|
15498
15382
|
}
|
|
15499
15383
|
} else if (e.key !== mapKey) {
|
|
15500
|
-
throw new Error(`project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
|
|
15384
|
+
throw new Error(`org project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
|
|
15501
15385
|
}
|
|
15502
|
-
if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
|
|
15503
|
-
if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`project set: secrets["${mapKey}"].provider must be a non-empty string`);
|
|
15386
|
+
if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`org project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
|
|
15387
|
+
if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`org project set: secrets["${mapKey}"].provider must be a non-empty string`);
|
|
15504
15388
|
if (!Array.isArray(e.stages) || e.stages.some((s) => !RUNTIME_SECRET_STAGES.includes(s))) {
|
|
15505
|
-
throw new Error(`project set: secrets["${mapKey}"].stages must be a subset of dev/rc/main ([] = shared)`);
|
|
15389
|
+
throw new Error(`org project set: secrets["${mapKey}"].stages must be a subset of dev/rc/main ([] = shared)`);
|
|
15506
15390
|
}
|
|
15507
15391
|
if (!Array.isArray(e.consumers) || e.consumers.some((c) => !SECRET_CONSUMERS.includes(c))) {
|
|
15508
|
-
throw new Error(`project set: secrets["${mapKey}"].consumers must be a subset of ${SECRET_CONSUMERS.join("/")}`);
|
|
15392
|
+
throw new Error(`org project set: secrets["${mapKey}"].consumers must be a subset of ${SECRET_CONSUMERS.join("/")}`);
|
|
15509
15393
|
}
|
|
15510
15394
|
if (e.aliases !== void 0 && (!Array.isArray(e.aliases) || e.aliases.some((a) => typeof a !== "string"))) {
|
|
15511
|
-
throw new Error(`project set: secrets["${mapKey}"].aliases must be a string array`);
|
|
15395
|
+
throw new Error(`org project set: secrets["${mapKey}"].aliases must be a string array`);
|
|
15512
15396
|
}
|
|
15513
15397
|
}
|
|
15514
15398
|
return parsed;
|
|
@@ -15518,19 +15402,19 @@ function parseRuntimeSecretsVar(raw) {
|
|
|
15518
15402
|
try {
|
|
15519
15403
|
parsed = JSON.parse(raw);
|
|
15520
15404
|
} catch {
|
|
15521
|
-
throw new Error('project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
|
|
15405
|
+
throw new Error('org project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
|
|
15522
15406
|
}
|
|
15523
15407
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15524
|
-
throw new Error("project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
|
|
15408
|
+
throw new Error("org project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
|
|
15525
15409
|
}
|
|
15526
15410
|
const map = parsed;
|
|
15527
15411
|
const out = {};
|
|
15528
15412
|
for (const [stage, names] of Object.entries(map)) {
|
|
15529
15413
|
if (!RUNTIME_SECRET_STAGES.includes(stage)) {
|
|
15530
|
-
throw new Error(`project set: requiredRuntimeSecrets stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
15414
|
+
throw new Error(`org project set: requiredRuntimeSecrets stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
15531
15415
|
}
|
|
15532
15416
|
if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || !n.trim())) {
|
|
15533
|
-
throw new Error(`project set: requiredRuntimeSecrets.${stage} must be an array of non-empty secret names`);
|
|
15417
|
+
throw new Error(`org project set: requiredRuntimeSecrets.${stage} must be an array of non-empty secret names`);
|
|
15534
15418
|
}
|
|
15535
15419
|
out[stage] = names;
|
|
15536
15420
|
}
|
|
@@ -15541,13 +15425,13 @@ function parseBuildSecretsVar(raw) {
|
|
|
15541
15425
|
try {
|
|
15542
15426
|
parsed = JSON.parse(raw);
|
|
15543
15427
|
} catch {
|
|
15544
|
-
throw new Error('project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
|
|
15428
|
+
throw new Error('org project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
|
|
15545
15429
|
}
|
|
15546
15430
|
if (!Array.isArray(parsed)) {
|
|
15547
|
-
throw new Error("project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
|
|
15431
|
+
throw new Error("org project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
|
|
15548
15432
|
}
|
|
15549
15433
|
if (parsed.some((n) => typeof n !== "string" || !BUILD_SECRET_REF_RE.test(n) || n === "@github-packages-token")) {
|
|
15550
|
-
throw new Error("project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
|
|
15434
|
+
throw new Error("org project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
|
|
15551
15435
|
}
|
|
15552
15436
|
return parsed;
|
|
15553
15437
|
}
|
|
@@ -15556,19 +15440,19 @@ function parseEdgeDomainsVar(raw) {
|
|
|
15556
15440
|
try {
|
|
15557
15441
|
parsed = JSON.parse(raw);
|
|
15558
15442
|
} catch {
|
|
15559
|
-
throw new Error('project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
|
|
15443
|
+
throw new Error('org project set: edgeDomains must be JSON, e.g. {"dev":"dev.example.co","rc":"rc.example.co","main":"example.co"}');
|
|
15560
15444
|
}
|
|
15561
15445
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15562
|
-
throw new Error("project set: edgeDomains must be a {dev,rc,main} map of domain strings");
|
|
15446
|
+
throw new Error("org project set: edgeDomains must be a {dev,rc,main} map of domain strings");
|
|
15563
15447
|
}
|
|
15564
15448
|
const map = parsed;
|
|
15565
15449
|
const out = {};
|
|
15566
15450
|
for (const [stage, domain] of Object.entries(map)) {
|
|
15567
15451
|
if (!RUNTIME_SECRET_STAGES.includes(stage)) {
|
|
15568
|
-
throw new Error(`project set: edgeDomains stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
15452
|
+
throw new Error(`org project set: edgeDomains stage "${stage}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
15569
15453
|
}
|
|
15570
15454
|
if (typeof domain !== "string" || !domain.trim()) {
|
|
15571
|
-
throw new Error(`project set: edgeDomains.${stage} must be a non-empty domain string`);
|
|
15455
|
+
throw new Error(`org project set: edgeDomains.${stage} must be a non-empty domain string`);
|
|
15572
15456
|
}
|
|
15573
15457
|
out[stage] = domain.trim();
|
|
15574
15458
|
}
|
|
@@ -15580,19 +15464,19 @@ function parsePriorityOptionsVar(raw) {
|
|
|
15580
15464
|
try {
|
|
15581
15465
|
parsed = JSON.parse(raw);
|
|
15582
15466
|
} catch {
|
|
15583
|
-
throw new Error('project set: priorityOptions must be JSON, e.g. {"Urgent":"<id>","High":"<id>","Medium":"<id>","Low":"<id>"}');
|
|
15467
|
+
throw new Error('org project set: priorityOptions must be JSON, e.g. {"Urgent":"<id>","High":"<id>","Medium":"<id>","Low":"<id>"}');
|
|
15584
15468
|
}
|
|
15585
15469
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15586
|
-
throw new Error("project set: priorityOptions must be a {Urgent,High,Medium,Low} map of option-id strings");
|
|
15470
|
+
throw new Error("org project set: priorityOptions must be a {Urgent,High,Medium,Low} map of option-id strings");
|
|
15587
15471
|
}
|
|
15588
15472
|
const map = parsed;
|
|
15589
15473
|
const out = {};
|
|
15590
15474
|
for (const [name, id] of Object.entries(map)) {
|
|
15591
15475
|
if (!PRIORITY_OPTION_NAMES.includes(name)) {
|
|
15592
|
-
throw new Error(`project set: priorityOptions "${name}" \u2014 expected only ${PRIORITY_OPTION_NAMES.join("/")}`);
|
|
15476
|
+
throw new Error(`org project set: priorityOptions "${name}" \u2014 expected only ${PRIORITY_OPTION_NAMES.join("/")}`);
|
|
15593
15477
|
}
|
|
15594
15478
|
if (typeof id !== "string" || !id.trim()) {
|
|
15595
|
-
throw new Error(`project set: priorityOptions.${name} must be a non-empty option-id string`);
|
|
15479
|
+
throw new Error(`org project set: priorityOptions.${name} must be a non-empty option-id string`);
|
|
15596
15480
|
}
|
|
15597
15481
|
out[name] = id.trim();
|
|
15598
15482
|
}
|
|
@@ -15603,7 +15487,7 @@ function parsePortRangeVar(raw) {
|
|
|
15603
15487
|
try {
|
|
15604
15488
|
parsed = JSON.parse(raw);
|
|
15605
15489
|
} catch {
|
|
15606
|
-
throw new Error('project set: portRange must be JSON, e.g. {"start":3700,"end":3710} or [3700,3710]');
|
|
15490
|
+
throw new Error('org project set: portRange must be JSON, e.g. {"start":3700,"end":3710} or [3700,3710]');
|
|
15607
15491
|
}
|
|
15608
15492
|
let start;
|
|
15609
15493
|
let end;
|
|
@@ -15612,12 +15496,12 @@ function parsePortRangeVar(raw) {
|
|
|
15612
15496
|
} else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
15613
15497
|
({ start, end } = parsed);
|
|
15614
15498
|
} else {
|
|
15615
|
-
throw new Error("project set: portRange must be {start,end} or a [start,end] tuple");
|
|
15499
|
+
throw new Error("org project set: portRange must be {start,end} or a [start,end] tuple");
|
|
15616
15500
|
}
|
|
15617
15501
|
if (typeof start !== "number" || typeof end !== "number" || !Number.isFinite(start) || !Number.isFinite(end)) {
|
|
15618
|
-
throw new Error("project set: portRange start/end must be finite numbers");
|
|
15502
|
+
throw new Error("org project set: portRange start/end must be finite numbers");
|
|
15619
15503
|
}
|
|
15620
|
-
if (start > end) throw new Error("project set: portRange start must be <= end");
|
|
15504
|
+
if (start > end) throw new Error("org project set: portRange start must be <= end");
|
|
15621
15505
|
return { start, end };
|
|
15622
15506
|
}
|
|
15623
15507
|
function parseStatusOptionsVar(raw) {
|
|
@@ -15625,16 +15509,16 @@ function parseStatusOptionsVar(raw) {
|
|
|
15625
15509
|
try {
|
|
15626
15510
|
parsed = JSON.parse(raw);
|
|
15627
15511
|
} catch {
|
|
15628
|
-
throw new Error('project set: statusOptions must be JSON, e.g. {"Todo":"<id>","In Progress":"<id>","In Review":"<id>","Done":"<id>"}');
|
|
15512
|
+
throw new Error('org project set: statusOptions must be JSON, e.g. {"Todo":"<id>","In Progress":"<id>","In Review":"<id>","Done":"<id>"}');
|
|
15629
15513
|
}
|
|
15630
15514
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15631
|
-
throw new Error("project set: statusOptions must be a map of status name to option-id string");
|
|
15515
|
+
throw new Error("org project set: statusOptions must be a map of status name to option-id string");
|
|
15632
15516
|
}
|
|
15633
15517
|
const map = parsed;
|
|
15634
15518
|
const out = {};
|
|
15635
15519
|
for (const [name, id] of Object.entries(map)) {
|
|
15636
15520
|
if (typeof id !== "string" || !id.trim()) {
|
|
15637
|
-
throw new Error(`project set: statusOptions.${name} must be a non-empty option-id string`);
|
|
15521
|
+
throw new Error(`org project set: statusOptions.${name} must be a non-empty option-id string`);
|
|
15638
15522
|
}
|
|
15639
15523
|
out[name] = id.trim();
|
|
15640
15524
|
}
|
|
@@ -15645,31 +15529,31 @@ function parseOauthVar(raw) {
|
|
|
15645
15529
|
try {
|
|
15646
15530
|
parsed = JSON.parse(raw);
|
|
15647
15531
|
} catch {
|
|
15648
|
-
throw new Error(`project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
15532
|
+
throw new Error(`org project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
15649
15533
|
}
|
|
15650
15534
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15651
|
-
throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
15535
|
+
throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
15652
15536
|
}
|
|
15653
15537
|
const map = parsed;
|
|
15654
15538
|
const out = {};
|
|
15655
15539
|
for (const [key, value] of Object.entries(map)) {
|
|
15656
15540
|
if (key === "subdomains" || key === "domains") {
|
|
15657
15541
|
if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim())) {
|
|
15658
|
-
throw new Error(`project set: oauth.${key} must be an array of non-empty strings`);
|
|
15542
|
+
throw new Error(`org project set: oauth.${key} must be an array of non-empty strings`);
|
|
15659
15543
|
}
|
|
15660
15544
|
out[key] = value.map((v) => v.trim());
|
|
15661
15545
|
} else if (key === "callbackPath") {
|
|
15662
|
-
if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
|
|
15546
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("org project set: oauth.callbackPath must be a non-empty string");
|
|
15663
15547
|
const callbackPath = value.trim();
|
|
15664
15548
|
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
15665
|
-
throw new Error(`project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
15549
|
+
throw new Error(`org project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
15666
15550
|
}
|
|
15667
15551
|
out.callbackPath = callbackPath;
|
|
15668
15552
|
} else if (key === "fofuSubdomain") {
|
|
15669
|
-
if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
15553
|
+
if (typeof value !== "string") throw new Error('org project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
15670
15554
|
out.fofuSubdomain = value.trim();
|
|
15671
15555
|
} else {
|
|
15672
|
-
throw new Error(`project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
|
|
15556
|
+
throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
|
|
15673
15557
|
}
|
|
15674
15558
|
}
|
|
15675
15559
|
return out;
|
|
@@ -15679,49 +15563,49 @@ function parseReposVar(raw) {
|
|
|
15679
15563
|
try {
|
|
15680
15564
|
parsed = JSON.parse(raw);
|
|
15681
15565
|
} catch {
|
|
15682
|
-
throw new Error('project set: repos must be a JSON array, e.g. ["mutmutco/mm-foo"]');
|
|
15566
|
+
throw new Error('org project set: repos must be a JSON array, e.g. ["mutmutco/mm-foo"]');
|
|
15683
15567
|
}
|
|
15684
15568
|
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((r) => typeof r !== "string" || !r.trim())) {
|
|
15685
|
-
throw new Error("project set: repos must be a non-empty array of owner/name strings");
|
|
15569
|
+
throw new Error("org project set: repos must be a non-empty array of owner/name strings");
|
|
15686
15570
|
}
|
|
15687
15571
|
return parsed.map((r) => r.trim());
|
|
15688
15572
|
}
|
|
15689
15573
|
function parsePublishRequiredVar(raw) {
|
|
15690
15574
|
if (raw === "true") return true;
|
|
15691
15575
|
if (raw === "false") return false;
|
|
15692
|
-
throw new Error("project set: publishRequired must be true or false");
|
|
15576
|
+
throw new Error("org project set: publishRequired must be true or false");
|
|
15693
15577
|
}
|
|
15694
15578
|
function parseFofuEnabledVar(raw) {
|
|
15695
15579
|
if (raw === "true") return true;
|
|
15696
15580
|
if (raw === "false") return false;
|
|
15697
|
-
throw new Error("project set: fofuEnabled must be true or false");
|
|
15581
|
+
throw new Error("org project set: fofuEnabled must be true or false");
|
|
15698
15582
|
}
|
|
15699
15583
|
function parseRuntimeVaultOnlyVar(raw) {
|
|
15700
15584
|
if (raw === "true") return true;
|
|
15701
15585
|
if (raw === "false") return false;
|
|
15702
|
-
throw new Error("project set: runtimeVaultOnly must be true or false");
|
|
15586
|
+
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
15703
15587
|
}
|
|
15704
15588
|
function parseConsumesDesignSystemVar(raw) {
|
|
15705
15589
|
if (raw === "fofu") return raw;
|
|
15706
|
-
throw new Error('project set: consumesDesignSystem must be "fofu"');
|
|
15590
|
+
throw new Error('org project set: consumesDesignSystem must be "fofu"');
|
|
15707
15591
|
}
|
|
15708
15592
|
function parsePublishDirVar(raw) {
|
|
15709
15593
|
const v = raw.trim();
|
|
15710
15594
|
if (v === "" || v === ".") {
|
|
15711
|
-
throw new Error("project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
|
|
15595
|
+
throw new Error("org project set: publishDir must be a non-empty relative subpath, e.g. packages/ui (omit it or --unset publishDir for the repo root)");
|
|
15712
15596
|
}
|
|
15713
15597
|
if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
15714
|
-
throw new Error('project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
|
|
15598
|
+
throw new Error('org project set: publishDir must be a safe relative subpath \u2014 no leading slash, no ".." segment');
|
|
15715
15599
|
}
|
|
15716
15600
|
return v;
|
|
15717
15601
|
}
|
|
15718
15602
|
function parseDsManifestPathVar(raw) {
|
|
15719
15603
|
const v = raw.trim();
|
|
15720
15604
|
if (v === "" || !/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
15721
|
-
throw new Error('project set: dsManifestPath must be a safe relative path \u2014 no leading slash, no ".." segment');
|
|
15605
|
+
throw new Error('org project set: dsManifestPath must be a safe relative path \u2014 no leading slash, no ".." segment');
|
|
15722
15606
|
}
|
|
15723
15607
|
if (v !== "package.json" && !v.endsWith("/package.json")) {
|
|
15724
|
-
throw new Error("project set: dsManifestPath must point to a package.json, e.g. web/package.json");
|
|
15608
|
+
throw new Error("org project set: dsManifestPath must point to a package.json, e.g. web/package.json");
|
|
15725
15609
|
}
|
|
15726
15610
|
return v;
|
|
15727
15611
|
}
|
|
@@ -15730,10 +15614,10 @@ function parseRequiredChecksVar(raw) {
|
|
|
15730
15614
|
try {
|
|
15731
15615
|
parsed = JSON.parse(raw);
|
|
15732
15616
|
} catch {
|
|
15733
|
-
throw new Error('project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
|
|
15617
|
+
throw new Error('org project set: requiredChecks must be a JSON array, e.g. ["gate"] or [] for an intentional no-ci repo');
|
|
15734
15618
|
}
|
|
15735
15619
|
if (!Array.isArray(parsed) || parsed.some((c) => typeof c !== "string" || !c.trim())) {
|
|
15736
|
-
throw new Error("project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
|
|
15620
|
+
throw new Error("org project set: requiredChecks must be a JSON array of non-empty check-context strings (use [] for none)");
|
|
15737
15621
|
}
|
|
15738
15622
|
return parsed.map((c) => c.trim());
|
|
15739
15623
|
}
|
|
@@ -15742,22 +15626,22 @@ function parseGateVar(raw) {
|
|
|
15742
15626
|
try {
|
|
15743
15627
|
parsed = JSON.parse(raw);
|
|
15744
15628
|
} catch {
|
|
15745
|
-
throw new Error('project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
|
|
15629
|
+
throw new Error('org project set: gate must be JSON, e.g. {"runtime":"python","cmd":"pytest","workdir":"app"}');
|
|
15746
15630
|
}
|
|
15747
15631
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15748
|
-
throw new Error("project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
|
|
15632
|
+
throw new Error("org project set: gate must be a {runtime,cmd,workdir,cacheDepPath,pyVersion} object");
|
|
15749
15633
|
}
|
|
15750
15634
|
const map = parsed;
|
|
15751
15635
|
const out = {};
|
|
15752
15636
|
for (const [key, value] of Object.entries(map)) {
|
|
15753
15637
|
if (key === "runtime") {
|
|
15754
|
-
if (value !== "node" && value !== "python") throw new Error('project set: gate.runtime must be "node" or "python"');
|
|
15638
|
+
if (value !== "node" && value !== "python") throw new Error('org project set: gate.runtime must be "node" or "python"');
|
|
15755
15639
|
out.runtime = value;
|
|
15756
15640
|
} else if (key === "cmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
|
|
15757
|
-
if (typeof value !== "string" || !value.trim()) throw new Error(`project set: gate.${key} must be a non-empty string`);
|
|
15641
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`org project set: gate.${key} must be a non-empty string`);
|
|
15758
15642
|
out[key] = value.trim();
|
|
15759
15643
|
} else {
|
|
15760
|
-
throw new Error(`project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
|
|
15644
|
+
throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/workdir/cacheDepPath/pyVersion`);
|
|
15761
15645
|
}
|
|
15762
15646
|
}
|
|
15763
15647
|
return out;
|
|
@@ -15835,25 +15719,25 @@ function buildProjectSetPatch(input) {
|
|
|
15835
15719
|
const patch = {};
|
|
15836
15720
|
if (input.class) {
|
|
15837
15721
|
if (input.class !== "deployable" && input.class !== "content") {
|
|
15838
|
-
throw new Error("project set: --class must be deployable or content");
|
|
15722
|
+
throw new Error("org project set: --class must be deployable or content");
|
|
15839
15723
|
}
|
|
15840
15724
|
patch.class = input.class;
|
|
15841
15725
|
}
|
|
15842
15726
|
if (input.projectType) {
|
|
15843
15727
|
if (!isProjectType(input.projectType)) {
|
|
15844
|
-
throw new Error(`project set: --project-type must be one of: ${PROJECT_TYPES.join(", ")}`);
|
|
15728
|
+
throw new Error(`org project set: --project-type must be one of: ${PROJECT_TYPES.join(", ")}`);
|
|
15845
15729
|
}
|
|
15846
15730
|
patch.projectType = input.projectType;
|
|
15847
15731
|
}
|
|
15848
15732
|
if (input.deployModel) {
|
|
15849
15733
|
if (!isDeployModel(input.deployModel)) {
|
|
15850
|
-
throw new Error(`project set: --deploy-model must be one of: ${DEPLOY_MODELS.join(", ")}`);
|
|
15734
|
+
throw new Error(`org project set: --deploy-model must be one of: ${DEPLOY_MODELS.join(", ")}`);
|
|
15851
15735
|
}
|
|
15852
15736
|
patch.deployModel = input.deployModel;
|
|
15853
15737
|
}
|
|
15854
15738
|
if (input.releaseTrack) {
|
|
15855
15739
|
if (!isReleaseTrack(input.releaseTrack)) {
|
|
15856
|
-
throw new Error(`project set: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
|
|
15740
|
+
throw new Error(`org project set: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
|
|
15857
15741
|
}
|
|
15858
15742
|
patch.releaseTrack = input.releaseTrack;
|
|
15859
15743
|
}
|
|
@@ -15862,16 +15746,16 @@ function buildProjectSetPatch(input) {
|
|
|
15862
15746
|
if (eq <= 0) {
|
|
15863
15747
|
const looksPositional = value.includes("/") || !value.includes("=");
|
|
15864
15748
|
const hint = looksPositional ? " \u2014 if that is the [owner/repo] argument, put it BEFORE --var; a variadic flag swallows the tokens after it" : "";
|
|
15865
|
-
throw new Error(`project set: --var must be KEY=VALUE (got "${value}")${hint}`);
|
|
15749
|
+
throw new Error(`org project set: --var must be KEY=VALUE (got "${value}")${hint}`);
|
|
15866
15750
|
}
|
|
15867
15751
|
const key = value.slice(0, eq);
|
|
15868
15752
|
const raw = value.slice(eq + 1);
|
|
15869
15753
|
if (!SETTABLE_VAR_KEY_SET.has(key)) {
|
|
15870
|
-
throw new Error(`project set: --var KEY "${key}" is not settable \u2014 allowed keys: ${SETTABLE_VAR_KEYS.join(", ")}`);
|
|
15754
|
+
throw new Error(`org project set: --var KEY "${key}" is not settable \u2014 allowed keys: ${SETTABLE_VAR_KEYS.join(", ")}`);
|
|
15871
15755
|
}
|
|
15872
15756
|
if (key === "projectNumber") {
|
|
15873
15757
|
const n = Number(raw);
|
|
15874
|
-
if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
|
|
15758
|
+
if (!Number.isFinite(n)) throw new Error("org project set: projectNumber must be numeric");
|
|
15875
15759
|
patch[key] = n;
|
|
15876
15760
|
} else if (key === "requiredRuntimeSecrets") {
|
|
15877
15761
|
patch[key] = parseRuntimeSecretsVar(raw);
|
|
@@ -15904,7 +15788,7 @@ function buildProjectSetPatch(input) {
|
|
|
15904
15788
|
} else if (key === "dsManifestPath") {
|
|
15905
15789
|
patch[key] = parseDsManifestPathVar(raw);
|
|
15906
15790
|
} else if (key === "ci") {
|
|
15907
|
-
if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
|
|
15791
|
+
if (raw !== "none") throw new Error('org project set: ci must be "none" (or use --unset ci to require checks)');
|
|
15908
15792
|
patch[key] = raw;
|
|
15909
15793
|
} else if (key === "requiredChecks") {
|
|
15910
15794
|
patch[key] = parseRequiredChecksVar(raw);
|
|
@@ -15916,7 +15800,7 @@ function buildProjectSetPatch(input) {
|
|
|
15916
15800
|
}
|
|
15917
15801
|
for (const key of input.unsets) {
|
|
15918
15802
|
if (!UNSET_KEY_SET.has(key)) {
|
|
15919
|
-
throw new Error(`project set: --unset must be one of: ${UNSET_KEYS.join(", ")}`);
|
|
15803
|
+
throw new Error(`org project set: --unset must be one of: ${UNSET_KEYS.join(", ")}`);
|
|
15920
15804
|
}
|
|
15921
15805
|
patch[key] = null;
|
|
15922
15806
|
}
|
|
@@ -15925,7 +15809,7 @@ function buildProjectSetPatch(input) {
|
|
|
15925
15809
|
patch.edgeDomains = null;
|
|
15926
15810
|
}
|
|
15927
15811
|
if (Object.keys(patch).length === 0) {
|
|
15928
|
-
throw new Error("project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
|
|
15812
|
+
throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
|
|
15929
15813
|
}
|
|
15930
15814
|
return patch;
|
|
15931
15815
|
}
|
|
@@ -15937,36 +15821,36 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
15937
15821
|
const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
|
|
15938
15822
|
const stage = clean4(input.stage);
|
|
15939
15823
|
if (!stage || !DEPLOY_STAGES.includes(stage)) {
|
|
15940
|
-
throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
|
|
15824
|
+
throw new Error(`org project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
|
|
15941
15825
|
}
|
|
15942
15826
|
const substrate = clean4(input.substrate);
|
|
15943
15827
|
if (substrate !== void 0 && !DEPLOY_SUBSTRATES.includes(substrate)) {
|
|
15944
|
-
throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
|
|
15828
|
+
throw new Error(`org project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
|
|
15945
15829
|
}
|
|
15946
15830
|
const sshHost = clean4(input.sshHost);
|
|
15947
15831
|
const sshUser = clean4(input.sshUser);
|
|
15948
15832
|
const deployPath = clean4(input.deployPath);
|
|
15949
15833
|
const serviceName = clean4(input.service);
|
|
15950
15834
|
for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
|
|
15951
|
-
if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
|
|
15835
|
+
if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`org project set-deploy: ${label} contains unsafe characters`);
|
|
15952
15836
|
}
|
|
15953
15837
|
let port;
|
|
15954
15838
|
if (input.port !== void 0 && input.port !== null && `${input.port}`.trim() !== "") {
|
|
15955
15839
|
port = Number(input.port);
|
|
15956
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
|
|
15840
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("org project set-deploy: --port must be an integer 1..65535");
|
|
15957
15841
|
}
|
|
15958
15842
|
const domain = clean4(input.domain);
|
|
15959
|
-
if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
|
|
15843
|
+
if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`org project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
|
|
15960
15844
|
const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
|
|
15961
15845
|
for (const a of aliases) {
|
|
15962
|
-
if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
|
|
15846
|
+
if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`org project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
|
|
15963
15847
|
}
|
|
15964
15848
|
const uniqueAliases = [...new Set(aliases)];
|
|
15965
15849
|
if (input.clearAliases && uniqueAliases.length) {
|
|
15966
|
-
throw new Error("project set-deploy: --clear-aliases cannot be combined with --alias (they contradict)");
|
|
15850
|
+
throw new Error("org project set-deploy: --clear-aliases cannot be combined with --alias (they contradict)");
|
|
15967
15851
|
}
|
|
15968
15852
|
if (input.noEnvFile !== void 0 && typeof input.noEnvFile !== "boolean") {
|
|
15969
|
-
throw new Error("project set-deploy: --no-env-file must be true or false");
|
|
15853
|
+
throw new Error("org project set-deploy: --no-env-file must be true or false");
|
|
15970
15854
|
}
|
|
15971
15855
|
return {
|
|
15972
15856
|
stage,
|
|
@@ -15984,17 +15868,44 @@ function buildSetDeployPatch(_slug, input) {
|
|
|
15984
15868
|
};
|
|
15985
15869
|
}
|
|
15986
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
|
+
}
|
|
15987
15891
|
function composeDeclaresEnvFile(composeText) {
|
|
15988
15892
|
return composeText.split(/\r?\n/).some((line) => /^\s*env_file\s*:/.test(line));
|
|
15989
15893
|
}
|
|
15990
15894
|
function composeHasRuntimeEnvLabel(composeText) {
|
|
15991
|
-
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
|
+
});
|
|
15992
15899
|
}
|
|
15993
15900
|
function evaluateFilelessComposeGuard(input) {
|
|
15994
15901
|
if (input.force) return { ok: true, warn: `--force: skipping the fileless-compose guard for ${input.branch}` };
|
|
15995
15902
|
if (input.composeText === null) {
|
|
15996
|
-
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.` };
|
|
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.` };
|
|
15997
15907
|
}
|
|
15908
|
+
if (input.noEnvFile === false) return { ok: true };
|
|
15998
15909
|
if (composeDeclaresEnvFile(input.composeText)) {
|
|
15999
15910
|
return {
|
|
16000
15911
|
ok: false,
|
|
@@ -16056,15 +15967,16 @@ function evaluatePromotionFilelessGuard(input) {
|
|
|
16056
15967
|
return {
|
|
16057
15968
|
ok: false,
|
|
16058
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:
|
|
16059
|
-
|
|
16060
|
-
|
|
16061
|
-
(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`
|
|
16062
15972
|
};
|
|
16063
15973
|
}
|
|
16064
15974
|
if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
|
|
16065
15975
|
return {
|
|
16066
15976
|
ok: false,
|
|
16067
|
-
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.`
|
|
16068
15980
|
};
|
|
16069
15981
|
}
|
|
16070
15982
|
return { ok: true };
|
|
@@ -16080,11 +15992,11 @@ function vaultCleanupNote(slug) {
|
|
|
16080
15992
|
function buildRetirePlan(input) {
|
|
16081
15993
|
const target = input.target?.trim();
|
|
16082
15994
|
if (!target) {
|
|
16083
|
-
throw new Error("project retire: pass <owner/repo> or a slug to retire");
|
|
15995
|
+
throw new Error("org project retire: pass <owner/repo> or a slug to retire");
|
|
16084
15996
|
}
|
|
16085
15997
|
const slug = retireSlugOf(target);
|
|
16086
15998
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
16087
|
-
throw new Error(`project retire: "${target}" does not resolve to a valid slug`);
|
|
15999
|
+
throw new Error(`org project retire: "${target}" does not resolve to a valid slug`);
|
|
16088
16000
|
}
|
|
16089
16001
|
const repo = target.includes("/") ? target : `mutmutco/${slug}`;
|
|
16090
16002
|
return { slug, repo, apply: Boolean(input.apply), skipBoard: Boolean(input.skipBoard) };
|
|
@@ -16153,6 +16065,27 @@ var import_node_fs17 = require("node:fs");
|
|
|
16153
16065
|
var import_node_path15 = require("node:path");
|
|
16154
16066
|
var import_node_os5 = require("node:os");
|
|
16155
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
|
+
|
|
16156
16089
|
// src/secrets-diff.ts
|
|
16157
16090
|
var TIMEOUT_MS2 = 8e3;
|
|
16158
16091
|
var OWNER3 = "mutmutco";
|
|
@@ -16324,13 +16257,13 @@ async function withSecrets(run) {
|
|
|
16324
16257
|
await run(makeSecretsDeps(cfg));
|
|
16325
16258
|
}
|
|
16326
16259
|
function registerSecretsCommands(program3) {
|
|
16327
|
-
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");
|
|
16328
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)));
|
|
16329
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)));
|
|
16330
|
-
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) => {
|
|
16331
16264
|
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
16332
16265
|
}));
|
|
16333
|
-
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)));
|
|
16334
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) => {
|
|
16335
16268
|
if (!await secretsDoctor(d, o)) process.exitCode = 1;
|
|
16336
16269
|
}));
|
|
@@ -16399,7 +16332,7 @@ function registerSecretsCommands(program3) {
|
|
|
16399
16332
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
16400
16333
|
const fact = facts?.stages?.[stage] ?? null;
|
|
16401
16334
|
const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
|
|
16402
|
-
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 });
|
|
16403
16336
|
if (!verdict.ok) {
|
|
16404
16337
|
d.err(`secrets preflight: ${verdict.reason}`);
|
|
16405
16338
|
filelessOk = false;
|
|
@@ -16414,7 +16347,7 @@ function registerSecretsCommands(program3) {
|
|
|
16414
16347
|
const ok = await secretsRequest(d, key, o);
|
|
16415
16348
|
if (!ok) process.exitCode = 1;
|
|
16416
16349
|
}));
|
|
16417
|
-
secrets.command("declare <key>").description("project-admin self-declare: register a NEW secrets-catalog entry for your own repo (name/purpose/group/owner/consumers/stages metadata only \u2014 never a value); closes the declare-first master-only gap (MM-PM#261/#262). Catalog-declare only \u2014 wiring the key into requiredRuntimeSecrets stays master-only (`secrets request` to ask, or `project set --var requiredRuntimeSecrets=...`); dropped from self-serve after adversarial review found real escalation paths in that additive-merge logic (MMI-Hub#2780). Separate from `project set`, which stays master-only for this field.").requiredOption("--purpose <text>", "what this secret is for").requiredOption("--group <text>", "catalog grouping, e.g. database, auth").option("--owner <login>", "catalog owner login (default: your own resolved GitHub login)").option("--consumers <list>", "comma-separated consumers (default: box)").option("--stages <list>", "comma-separated dev/rc/main this entry applies to (default: none \u2014 one stageless shared value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
|
|
16350
|
+
secrets.command("declare <key>").description("project-admin self-declare: register a NEW secrets-catalog entry for your own repo (name/purpose/group/owner/consumers/stages metadata only \u2014 never a value); closes the declare-first master-only gap (MM-PM#261/#262). Catalog-declare only \u2014 wiring the key into requiredRuntimeSecrets stays master-only (`secrets request` to ask, or `org project set --var requiredRuntimeSecrets=...`); dropped from self-serve after adversarial review found real escalation paths in that additive-merge logic (MMI-Hub#2780). Separate from `org project set`, which stays master-only for this field.").requiredOption("--purpose <text>", "what this secret is for").requiredOption("--group <text>", "catalog grouping, e.g. database, auth").option("--owner <login>", "catalog owner login (default: your own resolved GitHub login)").option("--consumers <list>", "comma-separated consumers (default: box)").option("--stages <list>", "comma-separated dev/rc/main this entry applies to (default: none \u2014 one stageless shared value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
|
|
16418
16351
|
const owner = o.owner ?? await githubLogin();
|
|
16419
16352
|
if (!owner) {
|
|
16420
16353
|
fail("secrets declare: could not resolve your GitHub login for --owner; pass --owner explicitly");
|
|
@@ -16441,8 +16374,7 @@ function registerSecretsCommands(program3) {
|
|
|
16441
16374
|
if (!ok) process.exitCode = 1;
|
|
16442
16375
|
});
|
|
16443
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);
|
|
16444
|
-
secrets.command("
|
|
16445
|
-
secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").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) => {
|
|
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) => {
|
|
16446
16378
|
const stages = ["dev", "rc", "main"];
|
|
16447
16379
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16448
16380
|
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
@@ -16482,13 +16414,13 @@ function registerSecretsCommands(program3) {
|
|
|
16482
16414
|
);
|
|
16483
16415
|
if (!ok) process.exitCode = 1;
|
|
16484
16416
|
}));
|
|
16485
|
-
secrets.command("rm <key>").description("remove a secret
|
|
16486
|
-
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) => {
|
|
16487
16419
|
const ok = await secretsUse(d, key, { ...o, command });
|
|
16488
16420
|
if (ok === false) process.exitCode = 1;
|
|
16489
16421
|
}));
|
|
16490
|
-
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to
|
|
16491
|
-
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, {})));
|
|
16492
16424
|
}
|
|
16493
16425
|
|
|
16494
16426
|
// src/app-actor.ts
|
|
@@ -16788,14 +16720,14 @@ async function fetchInventory() {
|
|
|
16788
16720
|
}
|
|
16789
16721
|
});
|
|
16790
16722
|
if (incomplete.length === PROJECTS.length) {
|
|
16791
|
-
throw new Error(`box: could not read any Hetzner project \u2014
|
|
16723
|
+
throw new Error(`runtime box: could not read any Hetzner project \u2014
|
|
16792
16724
|
${incomplete.join("\n ")}`);
|
|
16793
16725
|
}
|
|
16794
16726
|
return { boxes: boxes.sort((a, b) => a.name.localeCompare(b.name)), incomplete };
|
|
16795
16727
|
}
|
|
16796
16728
|
function warnIncomplete(incomplete) {
|
|
16797
|
-
for (const f of incomplete) console.error(`box: WARNING \u2014 ${f}`);
|
|
16798
|
-
if (incomplete.length) console.error("box: this inventory is INCOMPLETE \u2014 boxes may be missing from it.");
|
|
16729
|
+
for (const f of incomplete) console.error(`runtime box: WARNING \u2014 ${f}`);
|
|
16730
|
+
if (incomplete.length) console.error("runtime box: this inventory is INCOMPLETE \u2014 boxes may be missing from it.");
|
|
16799
16731
|
}
|
|
16800
16732
|
function registerBoxCommands(program3) {
|
|
16801
16733
|
const box = program3.command("box").description("the box inventory \u2014 which machines exist, their addresses, and the vault key that opens each (never key values)");
|
|
@@ -16820,12 +16752,12 @@ function registerBoxCommands(program3) {
|
|
|
16820
16752
|
if (result.kind === "unknown") {
|
|
16821
16753
|
warnIncomplete(result.reasons);
|
|
16822
16754
|
await failGraceful(
|
|
16823
|
-
`box: cannot confirm whether '${name}' exists \u2014 part of the inventory could not be read (see above). Refusing to answer "no such box" from a list I know is incomplete.`
|
|
16755
|
+
`runtime box: cannot confirm whether '${name}' exists \u2014 part of the inventory could not be read (see above). Refusing to answer "no such box" from a list I know is incomplete.`
|
|
16824
16756
|
);
|
|
16825
16757
|
return;
|
|
16826
16758
|
}
|
|
16827
16759
|
if (result.kind === "absent") {
|
|
16828
|
-
await failGraceful(`box: no box named '${name}'. Known boxes: ${result.known.join(", ") || "(none)"}`);
|
|
16760
|
+
await failGraceful(`runtime box: no box named '${name}'. Known boxes: ${result.known.join(", ") || "(none)"}`);
|
|
16829
16761
|
return;
|
|
16830
16762
|
}
|
|
16831
16763
|
const found = result.box;
|
|
@@ -16940,7 +16872,7 @@ function awsScheduleNames(payload) {
|
|
|
16940
16872
|
return schedules.map((s) => s && typeof s === "object" ? s.Name : void 0).filter((n) => typeof n === "string" && !isJervResource(n));
|
|
16941
16873
|
}
|
|
16942
16874
|
var DECLARED_ENTRIES = [
|
|
16943
|
-
{ name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli box get <box> --ssh)" },
|
|
16875
|
+
{ name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli runtime box get <box> --ssh)" },
|
|
16944
16876
|
{ name: "zuber-bake", cadence: "*:05/30 (every 30 min)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake.timer \u2192 rolling overlay bake, metro tier, next ~6h window (verify over ssh)" },
|
|
16945
16877
|
{ name: "zuber-bake-full", cadence: "00:30 UTC (03:30 TRT, daily)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake-full.timer \u2192 full 24h overlay rebuild, all 81 il (verify over ssh)" }
|
|
16946
16878
|
];
|
|
@@ -16972,7 +16904,7 @@ function renderDocSection(entries, generatedAtIso) {
|
|
|
16972
16904
|
}
|
|
16973
16905
|
return [
|
|
16974
16906
|
DOC_START_MARKER,
|
|
16975
|
-
`_Generated by \`mmi-cli schedules --doc\` at ${generatedAtIso}. Never hand-edit this section._`,
|
|
16907
|
+
`_Generated by \`mmi-cli org schedules --doc\` at ${generatedAtIso}. Never hand-edit this section._`,
|
|
16976
16908
|
"",
|
|
16977
16909
|
lines.join("\n"),
|
|
16978
16910
|
DOC_END_MARKER
|
|
@@ -16982,7 +16914,7 @@ function spliceDoc(docText, generatedSection) {
|
|
|
16982
16914
|
const start = docText.indexOf(DOC_START_MARKER);
|
|
16983
16915
|
const end = docText.indexOf(DOC_END_MARKER);
|
|
16984
16916
|
if (start === -1 || end === -1 || end < start) {
|
|
16985
|
-
throw new Error(`schedules --doc: the target doc is missing the ${DOC_START_MARKER} / ${DOC_END_MARKER} markers \u2014 refusing to guess where the generated inventory goes.`);
|
|
16917
|
+
throw new Error(`org schedules --doc: the target doc is missing the ${DOC_START_MARKER} / ${DOC_END_MARKER} markers \u2014 refusing to guess where the generated inventory goes.`);
|
|
16986
16918
|
}
|
|
16987
16919
|
return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
|
|
16988
16920
|
}
|
|
@@ -17190,11 +17122,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
17190
17122
|
};
|
|
17191
17123
|
}
|
|
17192
17124
|
function warnIncomplete2(incomplete) {
|
|
17193
|
-
for (const f of incomplete) console.error(`schedules: WARNING \u2014 ${f}`);
|
|
17194
|
-
if (incomplete.length) console.error("schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
|
|
17125
|
+
for (const f of incomplete) console.error(`org schedules: WARNING \u2014 ${f}`);
|
|
17126
|
+
if (incomplete.length) console.error("org schedules: this notebook is INCOMPLETE \u2014 schedules may be missing from it.");
|
|
17195
17127
|
}
|
|
17196
17128
|
function reportDrift(drift) {
|
|
17197
|
-
for (const d of drift) console.error(`schedules: DRIFT \u2014 ${d}`);
|
|
17129
|
+
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
17198
17130
|
}
|
|
17199
17131
|
function registerSchedulesCommands(program3) {
|
|
17200
17132
|
program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
|
|
@@ -17203,14 +17135,14 @@ function registerSchedulesCommands(program3) {
|
|
|
17203
17135
|
if (o.doc) {
|
|
17204
17136
|
if (incomplete.length) {
|
|
17205
17137
|
warnIncomplete2(incomplete);
|
|
17206
|
-
await failGraceful("schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
|
|
17138
|
+
await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
|
|
17207
17139
|
return;
|
|
17208
17140
|
}
|
|
17209
17141
|
const docText = await (0, import_promises2.readFile)(o.doc, "utf8");
|
|
17210
17142
|
const spliced = spliceDoc(docText, renderDocSection(entries, (/* @__PURE__ */ new Date()).toISOString()));
|
|
17211
17143
|
await (0, import_promises2.writeFile)(o.doc, spliced, "utf8");
|
|
17212
17144
|
reportDrift(drift);
|
|
17213
|
-
console.log(`schedules: wrote ${entries.length} entries into ${o.doc}`);
|
|
17145
|
+
console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
|
|
17214
17146
|
return;
|
|
17215
17147
|
}
|
|
17216
17148
|
if (o.json) {
|
|
@@ -17457,15 +17389,15 @@ async function runIssueChildren(deps, epic, opts) {
|
|
|
17457
17389
|
const seen = /* @__PURE__ */ new Set([childKey(repo, ref.number)]);
|
|
17458
17390
|
for (const raw of await queryChildren(deps, owner, name, ref.number)) {
|
|
17459
17391
|
if (out.length >= CHILDREN_MAX_TOTAL) break;
|
|
17460
|
-
const
|
|
17461
|
-
out.push(
|
|
17462
|
-
seen.add(childKey(
|
|
17392
|
+
const child2 = shapeChildNode(raw, 1, boardProjectId);
|
|
17393
|
+
out.push(child2);
|
|
17394
|
+
seen.add(childKey(child2.repo, child2.number));
|
|
17463
17395
|
}
|
|
17464
17396
|
if (!opts.recursive || out.length >= CHILDREN_MAX_TOTAL) return out;
|
|
17465
17397
|
const queue = [];
|
|
17466
|
-
for (const
|
|
17467
|
-
const sp = trySplitRepo(
|
|
17468
|
-
if (sp) queue.push({ owner: sp.owner, name: sp.name, number:
|
|
17398
|
+
for (const child2 of out) {
|
|
17399
|
+
const sp = trySplitRepo(child2.repo);
|
|
17400
|
+
if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child2.number, depth: 1 });
|
|
17469
17401
|
}
|
|
17470
17402
|
while (queue.length && out.length < CHILDREN_MAX_TOTAL) {
|
|
17471
17403
|
const frame = queue.shift();
|
|
@@ -17478,13 +17410,13 @@ async function runIssueChildren(deps, epic, opts) {
|
|
|
17478
17410
|
}
|
|
17479
17411
|
for (const raw of nodes) {
|
|
17480
17412
|
if (out.length >= CHILDREN_MAX_TOTAL) break;
|
|
17481
|
-
const
|
|
17482
|
-
const key = childKey(
|
|
17413
|
+
const child2 = shapeChildNode(raw, frame.depth + 1, boardProjectId);
|
|
17414
|
+
const key = childKey(child2.repo, child2.number);
|
|
17483
17415
|
if (seen.has(key)) continue;
|
|
17484
17416
|
seen.add(key);
|
|
17485
|
-
out.push(
|
|
17486
|
-
const sp = trySplitRepo(
|
|
17487
|
-
if (sp) queue.push({ owner: sp.owner, name: sp.name, number:
|
|
17417
|
+
out.push(child2);
|
|
17418
|
+
const sp = trySplitRepo(child2.repo);
|
|
17419
|
+
if (sp) queue.push({ owner: sp.owner, name: sp.name, number: child2.number, depth: frame.depth + 1 });
|
|
17488
17420
|
}
|
|
17489
17421
|
}
|
|
17490
17422
|
return out;
|
|
@@ -18235,7 +18167,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18235
18167
|
projectMeta: meta,
|
|
18236
18168
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
18237
18169
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
|
|
18238
|
-
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
18170
|
+
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
18239
18171
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
18240
18172
|
requiredGcpApis: (() => {
|
|
18241
18173
|
const v = meta?.requiredGcpApis;
|
|
@@ -18267,7 +18199,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18267
18199
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
18268
18200
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
18269
18201
|
});
|
|
18270
|
-
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) => {
|
|
18271
18203
|
const o = {
|
|
18272
18204
|
class: rawValue("--class", "deployable"),
|
|
18273
18205
|
projectType: rawValue("--project-type", ""),
|
|
@@ -18583,12 +18515,12 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
18583
18515
|
function shellFor(platform2 = process.platform) {
|
|
18584
18516
|
return platform2 === "win32" ? "powershell" : "bash";
|
|
18585
18517
|
}
|
|
18586
|
-
function
|
|
18518
|
+
function isCentralContainerModel(model) {
|
|
18587
18519
|
return model === "tenant-container" || model === "solo-container";
|
|
18588
18520
|
}
|
|
18589
18521
|
function deriveStageGap(inputs) {
|
|
18590
18522
|
const missing = [];
|
|
18591
|
-
if (!
|
|
18523
|
+
if (!isCentralContainerModel(inputs.deployModel)) {
|
|
18592
18524
|
return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
18593
18525
|
}
|
|
18594
18526
|
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
@@ -18770,7 +18702,7 @@ function registerStageCommands(program3) {
|
|
|
18770
18702
|
const read = await fetchProjectBySlugChecked(slug, reg);
|
|
18771
18703
|
const decision = decidePortRange({ metaReadOk: read.ok, metaPortRange: read.ok ? metaPortRange(read.project) : null });
|
|
18772
18704
|
if (decision.action === "fail") {
|
|
18773
|
-
return failGraceful(`port-range: ${decision.reason}${read.ok ? "" : ` (${read.error})`}`);
|
|
18705
|
+
return failGraceful(`stage port-range: ${decision.reason}${read.ok ? "" : ` (${read.error})`}`);
|
|
18774
18706
|
}
|
|
18775
18707
|
if (decision.action === "return") {
|
|
18776
18708
|
const [start2, end2] = decision.range;
|
|
@@ -18780,7 +18712,7 @@ function registerStageCommands(program3) {
|
|
|
18780
18712
|
const infra = resolvePortRangeInfra(process.cwd(), __dirname);
|
|
18781
18713
|
if (!infra) {
|
|
18782
18714
|
return failGraceful(
|
|
18783
|
-
`port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
|
|
18715
|
+
`stage port-range: no MMI-Hub allocator files found (checked cwd ${process.cwd()}, sibling MMI-Hub dirs, and the installed package location); ensure the Hub's infra/port-ranges.json and infra/port-ddb.mjs are reachable`
|
|
18784
18716
|
);
|
|
18785
18717
|
}
|
|
18786
18718
|
const path2 = infra.registryPath;
|
|
@@ -18793,7 +18725,7 @@ function registerStageCommands(program3) {
|
|
|
18793
18725
|
const { range: [start, end], source } = await ensurePortRangeAtomic(repo, path2, allocate);
|
|
18794
18726
|
const write = await upsertProject(slug, { portRange: { start, end } }, reg);
|
|
18795
18727
|
if (!write.ok && source === "ddb") {
|
|
18796
|
-
return failGraceful(`port-range: block [${start}, ${end}] was allocated (cursor advanced) but NOT recorded in the registry META (${write.error ?? `HTTP ${write.status}`}) \u2014 fix auth/connectivity and retry so the block is persisted; do not re-run blind`);
|
|
18728
|
+
return failGraceful(`stage port-range: block [${start}, ${end}] was allocated (cursor advanced) but NOT recorded in the registry META (${write.error ?? `HTTP ${write.status}`}) \u2014 fix auth/connectivity and retry so the block is persisted; do not re-run blind`);
|
|
18797
18729
|
}
|
|
18798
18730
|
if (o.json) {
|
|
18799
18731
|
printLine(JSON.stringify({ repo, portRange: [start, end], source: "allocated", persisted: write.ok, ...write.ok ? {} : { persistError: write.error ?? `HTTP ${write.status}` } }));
|
|
@@ -18833,7 +18765,7 @@ function registerStageCommands(program3) {
|
|
|
18833
18765
|
},
|
|
18834
18766
|
control: async ({ repo, action, host, ip }) => {
|
|
18835
18767
|
const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
|
|
18836
|
-
if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
18768
|
+
if (!res.ok) throw new Error(`runtime tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
18837
18769
|
}
|
|
18838
18770
|
};
|
|
18839
18771
|
try {
|
|
@@ -19060,7 +18992,7 @@ function registerBoardCommands(program3) {
|
|
|
19060
18992
|
expected: [...BOARD_STATUSES]
|
|
19061
18993
|
});
|
|
19062
18994
|
}
|
|
19063
|
-
async function runBulkMove(issueRefs, o, status
|
|
18995
|
+
async function runBulkMove(issueRefs, o, status) {
|
|
19064
18996
|
try {
|
|
19065
18997
|
const bulk = await moveBoardIssues({
|
|
19066
18998
|
config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
|
|
@@ -19078,7 +19010,7 @@ function registerBoardCommands(program3) {
|
|
|
19078
19010
|
}
|
|
19079
19011
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
19080
19012
|
} catch (e) {
|
|
19081
|
-
return failGraceful(
|
|
19013
|
+
return failGraceful(`board move failed: ${e.message}`);
|
|
19082
19014
|
}
|
|
19083
19015
|
}
|
|
19084
19016
|
withExamples(mutating(
|
|
@@ -19100,7 +19032,7 @@ function registerBoardCommands(program3) {
|
|
|
19100
19032
|
}
|
|
19101
19033
|
return;
|
|
19102
19034
|
}
|
|
19103
|
-
await runBulkMove(issueRefs, o, canonicalStatus
|
|
19035
|
+
await runBulkMove(issueRefs, o, canonicalStatus);
|
|
19104
19036
|
}), [
|
|
19105
19037
|
'mmi-cli board move "In Progress" 2680',
|
|
19106
19038
|
"mmi-cli board move Done 2680 2681 2682"
|
|
@@ -19133,19 +19065,6 @@ function registerBoardCommands(program3) {
|
|
|
19133
19065
|
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19134
19066
|
}
|
|
19135
19067
|
});
|
|
19136
|
-
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) => {
|
|
19137
|
-
if (issueRefs.length === 1) {
|
|
19138
|
-
try {
|
|
19139
|
-
const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
19140
|
-
if (o.json) return console.log(JSON.stringify(result));
|
|
19141
|
-
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
|
|
19142
|
-
} catch (e) {
|
|
19143
|
-
return failGraceful(`board done failed: ${e.message}`);
|
|
19144
|
-
}
|
|
19145
|
-
return;
|
|
19146
|
-
}
|
|
19147
|
-
await runBulkMove(issueRefs, o, "Done", "board done");
|
|
19148
|
-
});
|
|
19149
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) => {
|
|
19150
19069
|
const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
|
|
19151
19070
|
try {
|
|
@@ -19443,7 +19362,7 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
|
|
|
19443
19362
|
});
|
|
19444
19363
|
}
|
|
19445
19364
|
function renderGcApplyResult(result) {
|
|
19446
|
-
const lines = ["gc apply result:"];
|
|
19365
|
+
const lines = ["worktree gc apply result:"];
|
|
19447
19366
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
19448
19367
|
lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
|
|
19449
19368
|
lines.push(` tracking refs removed: ${result.removedTrackingRefs.length ? result.removedTrackingRefs.join(", ") : "none"}`);
|
|
@@ -19488,7 +19407,7 @@ function evaluatePrMergeHousekeeping(report, context, force = false) {
|
|
|
19488
19407
|
const detail = failed ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
|
|
19489
19408
|
return {
|
|
19490
19409
|
blocked: true,
|
|
19491
|
-
reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
|
|
19410
|
+
reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli worktree gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
|
|
19492
19411
|
};
|
|
19493
19412
|
}
|
|
19494
19413
|
function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
@@ -19650,7 +19569,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
19650
19569
|
}
|
|
19651
19570
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
19652
19571
|
function spawnDeferredGcSweep() {
|
|
19653
|
-
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] });
|
|
19654
19573
|
}
|
|
19655
19574
|
async function createDeferredWorktreeStore() {
|
|
19656
19575
|
try {
|
|
@@ -19907,7 +19826,7 @@ function classifyStaleLeaks(input) {
|
|
|
19907
19826
|
kind: "stale-worktree",
|
|
19908
19827
|
ref: wt.branch,
|
|
19909
19828
|
detail: `merged/closed branch ${wt.branch} still checked out at ${wt.path}`,
|
|
19910
|
-
remediation: `cd "${wt.path}" && mmi-cli worktree land --apply (or: mmi-cli gc --apply)`
|
|
19829
|
+
remediation: `cd "${wt.path}" && mmi-cli worktree land --apply (or: mmi-cli worktree gc --apply)`
|
|
19911
19830
|
});
|
|
19912
19831
|
}
|
|
19913
19832
|
for (const branch of input.localBranches) {
|
|
@@ -19920,7 +19839,7 @@ function classifyStaleLeaks(input) {
|
|
|
19920
19839
|
kind: "stale-branch",
|
|
19921
19840
|
ref: branch,
|
|
19922
19841
|
detail: `merged/closed branch ${branch} has no worktree`,
|
|
19923
|
-
remediation: "mmi-cli gc --apply"
|
|
19842
|
+
remediation: "mmi-cli worktree gc --apply"
|
|
19924
19843
|
});
|
|
19925
19844
|
}
|
|
19926
19845
|
for (const wt of input.worktrees) {
|
|
@@ -19944,7 +19863,7 @@ function classifyStaleLeaks(input) {
|
|
|
19944
19863
|
kind: "orphan-dir",
|
|
19945
19864
|
ref: dir,
|
|
19946
19865
|
detail: `directory under the worktrees root is not a registered worktree`,
|
|
19947
|
-
remediation: `mmi-cli gc --apply (or: Remove-Item/rm -rf "${dir}")`
|
|
19866
|
+
remediation: `mmi-cli worktree gc --apply (or: Remove-Item/rm -rf "${dir}")`
|
|
19948
19867
|
});
|
|
19949
19868
|
}
|
|
19950
19869
|
}
|
|
@@ -20738,9 +20657,9 @@ function buildPluginGuardDecision(i) {
|
|
|
20738
20657
|
if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
|
|
20739
20658
|
return { state: "healthy" };
|
|
20740
20659
|
}
|
|
20741
|
-
function
|
|
20660
|
+
function buildPluginGuardLine(state, opts = {}) {
|
|
20742
20661
|
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
20743
|
-
const recovery = opts.recovery ?? "mmi-cli plugin
|
|
20662
|
+
const recovery = opts.recovery ?? "mmi-cli plugin heal";
|
|
20744
20663
|
const restartHint = opts.restartHint ?? "restart your agent host / reload plugins";
|
|
20745
20664
|
const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
|
|
20746
20665
|
return {
|
|
@@ -20945,8 +20864,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
20945
20864
|
}
|
|
20946
20865
|
return true;
|
|
20947
20866
|
}
|
|
20948
|
-
async function runGuard(
|
|
20949
|
-
void opts;
|
|
20867
|
+
async function runGuard(readOrigin) {
|
|
20950
20868
|
try {
|
|
20951
20869
|
const surface = detectSurface(process.env);
|
|
20952
20870
|
if (surfaceToken(surface) !== "claude") {
|
|
@@ -20956,7 +20874,7 @@ async function runGuard(opts = {}, readOrigin) {
|
|
|
20956
20874
|
const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
|
|
20957
20875
|
const input = snapshotPluginGuardInput(surface, isOrgRepo);
|
|
20958
20876
|
const { state } = buildPluginGuardDecision(input);
|
|
20959
|
-
const { line, exitCode } =
|
|
20877
|
+
const { line, exitCode } = buildPluginGuardLine(state);
|
|
20960
20878
|
if (line) console.error(line);
|
|
20961
20879
|
process.exitCode = exitCode;
|
|
20962
20880
|
} catch {
|
|
@@ -21097,7 +21015,7 @@ function formatLastRun(r) {
|
|
|
21097
21015
|
}
|
|
21098
21016
|
function formatDeployStatus(r) {
|
|
21099
21017
|
const lines = [
|
|
21100
|
-
`deploy status \u2014 ${r.repo} (${r.slug}) stage ${r.stage}`,
|
|
21018
|
+
`runtime deploy status \u2014 ${r.repo} (${r.slug}) stage ${r.stage}`,
|
|
21101
21019
|
"",
|
|
21102
21020
|
`running version: ${r.runningVersion ?? "none stamped"}`,
|
|
21103
21021
|
`last deploy run: ${formatLastRun(r)}`,
|
|
@@ -21112,7 +21030,7 @@ function formatDeployStatus(r) {
|
|
|
21112
21030
|
}
|
|
21113
21031
|
|
|
21114
21032
|
// src/deploy-commands.ts
|
|
21115
|
-
var
|
|
21033
|
+
var STAGES = ["dev", "rc", "main"];
|
|
21116
21034
|
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
21117
21035
|
const controller = new AbortController();
|
|
21118
21036
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -21129,8 +21047,8 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
|
21129
21047
|
function registerDeployCommands(program3) {
|
|
21130
21048
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
21131
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) => {
|
|
21132
|
-
if (!
|
|
21133
|
-
return fail(`deploy status: <stage> must be dev, rc, or main`);
|
|
21050
|
+
if (!STAGES.includes(stage)) {
|
|
21051
|
+
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
21134
21052
|
}
|
|
21135
21053
|
try {
|
|
21136
21054
|
const cfg = await loadConfig();
|
|
@@ -21159,7 +21077,7 @@ function registerDeployCommands(program3) {
|
|
|
21159
21077
|
}
|
|
21160
21078
|
if (report.health?.ok === false) process.exitCode = 1;
|
|
21161
21079
|
} catch (e) {
|
|
21162
|
-
return failGraceful(`deploy status: ${e.message}`);
|
|
21080
|
+
return failGraceful(`runtime deploy status: ${e.message}`);
|
|
21163
21081
|
}
|
|
21164
21082
|
});
|
|
21165
21083
|
}
|
|
@@ -21278,7 +21196,7 @@ async function collectOnboardStatus() {
|
|
|
21278
21196
|
if (read.ok && read.project) {
|
|
21279
21197
|
registry2 = { ok: true, detail: `project "${read.project.name ?? slug}" registered` };
|
|
21280
21198
|
} else if (read.ok) {
|
|
21281
|
-
registry2 = { ok: false, detail: `repo "${slug}" not registered \u2014 run mmi-cli bootstrap or mmi-cli project set <owner/repo>` };
|
|
21199
|
+
registry2 = { ok: false, detail: `repo "${slug}" not registered \u2014 run mmi-cli bootstrap or mmi-cli org project set <owner/repo>` };
|
|
21282
21200
|
} else {
|
|
21283
21201
|
registry2 = { ok: false, detail: `read failed: ${read.error}` };
|
|
21284
21202
|
}
|
|
@@ -21305,7 +21223,7 @@ async function collectOnboardStatus() {
|
|
|
21305
21223
|
if (!cfg.sagaApiUrl) {
|
|
21306
21224
|
nextCommand = "mmi-cli doctor \u2014 fix Hub API URL configuration first";
|
|
21307
21225
|
} else if (!registry2.ok) {
|
|
21308
|
-
nextCommand = "mmi-cli project set <owner/repo>";
|
|
21226
|
+
nextCommand = "mmi-cli org project set <owner/repo>";
|
|
21309
21227
|
} else if (!board.ok) {
|
|
21310
21228
|
nextCommand = "mmi-cli board read";
|
|
21311
21229
|
} else {
|
|
@@ -21374,10 +21292,11 @@ var LOOP_PLAYBOOKS = {
|
|
|
21374
21292
|
agent: {
|
|
21375
21293
|
title: "Agent",
|
|
21376
21294
|
steps: [
|
|
21377
|
-
{ label: "
|
|
21295
|
+
{ label: "Orient in the current repository", command: "mmi-cli onboard" },
|
|
21378
21296
|
{ label: "Read the next board item", command: "mmi-cli board read" },
|
|
21379
|
-
{ label: "
|
|
21297
|
+
{ label: "Claim it and create an isolated worktree", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
|
|
21380
21298
|
{ label: "Build and test in the touched package", command: "npm test && npm run build" },
|
|
21299
|
+
{ label: "Publish the branch", command: "git push -u origin HEAD" },
|
|
21381
21300
|
{ label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
|
|
21382
21301
|
{ label: "Wait for checks and land to development", command: "mmi-cli pr checks-wait <PR-number> && mmi-cli pr land <PR-number>" },
|
|
21383
21302
|
{ label: "Release only after the gated train is authorized", command: "mmi-cli release --apply" }
|
|
@@ -21386,16 +21305,17 @@ var LOOP_PLAYBOOKS = {
|
|
|
21386
21305
|
"start-work": {
|
|
21387
21306
|
title: "Start Work",
|
|
21388
21307
|
steps: [
|
|
21389
|
-
{ label: "
|
|
21390
|
-
{ label: "
|
|
21391
|
-
{ label: "Claim the
|
|
21308
|
+
{ label: "Orient in the current repository", command: "mmi-cli onboard" },
|
|
21309
|
+
{ label: "Read the board item", command: "mmi-cli board show <issue-number>" },
|
|
21310
|
+
{ label: "Claim it and create the worktree from development", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
|
|
21392
21311
|
{ label: "Start a local stage (deployable repos)", command: "mmi-cli stage run --apply" }
|
|
21393
21312
|
]
|
|
21394
21313
|
},
|
|
21395
21314
|
"ship-pr": {
|
|
21396
21315
|
title: "Ship PR",
|
|
21397
21316
|
steps: [
|
|
21398
|
-
{ label: "
|
|
21317
|
+
{ label: "Publish the branch", command: "git push -u origin HEAD" },
|
|
21318
|
+
{ label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
|
|
21399
21319
|
{ label: "Wait for CI checks", command: "mmi-cli pr checks-wait <PR-number>" },
|
|
21400
21320
|
{ label: "Land the PR (merge to development)", command: "mmi-cli pr land <PR-number>" }
|
|
21401
21321
|
]
|
|
@@ -21403,9 +21323,9 @@ var LOOP_PLAYBOOKS = {
|
|
|
21403
21323
|
"hotfix": {
|
|
21404
21324
|
title: "Hotfix",
|
|
21405
21325
|
steps: [
|
|
21406
|
-
{ label: "
|
|
21407
|
-
{ label: "
|
|
21408
|
-
{ label: "
|
|
21326
|
+
{ label: "Create the main-base hotfix PR from an already-merged fix", command: "mmi-cli hotfix start --from <pr#|sha>" },
|
|
21327
|
+
{ label: "Wait for the hotfix PR checks", command: "mmi-cli pr checks-wait <PR-number>" },
|
|
21328
|
+
{ label: "After the PR is merged, run the gated release", command: "mmi-cli hotfix release <vX.Y.Z> --carries <pr#|sha>" }
|
|
21409
21329
|
]
|
|
21410
21330
|
}
|
|
21411
21331
|
};
|
|
@@ -21455,6 +21375,22 @@ function formatExplainCommand(cmd, rootName) {
|
|
|
21455
21375
|
}
|
|
21456
21376
|
return lines.join("\n").trimEnd();
|
|
21457
21377
|
}
|
|
21378
|
+
function formatExplainGroup(cmd, rootName) {
|
|
21379
|
+
const lines = [
|
|
21380
|
+
`${rootName} ${cmd.path}${cmd.description ? ` \u2014 ${cmd.description}` : ""}`,
|
|
21381
|
+
`Category: ${cmd.category} \xB7 Discovery: ${cmd.discovery}`,
|
|
21382
|
+
"",
|
|
21383
|
+
"Commands:"
|
|
21384
|
+
];
|
|
21385
|
+
for (const child2 of cmd.subcommands) {
|
|
21386
|
+
const args = child2.arguments.map((arg) => {
|
|
21387
|
+
const name = arg.variadic ? `${arg.name}...` : arg.name;
|
|
21388
|
+
return arg.required ? `<${name}>` : `[${name}]`;
|
|
21389
|
+
}).join(" ");
|
|
21390
|
+
lines.push(` ${child2.path}${args ? ` ${args}` : ""}${child2.description ? ` \u2014 ${child2.description}` : ""}`);
|
|
21391
|
+
}
|
|
21392
|
+
return lines.join("\n");
|
|
21393
|
+
}
|
|
21458
21394
|
function formatExplainLoop(playbook) {
|
|
21459
21395
|
const lines = [`${playbook.title} loop:`];
|
|
21460
21396
|
for (const step of playbook.steps) {
|
|
@@ -21464,7 +21400,15 @@ function formatExplainLoop(playbook) {
|
|
|
21464
21400
|
return lines.join("\n");
|
|
21465
21401
|
}
|
|
21466
21402
|
function findCommandInManifest(manifest, commandPath2) {
|
|
21467
|
-
|
|
21403
|
+
const visit = (command) => {
|
|
21404
|
+
if (command.path === commandPath2) return command;
|
|
21405
|
+
for (const child2 of command.subcommands) {
|
|
21406
|
+
const found = visit(child2);
|
|
21407
|
+
if (found) return found;
|
|
21408
|
+
}
|
|
21409
|
+
return void 0;
|
|
21410
|
+
};
|
|
21411
|
+
return visit(manifest.tree);
|
|
21468
21412
|
}
|
|
21469
21413
|
function registerExplainCommand(program3) {
|
|
21470
21414
|
program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").action((commandArgs, opts) => {
|
|
@@ -21479,18 +21423,16 @@ function registerExplainCommand(program3) {
|
|
|
21479
21423
|
}
|
|
21480
21424
|
const manifest = buildCommandManifest(program3);
|
|
21481
21425
|
if (!commandArgs.length) {
|
|
21482
|
-
console.log(
|
|
21483
|
-
console.log(manifest.tree.description ?? "");
|
|
21484
|
-
console.log("\nRun `mmi-cli commands` to list every subcommand + its flags.");
|
|
21426
|
+
console.log(formatManifestHuman(manifest));
|
|
21485
21427
|
return;
|
|
21486
21428
|
}
|
|
21487
21429
|
const commandPath2 = commandArgs.join(" ");
|
|
21488
|
-
const
|
|
21489
|
-
if (!
|
|
21430
|
+
const command = findCommandInManifest(manifest, commandPath2);
|
|
21431
|
+
if (!command) {
|
|
21490
21432
|
fail(`explain: unknown command "${commandPath2}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
|
|
21491
21433
|
return;
|
|
21492
21434
|
}
|
|
21493
|
-
console.log(formatExplainCommand(
|
|
21435
|
+
console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
|
|
21494
21436
|
});
|
|
21495
21437
|
}
|
|
21496
21438
|
|
|
@@ -22127,7 +22069,7 @@ function checkClaudePlugin(probe) {
|
|
|
22127
22069
|
return {
|
|
22128
22070
|
ok: false,
|
|
22129
22071
|
label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
|
|
22130
|
-
fix: "run `mmi-cli plugin
|
|
22072
|
+
fix: "run `mmi-cli plugin heal` to reinstall the MMI marketplace + plugin, then restart Claude",
|
|
22131
22073
|
verbose: evidence
|
|
22132
22074
|
};
|
|
22133
22075
|
}
|
|
@@ -22186,7 +22128,7 @@ function checkPluginCache(input) {
|
|
|
22186
22128
|
// Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
|
|
22187
22129
|
// on its own so a cache with zero versioned dirs still gets an honest ✗.
|
|
22188
22130
|
detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
|
|
22189
|
-
fix: "run `mmi-cli plugin
|
|
22131
|
+
fix: "run `mmi-cli plugin prune` to review, then `mmi-cli plugin prune --apply` to delete",
|
|
22190
22132
|
verbose: evidence
|
|
22191
22133
|
};
|
|
22192
22134
|
}
|
|
@@ -22216,7 +22158,7 @@ function checkSchedules(probe) {
|
|
|
22216
22158
|
ok: false,
|
|
22217
22159
|
label: "schedules",
|
|
22218
22160
|
detail: `${probe.incomplete.length} source(s) unreadable`,
|
|
22219
|
-
fix: "run `mmi-cli schedules` for the full report \u2014 the notebook may be missing armed entries",
|
|
22161
|
+
fix: "run `mmi-cli org schedules` for the full report \u2014 the notebook may be missing armed entries",
|
|
22220
22162
|
verbose: evidence
|
|
22221
22163
|
};
|
|
22222
22164
|
}
|
|
@@ -22225,7 +22167,7 @@ function checkSchedules(probe) {
|
|
|
22225
22167
|
ok: false,
|
|
22226
22168
|
label: "schedules",
|
|
22227
22169
|
detail: `${probe.drift.length} drift finding(s)`,
|
|
22228
|
-
fix: "run `mmi-cli schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
|
|
22170
|
+
fix: "run `mmi-cli org schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
|
|
22229
22171
|
verbose: evidence
|
|
22230
22172
|
};
|
|
22231
22173
|
}
|
|
@@ -22581,11 +22523,13 @@ function allLongFlags() {
|
|
|
22581
22523
|
if (cachedLongFlags) return cachedLongFlags;
|
|
22582
22524
|
const acc = /* @__PURE__ */ new Set();
|
|
22583
22525
|
const walk = (node) => {
|
|
22526
|
+
if (!isCanonicalSuggestion(node)) return;
|
|
22584
22527
|
for (const opt of node.options) {
|
|
22528
|
+
if (opt.discovery) continue;
|
|
22585
22529
|
const m = /--[\w-]+/.exec(opt.flags);
|
|
22586
22530
|
if (m) acc.add(m[0]);
|
|
22587
22531
|
}
|
|
22588
|
-
for (const
|
|
22532
|
+
for (const child2 of node.subcommands) walk(child2);
|
|
22589
22533
|
};
|
|
22590
22534
|
walk(buildCommandManifest(program2).tree);
|
|
22591
22535
|
cachedLongFlags = [...acc];
|
|
@@ -22621,7 +22565,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
22621
22565
|
process.stderr.write(str);
|
|
22622
22566
|
}
|
|
22623
22567
|
var program2 = new Command();
|
|
22624
|
-
program2.name("mmi-cli").description("MMI Future CLI \u2014 org
|
|
22568
|
+
program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError("(run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
|
|
22625
22569
|
function appActorDeps() {
|
|
22626
22570
|
return {
|
|
22627
22571
|
fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
|
|
@@ -22651,22 +22595,26 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
22651
22595
|
if (opts.write) {
|
|
22652
22596
|
if (plan.changed) {
|
|
22653
22597
|
(0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
|
|
22654
|
-
console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
|
|
22598
|
+
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
22655
22599
|
} else {
|
|
22656
|
-
console.log("mmi-cli rules gitignore: up to date");
|
|
22600
|
+
console.log("mmi-cli org rules gitignore: up to date");
|
|
22657
22601
|
}
|
|
22658
22602
|
return;
|
|
22659
22603
|
}
|
|
22660
22604
|
if (plan.changed) {
|
|
22661
|
-
console.error(`mmi-cli rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli rules gitignore --write\` and commit`);
|
|
22605
|
+
console.error(`mmi-cli org rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli org rules gitignore --write\` and commit`);
|
|
22662
22606
|
process.exitCode = 1;
|
|
22663
22607
|
} else {
|
|
22664
|
-
console.log("mmi-cli rules gitignore: up to date");
|
|
22608
|
+
console.log("mmi-cli org rules gitignore: up to date");
|
|
22665
22609
|
}
|
|
22666
22610
|
});
|
|
22667
|
-
program2.command("commands").description("print the
|
|
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) => {
|
|
22668
22612
|
const manifest = buildCommandManifest(program2);
|
|
22669
|
-
|
|
22613
|
+
if (o.json) {
|
|
22614
|
+
consoleIo.log(JSON.stringify(o.primary ? primaryCommandManifest(manifest) : manifest, null, 2));
|
|
22615
|
+
return;
|
|
22616
|
+
}
|
|
22617
|
+
consoleIo.log(formatManifestHuman(manifest, { all: o.all }));
|
|
22670
22618
|
});
|
|
22671
22619
|
async function runWhoami(io = consoleIo) {
|
|
22672
22620
|
const cfg = await loadConfig();
|
|
@@ -22743,21 +22691,21 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
22743
22691
|
);
|
|
22744
22692
|
if (o.json) return console.log(JSON.stringify(result));
|
|
22745
22693
|
if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
|
|
22746
|
-
if (result.removed.length) console.log(`gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
|
|
22747
|
-
if (result.stillDeferred.length) console.log(`gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
|
|
22748
|
-
if (result.skipped.length) console.log(`gc sweep-deferred: ${result.skipped.length} dropped (repurposed for different work since queued, left untouched)`);
|
|
22749
|
-
if (!result.removed.length && !result.stillDeferred.length && !result.skipped.length) console.log("gc sweep-deferred: nothing queued");
|
|
22694
|
+
if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
|
|
22695
|
+
if (result.stillDeferred.length) console.log(`worktree gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
|
|
22696
|
+
if (result.skipped.length) console.log(`worktree gc sweep-deferred: ${result.skipped.length} dropped (repurposed for different work since queued, left untouched)`);
|
|
22697
|
+
if (!result.removed.length && !result.stillDeferred.length && !result.skipped.length) console.log("worktree gc sweep-deferred: nothing queued");
|
|
22750
22698
|
}
|
|
22751
22699
|
} catch (e) {
|
|
22752
|
-
fail(`gc sweep-deferred: ${e.message}`);
|
|
22700
|
+
fail(`worktree gc sweep-deferred: ${e.message}`);
|
|
22753
22701
|
}
|
|
22754
22702
|
}, DEFERRED_SWEEP_HARD_TIMEOUT_MS, () => {
|
|
22755
|
-
console.error("gc sweep-deferred: exceeded hard timeout \u2014 exiting so the detached worker cannot leak (#2841)");
|
|
22703
|
+
console.error("worktree gc sweep-deferred: exceeded hard timeout \u2014 exiting so the detached worker cannot leak (#2841)");
|
|
22756
22704
|
process.exit(process.exitCode ?? 0);
|
|
22757
22705
|
});
|
|
22758
22706
|
});
|
|
22759
22707
|
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch and plans whose current hash is confirmed synced to North Star (#1864) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
22760
|
-
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
22708
|
+
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
22761
22709
|
if (o.scratch) {
|
|
22762
22710
|
try {
|
|
22763
22711
|
const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
@@ -22765,11 +22713,11 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
22765
22713
|
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
|
|
22766
22714
|
return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply), run.applied));
|
|
22767
22715
|
} catch (e) {
|
|
22768
|
-
return fail(`gc --scratch: ${e.message}`);
|
|
22716
|
+
return fail(`worktree gc --scratch: ${e.message}`);
|
|
22769
22717
|
}
|
|
22770
22718
|
}
|
|
22771
22719
|
const limit = Number.parseInt(o.limit, 10);
|
|
22772
|
-
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
22720
|
+
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
22773
22721
|
try {
|
|
22774
22722
|
const plan = await gcPlan(o.remote, limit);
|
|
22775
22723
|
if (o.apply && !o.json) console.log(formatGcPlan(plan, false));
|
|
@@ -22794,7 +22742,7 @@ ${renderGcApplyResult(applyResult)}`);
|
|
|
22794
22742
|
}
|
|
22795
22743
|
if (applyResult?.failed.length) process.exitCode = 1;
|
|
22796
22744
|
} catch (e) {
|
|
22797
|
-
fail(`gc: ${e.message}`);
|
|
22745
|
+
fail(`worktree gc: ${e.message}`);
|
|
22798
22746
|
}
|
|
22799
22747
|
});
|
|
22800
22748
|
var NPM_PROVISION_TIMEOUT_MS = 3e5;
|
|
@@ -22804,19 +22752,19 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
22804
22752
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
22805
22753
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
22806
22754
|
return new Promise((resolve5, reject) => {
|
|
22807
|
-
const
|
|
22755
|
+
const child2 = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
22808
22756
|
const timer = setTimeout(() => {
|
|
22809
22757
|
try {
|
|
22810
|
-
|
|
22758
|
+
child2.kill();
|
|
22811
22759
|
} catch {
|
|
22812
22760
|
}
|
|
22813
22761
|
reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
|
|
22814
22762
|
}, NPM_PROVISION_TIMEOUT_MS);
|
|
22815
|
-
|
|
22763
|
+
child2.on("error", (e) => {
|
|
22816
22764
|
clearTimeout(timer);
|
|
22817
22765
|
reject(e);
|
|
22818
22766
|
});
|
|
22819
|
-
|
|
22767
|
+
child2.on("exit", (code) => {
|
|
22820
22768
|
clearTimeout(timer);
|
|
22821
22769
|
if (code === 0) resolve5();
|
|
22822
22770
|
else reject(new Error(`${command} exited ${code} in ${cwd}`));
|
|
@@ -22994,7 +22942,7 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
22994
22942
|
return { onBoard: false };
|
|
22995
22943
|
}
|
|
22996
22944
|
if (!cfg.projectId) {
|
|
22997
|
-
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
22945
|
+
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli org project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
22998
22946
|
return { onBoard: false };
|
|
22999
22947
|
}
|
|
23000
22948
|
try {
|
|
@@ -23074,7 +23022,7 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
|
|
|
23074
23022
|
if (o.outcome === "clean") outcome = { kind: "clean" };
|
|
23075
23023
|
else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
|
|
23076
23024
|
else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
|
|
23077
|
-
else return failGraceful(`docs
|
|
23025
|
+
else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
|
|
23078
23026
|
const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
|
|
23079
23027
|
await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
|
|
23080
23028
|
} catch (e) {
|
|
@@ -23102,50 +23050,56 @@ async function reportWrite(label, res) {
|
|
|
23102
23050
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
23103
23051
|
}
|
|
23104
23052
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
23105
|
-
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) => {
|
|
23106
23054
|
try {
|
|
23107
23055
|
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch });
|
|
23108
23056
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
23109
23057
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
23110
23058
|
const { lines, failure } = renderVerifySecrets(body);
|
|
23111
23059
|
for (const line of lines) printLine(line);
|
|
23112
|
-
if (failure) return failGraceful(`tenant control ${stage} verify-secrets: ${failure}`);
|
|
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}`);
|
|
23113
23065
|
} else {
|
|
23114
23066
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
23115
23067
|
}
|
|
23116
23068
|
if (result.conclusion === "failure") {
|
|
23117
|
-
return failGraceful(`tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
|
|
23069
|
+
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
|
|
23118
23070
|
}
|
|
23119
23071
|
} catch (e) {
|
|
23120
|
-
return failGraceful(`tenant control: ${e.message}`);
|
|
23072
|
+
return failGraceful(`runtime tenant control: ${e.message}`);
|
|
23121
23073
|
}
|
|
23122
23074
|
});
|
|
23123
|
-
tenant.command("
|
|
23124
|
-
|
|
23125
|
-
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
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
|
+
}
|
|
23129
23083
|
});
|
|
23130
|
-
tenant.command("
|
|
23131
|
-
if (!["dev", "rc", "main"].includes(stage)) return fail("tenant
|
|
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) => {
|
|
23085
|
+
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
23132
23086
|
const cfg = await loadConfig();
|
|
23133
23087
|
const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
23134
23088
|
console.log(JSON.stringify(result, null, 2));
|
|
23135
23089
|
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
23136
23090
|
});
|
|
23137
23091
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev \u2014 the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
23138
|
-
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("tenant redeploy: <stage> must be dev, rc, or main");
|
|
23092
|
+
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime tenant redeploy: <stage> must be dev, rc, or main");
|
|
23139
23093
|
try {
|
|
23140
23094
|
const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage, ref: o.ref, watch: o.watch });
|
|
23141
23095
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
|
|
23142
23096
|
} catch (e) {
|
|
23143
|
-
return failGraceful(`tenant redeploy: ${e.message}`);
|
|
23097
|
+
return failGraceful(`runtime tenant redeploy: ${e.message}`);
|
|
23144
23098
|
}
|
|
23145
23099
|
});
|
|
23146
23100
|
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers \u2014 orphan cleanup after a failed post-release retire (#942)").option("--retire", "retire every running rc runtime found (requires --yes) \u2014 WARNING: tears down a legitimately-staged rc too").option("--yes", "confirm the destructive --retire").option("--json", "machine-readable output").action(async (o) => {
|
|
23147
23101
|
if (o.retire && !o.yes) {
|
|
23148
|
-
return fail("tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
|
|
23102
|
+
return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
|
|
23149
23103
|
}
|
|
23150
23104
|
const cfg = await loadConfig();
|
|
23151
23105
|
const cdeps = registryClientDeps(cfg);
|
|
@@ -23165,17 +23119,9 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
23165
23119
|
}, { retire: !!o.retire });
|
|
23166
23120
|
return printLine(o.json ? JSON.stringify(result) : renderSweep(result));
|
|
23167
23121
|
} catch (e) {
|
|
23168
|
-
return failGraceful(`tenant sweep-rc: ${e.message}`);
|
|
23122
|
+
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
23169
23123
|
}
|
|
23170
23124
|
});
|
|
23171
|
-
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
23172
|
-
const { lookup } = await import("node:dns/promises");
|
|
23173
|
-
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
23174
|
-
const timeout = new Promise((resolve5) => {
|
|
23175
|
-
setTimeout(() => resolve5(void 0), timeoutMs).unref?.();
|
|
23176
|
-
});
|
|
23177
|
-
return Promise.race([probe, timeout]);
|
|
23178
|
-
}
|
|
23179
23125
|
async function probeHttpBounded2(url, timeoutMs = 5e3) {
|
|
23180
23126
|
const controller = new AbortController();
|
|
23181
23127
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -23206,75 +23152,14 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
|
23206
23152
|
lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
|
|
23207
23153
|
});
|
|
23208
23154
|
}
|
|
23209
|
-
|
|
23210
|
-
const reg = registryClientDeps(cfg);
|
|
23211
|
-
return {
|
|
23212
|
-
resolveDns: (host) => resolveDnsBounded(host),
|
|
23213
|
-
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
23214
|
-
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
23215
|
-
hasDeployCoords: async (slug, stage) => {
|
|
23216
|
-
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
23217
|
-
return Boolean(status?.stages[stage]);
|
|
23218
|
-
},
|
|
23219
|
-
hasDeployState: async (slug, stage) => {
|
|
23220
|
-
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
23221
|
-
return Boolean(status?.deployState[stage]);
|
|
23222
|
-
},
|
|
23223
|
-
getDeployFact: async (slug, stage) => {
|
|
23224
|
-
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
23225
|
-
const fact = facts?.stages[stage];
|
|
23226
|
-
return fact ? { port: fact.port, domain: fact.domain } : null;
|
|
23227
|
-
},
|
|
23228
|
-
listSecrets: async (targetRepo2) => {
|
|
23229
|
-
const apiUrl = cfg.sagaApiUrl;
|
|
23230
|
-
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
23231
|
-
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
23232
|
-
const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
23233
|
-
method: "GET",
|
|
23234
|
-
headers: await hubHeaders()
|
|
23235
|
-
}, { attempts: 2, timeoutMs: 5e3 });
|
|
23236
|
-
if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
|
|
23237
|
-
const body = await res.json();
|
|
23238
|
-
return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
|
|
23239
|
-
}
|
|
23240
|
-
};
|
|
23241
|
-
}
|
|
23242
|
-
async function updateV2ReadinessIssue(repo, report, healed) {
|
|
23243
|
-
const title = "v2 readiness: central deploy + secrets alignment";
|
|
23244
|
-
const freshBody = renderReadinessIssueBody("", report, { healed });
|
|
23245
|
-
const list = await execFileP2("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
|
|
23246
|
-
const issues = JSON.parse(list.stdout || "[]");
|
|
23247
|
-
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
23248
|
-
if (!existing) {
|
|
23249
|
-
const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
|
|
23250
|
-
try {
|
|
23251
|
-
const created = await execFileP2("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23252
|
-
const url = created.stdout.trim();
|
|
23253
|
-
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
23254
|
-
return { number, url, action: "created" };
|
|
23255
|
-
} finally {
|
|
23256
|
-
await create.cleanup();
|
|
23257
|
-
}
|
|
23258
|
-
}
|
|
23259
|
-
const view = await execFileP2("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
23260
|
-
const current = JSON.parse(view.stdout || "{}");
|
|
23261
|
-
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
23262
|
-
const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
|
|
23263
|
-
try {
|
|
23264
|
-
await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23265
|
-
} finally {
|
|
23266
|
-
await edit.cleanup();
|
|
23267
|
-
}
|
|
23268
|
-
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
23269
|
-
}
|
|
23270
|
-
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");
|
|
23271
23156
|
async function projectTarget(commandName, explicitTarget) {
|
|
23272
23157
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
23273
23158
|
}
|
|
23274
23159
|
project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
|
|
23275
23160
|
const cfg = await loadConfig();
|
|
23276
23161
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
23277
|
-
if (!projects) return failGraceful("project list: Hub API unreachable or this repo is not bootstrapped");
|
|
23162
|
+
if (!projects) return failGraceful("org project list: Hub API unreachable or this repo is not bootstrapped");
|
|
23278
23163
|
if (o.json) {
|
|
23279
23164
|
console.log(JSON.stringify(projects));
|
|
23280
23165
|
return;
|
|
@@ -23287,16 +23172,16 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
23287
23172
|
const cfg = await loadConfig();
|
|
23288
23173
|
let target;
|
|
23289
23174
|
try {
|
|
23290
|
-
target = await projectTarget("project get", repoOrSlug);
|
|
23175
|
+
target = await projectTarget("org project get", repoOrSlug);
|
|
23291
23176
|
} catch (e) {
|
|
23292
23177
|
return fail(e.message);
|
|
23293
23178
|
}
|
|
23294
23179
|
const read = await fetchProjectBySlugChecked(slugOf(target), registryClientDeps(cfg));
|
|
23295
23180
|
if (!read.ok) {
|
|
23296
|
-
return failGraceful(`project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
|
|
23181
|
+
return failGraceful(`org project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
|
|
23297
23182
|
}
|
|
23298
23183
|
if (!read.project) {
|
|
23299
|
-
return failGraceful(`project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
23184
|
+
return failGraceful(`org project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
23300
23185
|
}
|
|
23301
23186
|
console.log(JSON.stringify(read.project));
|
|
23302
23187
|
if (!o.json) {
|
|
@@ -23307,117 +23192,31 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
23307
23192
|
console.error(
|
|
23308
23193
|
`${m.name ?? target} \xB7 class ${m.class ?? "?"} \xB7 deploy ${m.deployModel ?? "?"}
|
|
23309
23194
|
release track: ${track} \u2014 stages: ${stages}${note}
|
|
23310
|
-
deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
23195
|
+
deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Inspect nonsecret DEPLOY facts with \`mmi-cli org project deploy get\`; full coords remain OIDC-gated.`
|
|
23311
23196
|
);
|
|
23312
23197
|
}
|
|
23313
23198
|
});
|
|
23314
|
-
project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
|
|
23315
|
-
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect nonsecret DEPLOY facts with `mmi-cli project deploy get`. Full coords stay OIDC-gated.";
|
|
23316
|
-
if (o.json) {
|
|
23317
|
-
console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
|
|
23318
|
-
process.exitCode = 1;
|
|
23319
|
-
return;
|
|
23320
|
-
}
|
|
23321
|
-
fail(msg);
|
|
23322
|
-
});
|
|
23323
23199
|
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
|
|
23324
23200
|
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
23325
23201
|
const cfg = await loadConfig();
|
|
23326
23202
|
let target;
|
|
23327
23203
|
try {
|
|
23328
|
-
target = await projectTarget("project deploy get", repoOrSlug);
|
|
23329
|
-
} catch (e) {
|
|
23330
|
-
return fail(e.message);
|
|
23331
|
-
}
|
|
23332
|
-
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
23333
|
-
if (!out) return failGraceful(`project deploy get: Hub deploy facts read failed for ${target}`);
|
|
23334
|
-
const stage = o.stage?.trim();
|
|
23335
|
-
if (stage && !["dev", "rc", "main"].includes(stage)) return fail("project deploy get: --stage must be dev, rc, or main");
|
|
23336
|
-
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
23337
|
-
console.log(JSON.stringify(payload));
|
|
23338
|
-
});
|
|
23339
|
-
projectDeploy.command("list [owner/repo]").description("alias for project deploy get; lists all stages by default").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
23340
|
-
const cfg = await loadConfig();
|
|
23341
|
-
let target;
|
|
23342
|
-
try {
|
|
23343
|
-
target = await projectTarget("project deploy list", repoOrSlug);
|
|
23204
|
+
target = await projectTarget("org project deploy get", repoOrSlug);
|
|
23344
23205
|
} catch (e) {
|
|
23345
23206
|
return fail(e.message);
|
|
23346
23207
|
}
|
|
23347
23208
|
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
23348
|
-
if (!out) return failGraceful(`project deploy
|
|
23209
|
+
if (!out) return failGraceful(`org project deploy get: Hub deploy facts read failed for ${target}`);
|
|
23349
23210
|
const stage = o.stage?.trim();
|
|
23350
|
-
if (stage && !["dev", "rc", "main"].includes(stage)) return fail("project deploy
|
|
23211
|
+
if (stage && !["dev", "rc", "main"].includes(stage)) return fail("org project deploy get: --stage must be dev, rc, or main");
|
|
23351
23212
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
23352
23213
|
console.log(JSON.stringify(payload));
|
|
23353
23214
|
});
|
|
23354
|
-
project.command("
|
|
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) => {
|
|
23355
23216
|
const cfg = await loadConfig();
|
|
23356
23217
|
let target;
|
|
23357
23218
|
try {
|
|
23358
|
-
target = await projectTarget("project
|
|
23359
|
-
} catch (e) {
|
|
23360
|
-
return fail(e.message);
|
|
23361
|
-
}
|
|
23362
|
-
const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
|
|
23363
|
-
console.log(JSON.stringify(report));
|
|
23364
|
-
if (!report.ok) process.exitCode = 1;
|
|
23365
|
-
});
|
|
23366
|
-
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) => {
|
|
23367
|
-
const cfg = await loadConfig();
|
|
23368
|
-
let target;
|
|
23369
|
-
try {
|
|
23370
|
-
target = await projectTarget("project heal", repo);
|
|
23371
|
-
} catch (e) {
|
|
23372
|
-
return fail(e.message);
|
|
23373
|
-
}
|
|
23374
|
-
const reg = registryClientDeps(cfg);
|
|
23375
|
-
const out = await runV2Heal(target, { apply: o.apply }, {
|
|
23376
|
-
fetchProject: (s) => fetchProjectBySlugChecked(s, reg),
|
|
23377
|
-
upsertProject: (s, patch) => upsertProject(s, patch, reg)
|
|
23378
|
-
});
|
|
23379
|
-
if (out.status === "aborted") return failGraceful(`project heal: ${out.error}`);
|
|
23380
|
-
if (out.status === "planned") {
|
|
23381
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, dryRun: true, patch: out.patch, appOwnedGaps: out.appOwnedGaps }));
|
|
23382
|
-
return;
|
|
23383
|
-
}
|
|
23384
|
-
if (out.status === "noop") {
|
|
23385
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, applied: [], appOwnedGaps: out.appOwnedGaps }));
|
|
23386
|
-
return;
|
|
23387
|
-
}
|
|
23388
|
-
if (out.status === "write-failed") return reportWrite("project heal", out.write);
|
|
23389
|
-
console.log(JSON.stringify({ ok: true, slug: out.slug, applied: out.applied, appOwnedGaps: out.appOwnedGaps, result: out.result }));
|
|
23390
|
-
});
|
|
23391
|
-
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) => {
|
|
23392
|
-
if (!o.updateIssue) return fail("project readiness: pass --update-issue");
|
|
23393
|
-
const cfg = await loadConfig();
|
|
23394
|
-
let target;
|
|
23395
|
-
try {
|
|
23396
|
-
target = await projectTarget("project readiness", repo);
|
|
23397
|
-
} catch (e) {
|
|
23398
|
-
return fail(e.message);
|
|
23399
|
-
}
|
|
23400
|
-
const report = await buildV2Doctor(target, await v2ReadinessDeps(cfg));
|
|
23401
|
-
const issue2 = await updateV2ReadinessIssue(target, report, []);
|
|
23402
|
-
console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
|
|
23403
|
-
});
|
|
23404
|
-
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) => {
|
|
23405
|
-
const cfg = await loadConfig();
|
|
23406
|
-
let target;
|
|
23407
|
-
try {
|
|
23408
|
-
target = await projectTarget("project attest", repoOrSlug);
|
|
23409
|
-
} catch (e) {
|
|
23410
|
-
return fail(e.message);
|
|
23411
|
-
}
|
|
23412
|
-
const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
|
|
23413
|
-
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
23414
|
-
return reportWrite("project attest", res);
|
|
23415
|
-
});
|
|
23416
|
-
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) => {
|
|
23417
|
-
const cfg = await loadConfig();
|
|
23418
|
-
let target;
|
|
23419
|
-
try {
|
|
23420
|
-
target = await projectTarget("project set", repoOrSlug);
|
|
23219
|
+
target = await projectTarget("org project set", repoOrSlug);
|
|
23421
23220
|
} catch (e) {
|
|
23422
23221
|
return fail(e.message);
|
|
23423
23222
|
}
|
|
@@ -23426,12 +23225,12 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
23426
23225
|
const setAlias = o.set ?? [];
|
|
23427
23226
|
const vars = [...o.var ?? [], ...setAlias];
|
|
23428
23227
|
const dupe = duplicateVarKeyAcrossFlags(o.var ?? [], setAlias);
|
|
23429
|
-
if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
23228
|
+
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
23430
23229
|
if (o.secretsFile) {
|
|
23431
23230
|
try {
|
|
23432
23231
|
vars.push(`secrets=${(0, import_node_fs27.readFileSync)(o.secretsFile, "utf8")}`);
|
|
23433
23232
|
} catch (e) {
|
|
23434
|
-
return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
23233
|
+
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
23435
23234
|
}
|
|
23436
23235
|
}
|
|
23437
23236
|
let patch;
|
|
@@ -23446,19 +23245,19 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
23446
23245
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
23447
23246
|
});
|
|
23448
23247
|
} catch (e) {
|
|
23449
|
-
return fail(e.message.replace(/^project set: /, "project set: "));
|
|
23248
|
+
return fail(e.message.replace(/^org project set: /, "org project set: "));
|
|
23450
23249
|
}
|
|
23451
23250
|
const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
|
|
23452
23251
|
const boardError = boardLinkWriteError(patch, existing);
|
|
23453
|
-
if (boardError) return fail(`project set: ${boardError}`);
|
|
23252
|
+
if (boardError) return fail(`org project set: ${boardError}`);
|
|
23454
23253
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
23455
|
-
return reportWrite("project set", res);
|
|
23254
|
+
return reportWrite("org project set", res);
|
|
23456
23255
|
});
|
|
23457
23256
|
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default \u2014 pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only \u2014 leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
|
|
23458
23257
|
const cfg = await loadConfig();
|
|
23459
23258
|
let target;
|
|
23460
23259
|
try {
|
|
23461
|
-
target = await projectTarget("project retire", repoOrSlug);
|
|
23260
|
+
target = await projectTarget("org project retire", repoOrSlug);
|
|
23462
23261
|
} catch (e) {
|
|
23463
23262
|
return fail(e.message);
|
|
23464
23263
|
}
|
|
@@ -23484,7 +23283,7 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
23484
23283
|
printLine(JSON.stringify(structured));
|
|
23485
23284
|
} else {
|
|
23486
23285
|
const verb = result.applied ? "retired" : "WOULD retire (dry-run; pass --apply to delete)";
|
|
23487
|
-
printLine(`project retire: ${verb} ${result.slug} (${result.repo})`);
|
|
23286
|
+
printLine(`org project retire: ${verb} ${result.slug} (${result.repo})`);
|
|
23488
23287
|
if (result.applied) {
|
|
23489
23288
|
printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED \u2014 ${result.removedMeta.error}`}`);
|
|
23490
23289
|
} else {
|
|
@@ -23499,10 +23298,10 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
23499
23298
|
printLine(` ${result.vaultNote}`);
|
|
23500
23299
|
}
|
|
23501
23300
|
if (result.applied && !result.removedMeta.ok) {
|
|
23502
|
-
return failGraceful(`project retire: META delete failed \u2014 ${result.removedMeta.error}`);
|
|
23301
|
+
return failGraceful(`org project retire: META delete failed \u2014 ${result.removedMeta.error}`);
|
|
23503
23302
|
}
|
|
23504
23303
|
});
|
|
23505
|
-
var fullTrack = program2.command("full-track").description("direct-to-
|
|
23304
|
+
var fullTrack = program2.command("full-track").description("direct-to-train readiness audits");
|
|
23506
23305
|
fullTrack.command("readiness <owner/repo>").description("aggregate branch topology, train authority, deploy facts, endpoint health, and rcand readiness for direct -> full switches").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
23507
23306
|
const cfg = await loadConfig();
|
|
23508
23307
|
const reg = registryClientDeps(cfg);
|
|
@@ -23515,7 +23314,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
23515
23314
|
buildTenantRuntimeStatusFor(repo, "rc", cfg),
|
|
23516
23315
|
buildTenantRuntimeStatusFor(repo, "main", cfg)
|
|
23517
23316
|
]);
|
|
23518
|
-
if (!metaRead.ok) return failGraceful(`
|
|
23317
|
+
if (!metaRead.ok) return failGraceful(`train readiness: Hub registry read failed (${metaRead.error})`);
|
|
23519
23318
|
const report = buildFullTrackReadinessReport({
|
|
23520
23319
|
repo,
|
|
23521
23320
|
slug,
|
|
@@ -23527,31 +23326,37 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
23527
23326
|
console.log(JSON.stringify(report, null, 2));
|
|
23528
23327
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
23529
23328
|
});
|
|
23530
|
-
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) => {
|
|
23531
23330
|
const cfg = await loadConfig();
|
|
23532
23331
|
let target;
|
|
23533
23332
|
try {
|
|
23534
|
-
target = await projectTarget("project set-deploy", repoOrSlug);
|
|
23333
|
+
target = await projectTarget("org project set-deploy", repoOrSlug);
|
|
23535
23334
|
} catch (e) {
|
|
23536
23335
|
return fail(e.message);
|
|
23537
23336
|
}
|
|
23538
23337
|
let noEnvFile;
|
|
23539
23338
|
if (typeof o.envFile === "string") {
|
|
23540
23339
|
const v = o.envFile.trim().toLowerCase();
|
|
23541
|
-
if (v !== "true" && v !== "false") return fail("project set-deploy: --no-env-file must be true or false");
|
|
23340
|
+
if (v !== "true" && v !== "false") return fail("org project set-deploy: --no-env-file must be true or false");
|
|
23542
23341
|
noEnvFile = v === "true";
|
|
23543
23342
|
}
|
|
23544
|
-
if (noEnvFile
|
|
23343
|
+
if (noEnvFile !== void 0) {
|
|
23545
23344
|
const branch = DEPLOY_STAGE_BRANCH[o.stage.trim().toLowerCase()];
|
|
23546
23345
|
if (branch) {
|
|
23547
23346
|
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
23548
23347
|
const sameRepo = !repoOrSlug || Boolean(cwdRepo) && (repoOrSlug.includes("/") ? cwdRepo.toLowerCase() === target.toLowerCase() : slugOf(cwdRepo) === slugOf(target));
|
|
23549
23348
|
if (sameRepo) {
|
|
23550
|
-
const verdict = evaluateFilelessComposeGuard({ composeText: await readStageBranchCompose(branch), branch, force: Boolean(o.force) });
|
|
23551
|
-
if (!verdict.ok) return fail(`project set-deploy: ${verdict.reason}
|
|
23552
|
-
|
|
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)}`);
|
|
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
|
+
);
|
|
23553
23358
|
} else {
|
|
23554
|
-
console.error(`project set-deploy: --
|
|
23359
|
+
console.error(`org project set-deploy: --force: skipping cross-repo fileless-compose verification for ${target} ${branch}.`);
|
|
23555
23360
|
}
|
|
23556
23361
|
}
|
|
23557
23362
|
}
|
|
@@ -23571,17 +23376,18 @@ project.command("set-deploy [owner/repo]").description("patch the DEPLOY#<stage>
|
|
|
23571
23376
|
clearAliases: o.clearAliases,
|
|
23572
23377
|
noEnvFile
|
|
23573
23378
|
});
|
|
23379
|
+
if (target.includes("/")) body.repo = target;
|
|
23574
23380
|
} catch (e) {
|
|
23575
23381
|
return fail(e.message);
|
|
23576
23382
|
}
|
|
23577
23383
|
const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
|
|
23578
|
-
return reportWrite("project set-deploy", res);
|
|
23384
|
+
return reportWrite("org project set-deploy", res);
|
|
23579
23385
|
});
|
|
23580
23386
|
var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
|
|
23581
23387
|
registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
|
|
23582
23388
|
const cfg = await loadConfig();
|
|
23583
23389
|
const org = await fetchOrgConfig(registryClientDeps(cfg));
|
|
23584
|
-
if (!org) return failGraceful("
|
|
23390
|
+
if (!org) return failGraceful("org config get: Hub API unreachable, unseeded, or this repo is not bootstrapped");
|
|
23585
23391
|
console.log(JSON.stringify(org));
|
|
23586
23392
|
});
|
|
23587
23393
|
var oauth = program2.command("oauth").description("per-repo Google OAuth \u2014 plan the canonical URI set, verify the client is port-agnostic");
|
|
@@ -23593,7 +23399,7 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
23593
23399
|
try {
|
|
23594
23400
|
oc = parseOauthConfig(meta ?? {}, slug);
|
|
23595
23401
|
} catch (e) {
|
|
23596
|
-
return failGraceful(`oauth plan: ${e.message}`);
|
|
23402
|
+
return failGraceful(`org oauth plan: ${e.message}`);
|
|
23597
23403
|
}
|
|
23598
23404
|
const origins = expectedJsOrigins(oc);
|
|
23599
23405
|
const redirects = expectedRedirectUris(oc);
|
|
@@ -23611,18 +23417,18 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
23611
23417
|
console.log(`
|
|
23612
23418
|
SSM cred params (under /mmi-future/${slug}/):`);
|
|
23613
23419
|
ssm.forEach((k) => console.log(` ${k}`));
|
|
23614
|
-
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
|
|
23420
|
+
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli org oauth set-creds`.");
|
|
23615
23421
|
});
|
|
23616
23422
|
oauth.command("set-creds").description('store the OAuth client into the canonical stageless GOOGLE_CLIENT_ID/SECRET pair (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
23617
23423
|
const raw = await readStdin();
|
|
23618
23424
|
if (!raw.trim()) {
|
|
23619
|
-
return fail("oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli oauth set-creds --repo <owner/repo> < client.json");
|
|
23425
|
+
return fail("org oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli org oauth set-creds --repo <owner/repo> < client.json");
|
|
23620
23426
|
}
|
|
23621
23427
|
let creds;
|
|
23622
23428
|
try {
|
|
23623
23429
|
creds = parseOauthClientJson(raw);
|
|
23624
23430
|
} catch (e) {
|
|
23625
|
-
return fail(`oauth set-creds: ${e.message}`);
|
|
23431
|
+
return fail(`org oauth set-creds: ${e.message}`);
|
|
23626
23432
|
}
|
|
23627
23433
|
await withSecrets(async (d) => {
|
|
23628
23434
|
for (const key of oauthSsmKeys()) {
|
|
@@ -23632,7 +23438,7 @@ oauth.command("set-creds").description('store the OAuth client into the canonica
|
|
|
23632
23438
|
return;
|
|
23633
23439
|
}
|
|
23634
23440
|
}
|
|
23635
|
-
console.log(`OAuth client stored in the ${oauthSsmKeys().length} canonical stageless keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
|
|
23441
|
+
console.log(`OAuth client stored in the ${oauthSsmKeys().length} canonical stageless keys. Run \`mmi-cli org oauth verify\` to confirm the client is port-agnostic.`);
|
|
23636
23442
|
});
|
|
23637
23443
|
});
|
|
23638
23444
|
oauth.command("verify").description("probe Google authorize with an arbitrary port (:9123) to confirm the client is port-agnostic").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--client-id <id>", "OAuth client_id (else read the stageless GOOGLE_CLIENT_ID from SSM)").option("--json", "machine-readable output").action(async (o) => {
|
|
@@ -23643,7 +23449,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
23643
23449
|
try {
|
|
23644
23450
|
oc = parseOauthConfig(meta ?? {}, slug);
|
|
23645
23451
|
} catch (e) {
|
|
23646
|
-
return failGraceful(`oauth verify: ${e.message}`);
|
|
23452
|
+
return failGraceful(`org oauth verify: ${e.message}`);
|
|
23647
23453
|
}
|
|
23648
23454
|
let clientId = o.clientId;
|
|
23649
23455
|
if (!clientId) {
|
|
@@ -23652,7 +23458,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
23652
23458
|
});
|
|
23653
23459
|
}
|
|
23654
23460
|
if (!clientId) {
|
|
23655
|
-
return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
|
|
23461
|
+
return failGraceful("org oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
|
|
23656
23462
|
}
|
|
23657
23463
|
const redirectUri = probeRedirectUri(oc.callbackPath);
|
|
23658
23464
|
let body = "";
|
|
@@ -23660,7 +23466,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
23660
23466
|
const res = await fetch(buildAuthorizeProbeUrl(clientId, redirectUri), { redirect: "follow", signal: AbortSignal.timeout(1e4) });
|
|
23661
23467
|
body = await res.text();
|
|
23662
23468
|
} catch (e) {
|
|
23663
|
-
return failGraceful(`oauth verify: probe request failed: ${e.message}`);
|
|
23469
|
+
return failGraceful(`org oauth verify: probe request failed: ${e.message}`);
|
|
23664
23470
|
}
|
|
23665
23471
|
const mismatch = authorizeBodyHasMismatch(body);
|
|
23666
23472
|
if (o.json) {
|
|
@@ -24418,6 +24224,12 @@ function trainApplyDeps() {
|
|
|
24418
24224
|
const body = res.body;
|
|
24419
24225
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
24420
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
|
+
},
|
|
24421
24233
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
24422
24234
|
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
24423
24235
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
@@ -24479,7 +24291,7 @@ function renderTrainApply(commandName, r) {
|
|
|
24479
24291
|
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
24480
24292
|
}
|
|
24481
24293
|
function renderTenantRedeploy(r) {
|
|
24482
|
-
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
24294
|
+
return `mmi-cli runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
24483
24295
|
}
|
|
24484
24296
|
async function resolveRcandPlanTargets() {
|
|
24485
24297
|
try {
|
|
@@ -24619,7 +24431,7 @@ var access = program2.command("access").description("org access audit (read-only
|
|
|
24619
24431
|
access.command("role [repo]").description("D14 train authority for a repo (server-side Hub check): master | project-admin | developer").option("--json", "machine-readable output").action(async (repoArg) => {
|
|
24620
24432
|
const o = { json: rawFlag("--json") };
|
|
24621
24433
|
const repo = repoArg ?? await resolveRepo();
|
|
24622
|
-
if (!repo) return fail("access role: pass <owner/repo> or run inside a repo checkout");
|
|
24434
|
+
if (!repo) return fail("org access role: pass <owner/repo> or run inside a repo checkout");
|
|
24623
24435
|
const cfg = await loadConfig();
|
|
24624
24436
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(cfg));
|
|
24625
24437
|
if (!verdict.ok) {
|
|
@@ -24628,7 +24440,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
24628
24440
|
process.exitCode = 1;
|
|
24629
24441
|
return;
|
|
24630
24442
|
}
|
|
24631
|
-
return failGraceful(`access role: ${verdict.error}`);
|
|
24443
|
+
return failGraceful(`org access role: ${verdict.error}`);
|
|
24632
24444
|
}
|
|
24633
24445
|
const a = verdict.authority;
|
|
24634
24446
|
if (o.json) {
|
|
@@ -24646,7 +24458,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
24646
24458
|
const cfg = await loadConfig();
|
|
24647
24459
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
24648
24460
|
if (o.repo) {
|
|
24649
|
-
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
24461
|
+
if (o.class !== "deployable" && o.class !== "content") return failGraceful("org access audit: --class must be deployable or content");
|
|
24650
24462
|
const meta = registryProjects?.find((project2) => (project2.repos ?? []).some((repo) => repo.toLowerCase() === o.repo.toLowerCase())) ?? null;
|
|
24651
24463
|
const repoClass = o.class;
|
|
24652
24464
|
targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
|
|
@@ -24672,7 +24484,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
24672
24484
|
});
|
|
24673
24485
|
access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
24674
24486
|
var isWin2 = process.platform === "win32";
|
|
24675
|
-
program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin
|
|
24487
|
+
program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin-heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
|
|
24676
24488
|
if (opts.guide) {
|
|
24677
24489
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
24678
24490
|
return;
|
|
@@ -24691,7 +24503,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
24691
24503
|
mmiDoctorDeps()
|
|
24692
24504
|
);
|
|
24693
24505
|
});
|
|
24694
|
-
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());
|
|
24695
24507
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
24696
24508
|
function directoryBytes(path2) {
|
|
24697
24509
|
let total = 0;
|
|
@@ -24702,11 +24514,11 @@ function directoryBytes(path2) {
|
|
|
24702
24514
|
return 0;
|
|
24703
24515
|
}
|
|
24704
24516
|
for (const entry of entries) {
|
|
24705
|
-
const
|
|
24706
|
-
if (entry.isDirectory()) total += directoryBytes(
|
|
24517
|
+
const child2 = (0, import_node_path23.join)(path2, entry.name);
|
|
24518
|
+
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
24707
24519
|
else {
|
|
24708
24520
|
try {
|
|
24709
|
-
total += (0, import_node_fs27.statSync)(
|
|
24521
|
+
total += (0, import_node_fs27.statSync)(child2).size;
|
|
24710
24522
|
} catch {
|
|
24711
24523
|
}
|
|
24712
24524
|
}
|
|
@@ -24773,7 +24585,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
24773
24585
|
});
|
|
24774
24586
|
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
24775
24587
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
24776
|
-
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
24588
|
+
console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
24777
24589
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
|
|
24778
24590
|
return;
|
|
24779
24591
|
}
|
|
@@ -24835,13 +24647,9 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
24835
24647
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
|
|
24836
24648
|
});
|
|
24837
24649
|
installProcessBackstop();
|
|
24838
|
-
program2
|
|
24839
|
-
|
|
24840
|
-
|
|
24841
|
-
program2.command("fusion", { hidden: true }).action(() => {
|
|
24842
|
-
fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
|
|
24843
|
-
});
|
|
24844
|
-
program2.parseAsync().then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
24650
|
+
consolidateCommandNamespaces(program2);
|
|
24651
|
+
applyCommandTaxonomy(program2);
|
|
24652
|
+
program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
24845
24653
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24846
24654
|
0 && (module.exports = {
|
|
24847
24655
|
DEFAULT_PRIORITY,
|