@mutmutco/cli 4.3.27 → 4.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +257 -34
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3599,6 +3599,10 @@ var ERROR_CODES = {
|
|
|
3599
3599
|
ERR_BAD_ENUM: "ERR_BAD_ENUM",
|
|
3600
3600
|
/** An unknown flag or subcommand — usually a typo; carries a `did_you_mean`. */
|
|
3601
3601
|
ERR_UNKNOWN_FLAG: "ERR_UNKNOWN_FLAG",
|
|
3602
|
+
/** A positional argument was passed to a command that does not take it (e.g. `oracle board read 496`,
|
|
3603
|
+
* a whole-board read given an issue number). Carries a `corrected_command` when exactly one sibling
|
|
3604
|
+
* answers the same verb positionally (#6354). */
|
|
3605
|
+
ERR_EXCESS_ARGUMENT: "ERR_EXCESS_ARGUMENT",
|
|
3602
3606
|
/** A referenced resource (issue, repo, board item) does not exist. */
|
|
3603
3607
|
ERR_NOT_FOUND: "ERR_NOT_FOUND",
|
|
3604
3608
|
/** Missing / rejected credentials on a path that needs auth. */
|
|
@@ -3642,6 +3646,11 @@ var ERROR_CODE_REFERENCE = [
|
|
|
3642
3646
|
meaning: "A flag or subcommand is not known to this CLI version.",
|
|
3643
3647
|
typical_fix: "Check `did_you_mean`, route with `mmi-cli commands --json`, then inspect exact detail with `mmi-cli explain <command> --json`; update the CLI if it should exist."
|
|
3644
3648
|
},
|
|
3649
|
+
{
|
|
3650
|
+
code: ERROR_CODES.ERR_EXCESS_ARGUMENT,
|
|
3651
|
+
meaning: "A positional argument was supplied to a command that takes none (or fewer).",
|
|
3652
|
+
typical_fix: "Run the command named by `corrected_command`; otherwise drop the extra argument and select the target with the flags in `mmi-cli explain <command> --json`."
|
|
3653
|
+
},
|
|
3645
3654
|
{
|
|
3646
3655
|
code: ERROR_CODES.ERR_NOT_FOUND,
|
|
3647
3656
|
meaning: "The referenced issue, PR, repo, board item, or other resource was not found.",
|
|
@@ -4329,8 +4338,8 @@ function fail(msg, payload) {
|
|
|
4329
4338
|
if (payload && json) {
|
|
4330
4339
|
console.error(formatErrorEnvelope(canonicalMsg, payload));
|
|
4331
4340
|
} else {
|
|
4332
|
-
console.error(`mmi-cli ${canonicalMsg}`);
|
|
4333
4341
|
if (!json) console.error(`run: mmi-cli explain ${commandFromFailMessage(msg)}`);
|
|
4342
|
+
console.error(`mmi-cli ${canonicalMsg}`);
|
|
4334
4343
|
}
|
|
4335
4344
|
hardExit(1);
|
|
4336
4345
|
}
|
|
@@ -4755,6 +4764,26 @@ function synonymFlagFor(flag, ownFlags) {
|
|
|
4755
4764
|
function unknownTargetFlagMessage(flag, positional) {
|
|
4756
4765
|
return `unknown option '${flag}' \u2014 this command takes its target as a positional: ${positional}`;
|
|
4757
4766
|
}
|
|
4767
|
+
function parseExcessArgumentsError(plain) {
|
|
4768
|
+
const m = /too many arguments(?: for '[^']*')?\. Expected \d+ arguments? but got \d+: (.+?)\.\s*$/.exec(plain.trim());
|
|
4769
|
+
if (!m) return void 0;
|
|
4770
|
+
const extra = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
4771
|
+
return extra.length ? { extra } : void 0;
|
|
4772
|
+
}
|
|
4773
|
+
function siblingTakingPositional(leaf, siblings) {
|
|
4774
|
+
const hits = siblings.filter((s) => s.path && s.names.includes(leaf));
|
|
4775
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
4776
|
+
}
|
|
4777
|
+
function correctedPositionalCommand(sibling, value, argv) {
|
|
4778
|
+
const i = argv.indexOf(value);
|
|
4779
|
+
const rest = i < 0 ? argv : [...argv.slice(0, i), ...argv.slice(i + 1)];
|
|
4780
|
+
return [`mmi-cli ${sibling.path}`, value, ...survivingFlagArgs(rest, "", sibling.ownFlags)].join(" ");
|
|
4781
|
+
}
|
|
4782
|
+
function excessArgumentMessage(extra, corrected) {
|
|
4783
|
+
const list = extra.map((t) => `'${t}'`).join(", ");
|
|
4784
|
+
const head = `unexpected positional argument ${list} \u2014 this command does not take it`;
|
|
4785
|
+
return corrected ? `${head}. Did you mean \`${corrected}\`?` : head;
|
|
4786
|
+
}
|
|
4758
4787
|
|
|
4759
4788
|
// src/released-version-cache.ts
|
|
4760
4789
|
var import_node_fs7 = require("node:fs");
|
|
@@ -9029,9 +9058,9 @@ async function secretsCopy(deps, opts) {
|
|
|
9029
9058
|
deps.err("secrets copy: --from and --to must differ");
|
|
9030
9059
|
return false;
|
|
9031
9060
|
}
|
|
9032
|
-
const keys = [...new Set(opts.keys.
|
|
9061
|
+
const keys = [...new Set(opts.keys.flatMap((k) => k.split(/[,\s]+/)).filter(Boolean))];
|
|
9033
9062
|
if (!keys.length) {
|
|
9034
|
-
deps.err("secrets copy: --keys required (comma-separated allowlist)");
|
|
9063
|
+
deps.err("secrets copy: --keys required (comma- or space-separated allowlist)");
|
|
9035
9064
|
return false;
|
|
9036
9065
|
}
|
|
9037
9066
|
for (const key of keys) {
|
|
@@ -11867,7 +11896,10 @@ function laneResumeEvidence(evidence) {
|
|
|
11867
11896
|
return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
|
|
11868
11897
|
}
|
|
11869
11898
|
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin) {
|
|
11870
|
-
|
|
11899
|
+
let evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
|
|
11900
|
+
if (evidence.failed.some((channel) => channel !== "session")) {
|
|
11901
|
+
evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
|
|
11902
|
+
}
|
|
11871
11903
|
const ownership = laneOwnership(evidence.marker, actor);
|
|
11872
11904
|
const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
|
|
11873
11905
|
const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
|
|
@@ -13963,11 +13995,13 @@ function expectedJsOrigins(cfg) {
|
|
|
13963
13995
|
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
13964
13996
|
}
|
|
13965
13997
|
function expectedRedirectUris(cfg) {
|
|
13966
|
-
const
|
|
13967
|
-
return uniq(
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13998
|
+
const paths = [cfg.callbackPath, ...cfg.extraCallbackPaths ?? []];
|
|
13999
|
+
return uniq(
|
|
14000
|
+
paths.flatMap((path2) => [
|
|
14001
|
+
...expectedHosts(cfg).map((h) => `https://${h}${path2}`),
|
|
14002
|
+
...LOOPBACK.map((l) => `${l}${path2}`)
|
|
14003
|
+
])
|
|
14004
|
+
);
|
|
13971
14005
|
}
|
|
13972
14006
|
function oauthSsmKeys() {
|
|
13973
14007
|
return [...SSM_NAMES];
|
|
@@ -13988,6 +14022,18 @@ function parseOauthClientJson(input) {
|
|
|
13988
14022
|
}
|
|
13989
14023
|
return { clientId, clientSecret };
|
|
13990
14024
|
}
|
|
14025
|
+
function parseExtraCallbackPaths(raw) {
|
|
14026
|
+
if (raw === void 0) return void 0;
|
|
14027
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((p) => typeof p !== "string" || !p.trim())) {
|
|
14028
|
+
throw new Error("oauth.extraCallbackPaths must be a non-empty array of callback path strings");
|
|
14029
|
+
}
|
|
14030
|
+
const paths = raw.map((p) => p.trim());
|
|
14031
|
+
const bad = paths.find((p) => !p.startsWith("/"));
|
|
14032
|
+
if (bad !== void 0) {
|
|
14033
|
+
throw new Error(`oauth.extraCallbackPaths entries must start with "/" (got ${JSON.stringify(bad)})`);
|
|
14034
|
+
}
|
|
14035
|
+
return uniq(paths);
|
|
14036
|
+
}
|
|
13991
14037
|
function parseOauthConfig(mmiConfig, slug) {
|
|
13992
14038
|
const rawUnknown = mmiConfig?.oauth;
|
|
13993
14039
|
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
@@ -14002,12 +14048,15 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
14002
14048
|
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
14003
14049
|
}
|
|
14004
14050
|
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
14005
|
-
throw new Error(
|
|
14051
|
+
throw new Error(
|
|
14052
|
+
`oauth.callbackPath is the app-login callback and must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)}) \u2014 a second callback on the same client (e.g. a Workspace connect endpoint) goes in oauth.extraCallbackPaths`
|
|
14053
|
+
);
|
|
14006
14054
|
}
|
|
14055
|
+
const extraCallbackPaths = parseExtraCallbackPaths(raw.extraCallbackPaths);
|
|
14007
14056
|
const meta = mmiConfig ?? {};
|
|
14008
14057
|
const rawFofuSub = raw.fofuSubdomain;
|
|
14009
14058
|
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
|
|
14010
|
-
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
14059
|
+
return { subdomains, domains, callbackPath, extraCallbackPaths, fofuSubdomain };
|
|
14011
14060
|
}
|
|
14012
14061
|
function probeRedirectUri(callbackPath, port = 9123) {
|
|
14013
14062
|
return `http://localhost:${port}${callbackPath}`;
|
|
@@ -15535,10 +15584,10 @@ var rollout_plan_default = {
|
|
|
15535
15584
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15536
15585
|
},
|
|
15537
15586
|
baseline: {
|
|
15538
|
-
version: "4.3.
|
|
15539
|
-
tag: "v4.3.
|
|
15540
|
-
commit: "
|
|
15541
|
-
npm: "@mutmutco/cli@4.3.
|
|
15587
|
+
version: "4.3.29",
|
|
15588
|
+
tag: "v4.3.29",
|
|
15589
|
+
commit: "5a5b64c2d604",
|
|
15590
|
+
npm: "@mutmutco/cli@4.3.29"
|
|
15542
15591
|
},
|
|
15543
15592
|
exitCriterion: "fleet-n-of-n",
|
|
15544
15593
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15555,14 +15604,14 @@ var rollout_plan_default = {
|
|
|
15555
15604
|
repo: "mutmutco/mmi-hub",
|
|
15556
15605
|
role: "canary",
|
|
15557
15606
|
schedule: "train",
|
|
15558
|
-
v3Target: "v4.3.
|
|
15607
|
+
v3Target: "v4.3.29"
|
|
15559
15608
|
}
|
|
15560
15609
|
],
|
|
15561
15610
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
15562
15611
|
rollback: {
|
|
15563
15612
|
independent: true,
|
|
15564
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15565
|
-
v3Target: "v4.3.
|
|
15613
|
+
mechanism: "npm dist-tag latest -> 4.3.29 and redeploy the Hub Lambda from tag v4.3.29 (5a5b64c2d604); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15614
|
+
v3Target: "v4.3.29 (@mutmutco/cli@4.3.29, tag commit 5a5b64c2d604 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15566
15615
|
}
|
|
15567
15616
|
},
|
|
15568
15617
|
{
|
|
@@ -23598,6 +23647,12 @@ function refPatternCovers(pattern, ref) {
|
|
|
23598
23647
|
function uncoveredTrackBranches(liveIncludes, trackBranches) {
|
|
23599
23648
|
return trackBranches.filter((branch) => !liveIncludes.some((pattern) => refPatternCovers(pattern, `refs/heads/${branch}`)));
|
|
23600
23649
|
}
|
|
23650
|
+
function unreachableTrackBranches(uncovered, requiredContexts, emittedByBranch) {
|
|
23651
|
+
return uncovered.filter((branch) => {
|
|
23652
|
+
const emitted = emittedByBranch[branch];
|
|
23653
|
+
return emitted !== void 0 && !requiredContexts.every((context) => emitted.includes(context));
|
|
23654
|
+
});
|
|
23655
|
+
}
|
|
23601
23656
|
function rulesetCoversReleaseBranches(payload, track) {
|
|
23602
23657
|
const conditions = payload.conditions;
|
|
23603
23658
|
const include = conditions?.ref_name?.include;
|
|
@@ -23899,14 +23954,19 @@ async function auditRepoCi(repo, deps) {
|
|
|
23899
23954
|
}
|
|
23900
23955
|
}
|
|
23901
23956
|
if (productRuleset != null) {
|
|
23902
|
-
const
|
|
23957
|
+
const presentTrackBranches = [];
|
|
23903
23958
|
for (const branch of uncoveredTrackBranches(liveBranchIncludes, branchesForTrack(resolveReleaseTrack(meta, void 0, repo)))) {
|
|
23904
|
-
if (await branchPresence(deps, repo, branch) === true)
|
|
23959
|
+
if (await branchPresence(deps, repo, branch) === true) presentTrackBranches.push(branch);
|
|
23905
23960
|
}
|
|
23961
|
+
const emittedByTrackBranch = {};
|
|
23962
|
+
for (const branch of presentTrackBranches) emittedByTrackBranch[branch] = await resolveEmittedPrContexts(deps, repo, branch);
|
|
23963
|
+
const toleratedTrackBranches = unreachableTrackBranches(presentTrackBranches, liveContexts, emittedByTrackBranch);
|
|
23964
|
+
const missingTrackBranches = presentTrackBranches.filter((branch) => !toleratedTrackBranches.includes(branch));
|
|
23965
|
+
const toleratedNote = toleratedTrackBranches.length ? ` (tolerated until first release: [${toleratedTrackBranches.join(", ")}] \u2014 their trees cannot emit every one of [${liveContexts.join(", ")}], #6385)` : "";
|
|
23906
23966
|
checks.push({
|
|
23907
23967
|
ok: missingTrackBranches.length === 0,
|
|
23908
23968
|
label: RULESET_TRACK_SCOPE_LABEL,
|
|
23909
|
-
detail: missingTrackBranches.length ? `live ${PRODUCT_RULESET_NAME} includes [${liveBranchIncludes.join(", ")}] but the release track also requires [${missingTrackBranches.join(", ")}] \u2014 those branches tag and merge with no required check` : void 0,
|
|
23969
|
+
detail: missingTrackBranches.length ? `live ${PRODUCT_RULESET_NAME} includes [${liveBranchIncludes.join(", ")}] but the release track also requires [${missingTrackBranches.join(", ")}] \u2014 those branches tag and merge with no required check${toleratedNote}` : toleratedNote ? toleratedNote.trim().replace(/^\(|\)$/g, "") : void 0,
|
|
23910
23970
|
remediation: missingTrackBranches.length ? `mmi-cli oracle org project set ${repo} --var requiredCheckBranches=${JSON.stringify(branchesForTrack(resolveReleaseTrack(meta, void 0, repo)))} then mmi-cli devops bootstrap apply ${repo} --execute` : void 0
|
|
23911
23971
|
});
|
|
23912
23972
|
}
|
|
@@ -28959,7 +29019,7 @@ function parseOauthVar(raw) {
|
|
|
28959
29019
|
throw new Error(`org project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
28960
29020
|
}
|
|
28961
29021
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28962
|
-
throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
29022
|
+
throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,extraCallbackPaths,fofuSubdomain} object");
|
|
28963
29023
|
}
|
|
28964
29024
|
const map = parsed;
|
|
28965
29025
|
const out = {};
|
|
@@ -28973,14 +29033,22 @@ function parseOauthVar(raw) {
|
|
|
28973
29033
|
if (typeof value !== "string" || !value.trim()) throw new Error("org project set: oauth.callbackPath must be a non-empty string");
|
|
28974
29034
|
const callbackPath = value.trim();
|
|
28975
29035
|
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
28976
|
-
throw new Error(
|
|
29036
|
+
throw new Error(
|
|
29037
|
+
`org project set: oauth.callbackPath is the app-login callback and must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)}) \u2014 a second callback on the same client (e.g. a Workspace connect endpoint) goes in oauth.extraCallbackPaths`
|
|
29038
|
+
);
|
|
28977
29039
|
}
|
|
28978
29040
|
out.callbackPath = callbackPath;
|
|
29041
|
+
} else if (key === "extraCallbackPaths") {
|
|
29042
|
+
try {
|
|
29043
|
+
out.extraCallbackPaths = parseExtraCallbackPaths(value);
|
|
29044
|
+
} catch (e) {
|
|
29045
|
+
throw new Error(`org project set: ${e.message}`);
|
|
29046
|
+
}
|
|
28979
29047
|
} else if (key === "fofuSubdomain") {
|
|
28980
29048
|
if (typeof value !== "string") throw new Error('org project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
28981
29049
|
out.fofuSubdomain = value.trim();
|
|
28982
29050
|
} else {
|
|
28983
|
-
throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
|
|
29051
|
+
throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/extraCallbackPaths/fofuSubdomain`);
|
|
28984
29052
|
}
|
|
28985
29053
|
}
|
|
28986
29054
|
return out;
|
|
@@ -29180,7 +29248,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
29180
29248
|
runtimeVaultOnly: "true|false",
|
|
29181
29249
|
seedCanary: "true|false",
|
|
29182
29250
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
29183
|
-
oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
|
|
29251
|
+
oauth: "JSON {subdomains,domains,callbackPath,extraCallbackPaths,fofuSubdomain}",
|
|
29184
29252
|
requiredGcpApis: "comma-string",
|
|
29185
29253
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
29186
29254
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
@@ -29652,6 +29720,51 @@ function findEnvFiles(root) {
|
|
|
29652
29720
|
return found.sort();
|
|
29653
29721
|
}
|
|
29654
29722
|
|
|
29723
|
+
// src/stage-runtime-env.ts
|
|
29724
|
+
var SERVICE_NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
29725
|
+
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29726
|
+
var RUNTIME_ENV_OVERRIDE_RELPATH = "tmp/stage/runtime-env.override.yml";
|
|
29727
|
+
function labelsMarkRuntimeEnv(labels) {
|
|
29728
|
+
if (Array.isArray(labels)) {
|
|
29729
|
+
return labels.some((l) => typeof l === "string" && /^mmi\.runtime-env\s*=\s*["']?true["']?$/.test(l.trim()));
|
|
29730
|
+
}
|
|
29731
|
+
if (labels && typeof labels === "object") {
|
|
29732
|
+
return String(labels["mmi.runtime-env"] ?? "") === "true";
|
|
29733
|
+
}
|
|
29734
|
+
return false;
|
|
29735
|
+
}
|
|
29736
|
+
function runtimeEnvMarkedServices(composeConfigJson) {
|
|
29737
|
+
let parsed;
|
|
29738
|
+
try {
|
|
29739
|
+
parsed = JSON.parse(composeConfigJson);
|
|
29740
|
+
} catch {
|
|
29741
|
+
return [];
|
|
29742
|
+
}
|
|
29743
|
+
const services = parsed?.services;
|
|
29744
|
+
if (!services || typeof services !== "object" || Array.isArray(services)) return [];
|
|
29745
|
+
return Object.entries(services).filter(([name, svc]) => SERVICE_NAME_RE.test(name) && labelsMarkRuntimeEnv(svc?.labels)).map(([name]) => name).sort();
|
|
29746
|
+
}
|
|
29747
|
+
function renderRuntimeEnvOverride(services, keys) {
|
|
29748
|
+
const safeServices = services.filter((s) => SERVICE_NAME_RE.test(s)).sort();
|
|
29749
|
+
const safeKeys = [...new Set(keys.filter((k) => ENV_KEY_RE.test(k)))].sort();
|
|
29750
|
+
if (!safeServices.length || !safeKeys.length) return null;
|
|
29751
|
+
const lines2 = [
|
|
29752
|
+
"# Generated by `mmi-cli stage` (#6345) \u2014 do not edit, do not commit.",
|
|
29753
|
+
'# Passes the vault-resolved runtime env through to every service labelled mmi.runtime-env: "true",',
|
|
29754
|
+
"# exactly as the box deploy does. KEY NAMES ONLY: the values live in the compose process env (#2655).",
|
|
29755
|
+
"services:"
|
|
29756
|
+
];
|
|
29757
|
+
for (const service of safeServices) {
|
|
29758
|
+
lines2.push(` ${service}:`, " environment:");
|
|
29759
|
+
for (const key of safeKeys) lines2.push(` ${key}: \${${key}:?}`);
|
|
29760
|
+
}
|
|
29761
|
+
return `${lines2.join("\n")}
|
|
29762
|
+
`;
|
|
29763
|
+
}
|
|
29764
|
+
function stageComposeFileEnv(files) {
|
|
29765
|
+
return { COMPOSE_PATH_SEPARATOR: ",", COMPOSE_FILE: files.join(",") };
|
|
29766
|
+
}
|
|
29767
|
+
|
|
29655
29768
|
// src/stage-runner.ts
|
|
29656
29769
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process11.execFile);
|
|
29657
29770
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
@@ -30017,6 +30130,36 @@ function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
|
|
|
30017
30130
|
...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
|
|
30018
30131
|
};
|
|
30019
30132
|
}
|
|
30133
|
+
function stageComposeFiles(cwd) {
|
|
30134
|
+
const files = ["docker-compose.yml"];
|
|
30135
|
+
for (const conventional of ["docker-compose.override.yml", "docker-compose.override.yaml"]) {
|
|
30136
|
+
if ((0, import_node_fs28.existsSync)((0, import_node_path27.join)(cwd, conventional))) files.push(conventional);
|
|
30137
|
+
}
|
|
30138
|
+
return [...files, RUNTIME_ENV_OVERRIDE_RELPATH];
|
|
30139
|
+
}
|
|
30140
|
+
async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
|
|
30141
|
+
const keys = Object.keys(vaultEnvMerge ?? {});
|
|
30142
|
+
if (!keys.length || !(0, import_node_fs28.existsSync)((0, import_node_path27.join)(cwd, "docker-compose.yml"))) return {};
|
|
30143
|
+
let configJson;
|
|
30144
|
+
try {
|
|
30145
|
+
const { stdout } = await execFileP3("docker", ["compose", "config", "--format", "json"], {
|
|
30146
|
+
cwd,
|
|
30147
|
+
windowsHide: true,
|
|
30148
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
30149
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
30150
|
+
env: { ...process.env, ...composeEnv }
|
|
30151
|
+
});
|
|
30152
|
+
configJson = stdout;
|
|
30153
|
+
} catch {
|
|
30154
|
+
return {};
|
|
30155
|
+
}
|
|
30156
|
+
const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys);
|
|
30157
|
+
if (!body) return {};
|
|
30158
|
+
const overridePath = (0, import_node_path27.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
|
|
30159
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path27.join)(cwd, "tmp", "stage"), { recursive: true });
|
|
30160
|
+
(0, import_node_fs28.writeFileSync)(overridePath, body, "utf8");
|
|
30161
|
+
return stageComposeFileEnv(stageComposeFiles(cwd));
|
|
30162
|
+
}
|
|
30020
30163
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
30021
30164
|
if (!config.ensureEnv) return;
|
|
30022
30165
|
const target = (0, import_node_path27.join)(cwd, config.ensureEnv.target);
|
|
@@ -30196,6 +30339,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30196
30339
|
if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30197
30340
|
if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
|
|
30198
30341
|
const composeEnv = stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd);
|
|
30342
|
+
const upEnv = { ...composeEnv, ...await prepareRuntimeEnvPassthrough(cwd, composeEnv, opts.vaultEnvMerge) };
|
|
30199
30343
|
let up = sub(config.up.trim());
|
|
30200
30344
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
30201
30345
|
const identity = await resolveStageIdentity(cwd);
|
|
@@ -30208,7 +30352,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30208
30352
|
detached: process.platform !== "win32",
|
|
30209
30353
|
windowsHide: true,
|
|
30210
30354
|
stdio: "ignore",
|
|
30211
|
-
env: { ...process.env, ...
|
|
30355
|
+
env: { ...process.env, ...upEnv }
|
|
30212
30356
|
});
|
|
30213
30357
|
const state = {
|
|
30214
30358
|
pid: child2.pid ?? 0,
|
|
@@ -30222,6 +30366,9 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30222
30366
|
// #6343: record the non-secret half of the interpolation env so a LATER `stage stop` can run
|
|
30223
30367
|
// `docker compose down` against the same file. `vaultEnvMerge` is omitted deliberately — no secret
|
|
30224
30368
|
// value ever reaches disk (#2655); the stopping invocation re-fetches them.
|
|
30369
|
+
// #6345: the passthrough `COMPOSE_FILE` is omitted too. The override only ADDS container env to services
|
|
30370
|
+
// the project file already declares, so `down` removes the same containers without it — and a later
|
|
30371
|
+
// `stage stop` that resolved no secrets would otherwise die on the override's `${KEY:?}`.
|
|
30225
30372
|
teardown: config.teardown?.trim() ? { command: sub(config.teardown.trim()), cwd, env: stageComposeEnv(config, stagePort, void 0, cwd) } : void 0
|
|
30226
30373
|
};
|
|
30227
30374
|
writeState(statePath, state);
|
|
@@ -30230,7 +30377,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30230
30377
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
30231
30378
|
else await waitForProcessStability(child2);
|
|
30232
30379
|
} catch (e) {
|
|
30233
|
-
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd,
|
|
30380
|
+
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, upEnv);
|
|
30234
30381
|
throw e;
|
|
30235
30382
|
}
|
|
30236
30383
|
const result = {
|
|
@@ -36001,7 +36148,17 @@ function registerStageCommands(program3) {
|
|
|
36001
36148
|
return printLine(o.json ? JSON.stringify({ command: "stage stop", steps }, null, 2) : renderSteps("mmi-cli stage stop: dry-run plan", steps));
|
|
36002
36149
|
}
|
|
36003
36150
|
try {
|
|
36004
|
-
|
|
36151
|
+
let vaultEnvMerge;
|
|
36152
|
+
try {
|
|
36153
|
+
const { project: project2, cfg: stageCfg } = await resolveStage();
|
|
36154
|
+
vaultEnvMerge = await fetchStageVaultEnvMerge(project2, stageCfg);
|
|
36155
|
+
} catch {
|
|
36156
|
+
}
|
|
36157
|
+
const result = await stopStage({
|
|
36158
|
+
cwd: process.cwd(),
|
|
36159
|
+
requiredIdentityCwd: process.cwd(),
|
|
36160
|
+
...vaultEnvMerge ? { vaultEnvMerge } : {}
|
|
36161
|
+
});
|
|
36005
36162
|
printLine(o.json ? JSON.stringify(result) : `mmi-cli stage stop: ${result.message}`);
|
|
36006
36163
|
} catch (e) {
|
|
36007
36164
|
fail(`stage stop: ${e.message}`);
|
|
@@ -44293,11 +44450,55 @@ function resolveParseHint() {
|
|
|
44293
44450
|
`Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2} --json\` for exact detail, or \`mmi-cli commands\` for the task map.`
|
|
44294
44451
|
].join("\n");
|
|
44295
44452
|
}
|
|
44453
|
+
function positionalCommandEntries(house) {
|
|
44454
|
+
const acc = [];
|
|
44455
|
+
const walk2 = (node) => {
|
|
44456
|
+
if (commandMetadata(node)?.category !== "internal") {
|
|
44457
|
+
const args = node.registeredArguments ?? [];
|
|
44458
|
+
const path2 = commandPath2(node);
|
|
44459
|
+
const canonical = path2 ? canonicalPathFor(path2) ?? path2 : "";
|
|
44460
|
+
if (args[0] && canonical && (house === void 0 || houseForPath(canonical) === house)) {
|
|
44461
|
+
acc.push({
|
|
44462
|
+
path: canonical,
|
|
44463
|
+
names: [node.name(), ...node.aliases()],
|
|
44464
|
+
argName: args[0].name(),
|
|
44465
|
+
ownFlags: commandOwnLongFlags(node)
|
|
44466
|
+
});
|
|
44467
|
+
}
|
|
44468
|
+
}
|
|
44469
|
+
for (const child2 of node.commands) walk2(child2);
|
|
44470
|
+
};
|
|
44471
|
+
for (const child2 of program2.commands) walk2(child2);
|
|
44472
|
+
return acc;
|
|
44473
|
+
}
|
|
44474
|
+
function excessArgumentCorrection(extra, argv) {
|
|
44475
|
+
const invoked = resolveCommandFromArgv(program2, [...argv]);
|
|
44476
|
+
if (!invoked || extra.length !== 1) return void 0;
|
|
44477
|
+
const canonical = canonicalPathFor(commandPath2(invoked)) ?? commandPath2(invoked);
|
|
44478
|
+
const entries = positionalCommandEntries(houseForPath(canonical)).filter((e) => e.path !== canonical);
|
|
44479
|
+
const sibling = siblingTakingPositional(invoked.name(), entries);
|
|
44480
|
+
return sibling ? correctedPositionalCommand(sibling, extra[0], argv) : void 0;
|
|
44481
|
+
}
|
|
44482
|
+
var pendingHumanParseError;
|
|
44483
|
+
function writeHumanParseError(str) {
|
|
44484
|
+
pendingHumanParseError = str;
|
|
44485
|
+
}
|
|
44296
44486
|
function envelopeAwareWriteErr(str) {
|
|
44297
44487
|
const plain = str.replace(/\[[0-9;]*m/g, "");
|
|
44298
44488
|
if (plain.includes(PARSE_HINT_SENTINEL)) {
|
|
44299
|
-
if (unknownFlagJsonHandled && argvWantsJson3())
|
|
44300
|
-
|
|
44489
|
+
if (unknownFlagJsonHandled && argvWantsJson3()) {
|
|
44490
|
+
pendingHumanParseError = void 0;
|
|
44491
|
+
return;
|
|
44492
|
+
}
|
|
44493
|
+
const hint = str.replace(PARSE_HINT_SENTINEL, resolveParseHint());
|
|
44494
|
+
if (lastParseErrorKind === "bad-arguments") {
|
|
44495
|
+
process.stderr.write(hint);
|
|
44496
|
+
if (pendingHumanParseError) process.stderr.write(pendingHumanParseError);
|
|
44497
|
+
} else {
|
|
44498
|
+
if (pendingHumanParseError) process.stderr.write(pendingHumanParseError);
|
|
44499
|
+
process.stderr.write(hint);
|
|
44500
|
+
}
|
|
44501
|
+
pendingHumanParseError = void 0;
|
|
44301
44502
|
return;
|
|
44302
44503
|
}
|
|
44303
44504
|
lastParseErrorKind = classifyParseError(plain);
|
|
@@ -44305,7 +44506,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
44305
44506
|
const badChoice = parseInvalidChoiceError(plain);
|
|
44306
44507
|
if (badChoice) {
|
|
44307
44508
|
if (!argvWantsJson3()) {
|
|
44308
|
-
|
|
44509
|
+
writeHumanParseError(str);
|
|
44309
44510
|
return;
|
|
44310
44511
|
}
|
|
44311
44512
|
process.stderr.write(
|
|
@@ -44318,6 +44519,24 @@ function envelopeAwareWriteErr(str) {
|
|
|
44318
44519
|
unknownFlagJsonHandled = true;
|
|
44319
44520
|
return;
|
|
44320
44521
|
}
|
|
44522
|
+
const excess = parseExcessArgumentsError(plain);
|
|
44523
|
+
if (excess) {
|
|
44524
|
+
const argv = process.argv.slice(2);
|
|
44525
|
+
const corrected = excessArgumentCorrection(excess.extra, argv);
|
|
44526
|
+
const message2 = excessArgumentMessage(excess.extra, corrected);
|
|
44527
|
+
if (argvWantsJson3()) {
|
|
44528
|
+
process.stderr.write(
|
|
44529
|
+
formatErrorEnvelope(message2, {
|
|
44530
|
+
code: ERROR_CODES.ERR_EXCESS_ARGUMENT,
|
|
44531
|
+
...corrected ? { corrected_command: corrected } : {}
|
|
44532
|
+
}) + "\n"
|
|
44533
|
+
);
|
|
44534
|
+
unknownFlagJsonHandled = true;
|
|
44535
|
+
return;
|
|
44536
|
+
}
|
|
44537
|
+
writeHumanParseError(str.replace(/error: too many arguments[^\n]*/, `error: ${message2}`));
|
|
44538
|
+
return;
|
|
44539
|
+
}
|
|
44321
44540
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
44322
44541
|
if (match) {
|
|
44323
44542
|
const flag = match[1];
|
|
@@ -44341,14 +44560,18 @@ function envelopeAwareWriteErr(str) {
|
|
|
44341
44560
|
return;
|
|
44342
44561
|
}
|
|
44343
44562
|
if (positional) {
|
|
44344
|
-
|
|
44563
|
+
writeHumanParseError(str.replace(/unknown option '[^']+'/, unknownTargetFlagMessage(flag, positional)));
|
|
44345
44564
|
return;
|
|
44346
44565
|
}
|
|
44347
|
-
|
|
44566
|
+
writeHumanParseError(str);
|
|
44348
44567
|
return;
|
|
44349
44568
|
}
|
|
44350
44569
|
if (unknownFlagJsonHandled && argvWantsJson3()) return;
|
|
44351
|
-
|
|
44570
|
+
if (argvWantsJson3()) {
|
|
44571
|
+
process.stderr.write(str);
|
|
44572
|
+
return;
|
|
44573
|
+
}
|
|
44574
|
+
writeHumanParseError(str);
|
|
44352
44575
|
}
|
|
44353
44576
|
var INVOKED_ARGV3 = process.argv.slice(2);
|
|
44354
44577
|
var program2 = new Command();
|
package/package.json
CHANGED