@mutmutco/cli 4.3.26 → 4.3.28
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 +329 -50
- 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) {
|
|
@@ -10939,9 +10968,15 @@ function boardNotFoundError(ref, board, opts = {}) {
|
|
|
10939
10968
|
function evaluateClaim(item, login) {
|
|
10940
10969
|
const others = item.assignees.filter((a) => a.toLowerCase() !== login.toLowerCase());
|
|
10941
10970
|
const mine = item.assignees.some((a) => a.toLowerCase() === login.toLowerCase());
|
|
10971
|
+
if (others.length && item.status === "In Review") {
|
|
10972
|
+
return {
|
|
10973
|
+
ok: false,
|
|
10974
|
+
reason: `${item.ref} is not claimable: In Review and held by @${others.join(", @")} \u2014 ask the holder, or wait for the review to land`
|
|
10975
|
+
};
|
|
10976
|
+
}
|
|
10942
10977
|
if (others.length) return { ok: false, reason: `${item.ref} is already assigned to @${others.join(", @")}` };
|
|
10943
10978
|
if (item.status === "In Progress" && mine) return { ok: true, alreadyClaimed: true };
|
|
10944
|
-
if (item.status !== "Todo" && item.status !== "In Progress") {
|
|
10979
|
+
if (item.status !== "Todo" && item.status !== "In Progress" && item.status !== "In Review") {
|
|
10945
10980
|
return { ok: false, reason: `${item.ref} is not claimable: Status is ${item.status}` };
|
|
10946
10981
|
}
|
|
10947
10982
|
return { ok: true, alreadyClaimed: false };
|
|
@@ -11064,7 +11099,7 @@ async function repoCanPush(repo, client) {
|
|
|
11064
11099
|
}
|
|
11065
11100
|
}
|
|
11066
11101
|
async function resolveWritableReposForClaimables(items, client) {
|
|
11067
|
-
const candidateRepos = [...new Set(items.filter((item) => (item.status === "Todo" || item.status === "In Progress") && item.assignees.length === 0).map((item) => item.repository))];
|
|
11102
|
+
const candidateRepos = [...new Set(items.filter((item) => (item.status === "Todo" || item.status === "In Progress" || item.status === "In Review") && item.assignees.length === 0).map((item) => item.repository))];
|
|
11068
11103
|
const repos = /* @__PURE__ */ new Set();
|
|
11069
11104
|
const unknown = /* @__PURE__ */ new Set();
|
|
11070
11105
|
const warnings = [];
|
|
@@ -12177,7 +12212,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
12177
12212
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
12178
12213
|
const { cfg, client, report } = ctx;
|
|
12179
12214
|
const flatItem = findBoardItem(ctx.items, selector, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
12180
|
-
const wouldWrite = flatItem.assignees.length === 0 && (flatItem.status === "Todo" || flatItem.status === "In Progress");
|
|
12215
|
+
const wouldWrite = flatItem.assignees.length === 0 && (flatItem.status === "Todo" || flatItem.status === "In Progress" || flatItem.status === "In Review");
|
|
12181
12216
|
if (wouldWrite && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
12182
12217
|
throw new Error(
|
|
12183
12218
|
`${flatItem.ref} is not claimable: the token from ${describeTokenSource()} reports no write access to ${flatItem.repository}`
|
|
@@ -12216,7 +12251,10 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12216
12251
|
};
|
|
12217
12252
|
let previousHolder;
|
|
12218
12253
|
let resumeEvidence;
|
|
12219
|
-
const claimedReceipt = () =>
|
|
12254
|
+
const claimedReceipt = () => ({
|
|
12255
|
+
...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
|
|
12256
|
+
...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder }
|
|
12257
|
+
});
|
|
12220
12258
|
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
|
|
12221
12259
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
12222
12260
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
@@ -12274,7 +12312,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12274
12312
|
} catch (e) {
|
|
12275
12313
|
const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
|
|
12276
12314
|
if (!options.allowPartial) throw new Error(warning);
|
|
12277
|
-
return { item, viewer: report.viewer, repo: report.repo, status:
|
|
12315
|
+
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: true, warning, ...claimedReceipt() };
|
|
12278
12316
|
}
|
|
12279
12317
|
return {
|
|
12280
12318
|
item: {
|
|
@@ -12320,7 +12358,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12320
12358
|
const ref = `${selector.repo}#${selector.number}`;
|
|
12321
12359
|
try {
|
|
12322
12360
|
const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
|
|
12323
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12361
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12324
12362
|
} catch (e) {
|
|
12325
12363
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
12326
12364
|
}
|
|
@@ -13954,11 +13992,13 @@ function expectedJsOrigins(cfg) {
|
|
|
13954
13992
|
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
13955
13993
|
}
|
|
13956
13994
|
function expectedRedirectUris(cfg) {
|
|
13957
|
-
const
|
|
13958
|
-
return uniq(
|
|
13959
|
-
|
|
13960
|
-
|
|
13961
|
-
|
|
13995
|
+
const paths = [cfg.callbackPath, ...cfg.extraCallbackPaths ?? []];
|
|
13996
|
+
return uniq(
|
|
13997
|
+
paths.flatMap((path2) => [
|
|
13998
|
+
...expectedHosts(cfg).map((h) => `https://${h}${path2}`),
|
|
13999
|
+
...LOOPBACK.map((l) => `${l}${path2}`)
|
|
14000
|
+
])
|
|
14001
|
+
);
|
|
13962
14002
|
}
|
|
13963
14003
|
function oauthSsmKeys() {
|
|
13964
14004
|
return [...SSM_NAMES];
|
|
@@ -13979,6 +14019,18 @@ function parseOauthClientJson(input) {
|
|
|
13979
14019
|
}
|
|
13980
14020
|
return { clientId, clientSecret };
|
|
13981
14021
|
}
|
|
14022
|
+
function parseExtraCallbackPaths(raw) {
|
|
14023
|
+
if (raw === void 0) return void 0;
|
|
14024
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((p) => typeof p !== "string" || !p.trim())) {
|
|
14025
|
+
throw new Error("oauth.extraCallbackPaths must be a non-empty array of callback path strings");
|
|
14026
|
+
}
|
|
14027
|
+
const paths = raw.map((p) => p.trim());
|
|
14028
|
+
const bad = paths.find((p) => !p.startsWith("/"));
|
|
14029
|
+
if (bad !== void 0) {
|
|
14030
|
+
throw new Error(`oauth.extraCallbackPaths entries must start with "/" (got ${JSON.stringify(bad)})`);
|
|
14031
|
+
}
|
|
14032
|
+
return uniq(paths);
|
|
14033
|
+
}
|
|
13982
14034
|
function parseOauthConfig(mmiConfig, slug) {
|
|
13983
14035
|
const rawUnknown = mmiConfig?.oauth;
|
|
13984
14036
|
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
@@ -13993,12 +14045,15 @@ function parseOauthConfig(mmiConfig, slug) {
|
|
|
13993
14045
|
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
13994
14046
|
}
|
|
13995
14047
|
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
13996
|
-
throw new Error(
|
|
14048
|
+
throw new Error(
|
|
14049
|
+
`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`
|
|
14050
|
+
);
|
|
13997
14051
|
}
|
|
14052
|
+
const extraCallbackPaths = parseExtraCallbackPaths(raw.extraCallbackPaths);
|
|
13998
14053
|
const meta = mmiConfig ?? {};
|
|
13999
14054
|
const rawFofuSub = raw.fofuSubdomain;
|
|
14000
14055
|
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
|
|
14001
|
-
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
14056
|
+
return { subdomains, domains, callbackPath, extraCallbackPaths, fofuSubdomain };
|
|
14002
14057
|
}
|
|
14003
14058
|
function probeRedirectUri(callbackPath, port = 9123) {
|
|
14004
14059
|
return `http://localhost:${port}${callbackPath}`;
|
|
@@ -15526,10 +15581,10 @@ var rollout_plan_default = {
|
|
|
15526
15581
|
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)."
|
|
15527
15582
|
},
|
|
15528
15583
|
baseline: {
|
|
15529
|
-
version: "4.3.
|
|
15530
|
-
tag: "v4.3.
|
|
15531
|
-
commit: "
|
|
15532
|
-
npm: "@mutmutco/cli@4.3.
|
|
15584
|
+
version: "4.3.28",
|
|
15585
|
+
tag: "v4.3.28",
|
|
15586
|
+
commit: "9a58493b4404",
|
|
15587
|
+
npm: "@mutmutco/cli@4.3.28"
|
|
15533
15588
|
},
|
|
15534
15589
|
exitCriterion: "fleet-n-of-n",
|
|
15535
15590
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15546,14 +15601,14 @@ var rollout_plan_default = {
|
|
|
15546
15601
|
repo: "mutmutco/mmi-hub",
|
|
15547
15602
|
role: "canary",
|
|
15548
15603
|
schedule: "train",
|
|
15549
|
-
v3Target: "v4.3.
|
|
15604
|
+
v3Target: "v4.3.28"
|
|
15550
15605
|
}
|
|
15551
15606
|
],
|
|
15552
15607
|
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.",
|
|
15553
15608
|
rollback: {
|
|
15554
15609
|
independent: true,
|
|
15555
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15556
|
-
v3Target: "v4.3.
|
|
15610
|
+
mechanism: "npm dist-tag latest -> 4.3.28 and redeploy the Hub Lambda from tag v4.3.28 (9a58493b4404); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15611
|
+
v3Target: "v4.3.28 (@mutmutco/cli@4.3.28, tag commit 9a58493b4404 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15557
15612
|
}
|
|
15558
15613
|
},
|
|
15559
15614
|
{
|
|
@@ -25575,11 +25630,12 @@ function registerBoardCommands(program3) {
|
|
|
25575
25630
|
function claimVerdict(ref, result) {
|
|
25576
25631
|
const holder = formatClaimHolder(result.holder);
|
|
25577
25632
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
25633
|
+
const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
|
|
25578
25634
|
if (result.checked) {
|
|
25579
25635
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
25580
25636
|
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
25581
25637
|
if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
|
|
25582
|
-
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
25638
|
+
return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
|
|
25583
25639
|
}
|
|
25584
25640
|
if (result.partial) {
|
|
25585
25641
|
if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
|
|
@@ -25589,12 +25645,12 @@ function registerBoardCommands(program3) {
|
|
|
25589
25645
|
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
25590
25646
|
if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
|
|
25591
25647
|
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
25592
|
-
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
25648
|
+
return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
|
|
25593
25649
|
}
|
|
25594
25650
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25595
25651
|
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
25596
25652
|
withExamples(mutating(
|
|
25597
|
-
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
25653
|
+
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
25598
25654
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25599
25655
|
).action(async (issueRefs, o) => {
|
|
25600
25656
|
if (issueRefs.length === 1) {
|
|
@@ -28949,7 +29005,7 @@ function parseOauthVar(raw) {
|
|
|
28949
29005
|
throw new Error(`org project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
28950
29006
|
}
|
|
28951
29007
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28952
|
-
throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
29008
|
+
throw new Error("org project set: oauth must be a {subdomains,domains,callbackPath,extraCallbackPaths,fofuSubdomain} object");
|
|
28953
29009
|
}
|
|
28954
29010
|
const map = parsed;
|
|
28955
29011
|
const out = {};
|
|
@@ -28963,14 +29019,22 @@ function parseOauthVar(raw) {
|
|
|
28963
29019
|
if (typeof value !== "string" || !value.trim()) throw new Error("org project set: oauth.callbackPath must be a non-empty string");
|
|
28964
29020
|
const callbackPath = value.trim();
|
|
28965
29021
|
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
28966
|
-
throw new Error(
|
|
29022
|
+
throw new Error(
|
|
29023
|
+
`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`
|
|
29024
|
+
);
|
|
28967
29025
|
}
|
|
28968
29026
|
out.callbackPath = callbackPath;
|
|
29027
|
+
} else if (key === "extraCallbackPaths") {
|
|
29028
|
+
try {
|
|
29029
|
+
out.extraCallbackPaths = parseExtraCallbackPaths(value);
|
|
29030
|
+
} catch (e) {
|
|
29031
|
+
throw new Error(`org project set: ${e.message}`);
|
|
29032
|
+
}
|
|
28969
29033
|
} else if (key === "fofuSubdomain") {
|
|
28970
29034
|
if (typeof value !== "string") throw new Error('org project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
28971
29035
|
out.fofuSubdomain = value.trim();
|
|
28972
29036
|
} else {
|
|
28973
|
-
throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/fofuSubdomain`);
|
|
29037
|
+
throw new Error(`org project set: oauth key "${key}" \u2014 expected only subdomains/domains/callbackPath/extraCallbackPaths/fofuSubdomain`);
|
|
28974
29038
|
}
|
|
28975
29039
|
}
|
|
28976
29040
|
return out;
|
|
@@ -29170,7 +29234,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
29170
29234
|
runtimeVaultOnly: "true|false",
|
|
29171
29235
|
seedCanary: "true|false",
|
|
29172
29236
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
29173
|
-
oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
|
|
29237
|
+
oauth: "JSON {subdomains,domains,callbackPath,extraCallbackPaths,fofuSubdomain}",
|
|
29174
29238
|
requiredGcpApis: "comma-string",
|
|
29175
29239
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
29176
29240
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
@@ -29642,6 +29706,51 @@ function findEnvFiles(root) {
|
|
|
29642
29706
|
return found.sort();
|
|
29643
29707
|
}
|
|
29644
29708
|
|
|
29709
|
+
// src/stage-runtime-env.ts
|
|
29710
|
+
var SERVICE_NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
29711
|
+
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29712
|
+
var RUNTIME_ENV_OVERRIDE_RELPATH = "tmp/stage/runtime-env.override.yml";
|
|
29713
|
+
function labelsMarkRuntimeEnv(labels) {
|
|
29714
|
+
if (Array.isArray(labels)) {
|
|
29715
|
+
return labels.some((l) => typeof l === "string" && /^mmi\.runtime-env\s*=\s*["']?true["']?$/.test(l.trim()));
|
|
29716
|
+
}
|
|
29717
|
+
if (labels && typeof labels === "object") {
|
|
29718
|
+
return String(labels["mmi.runtime-env"] ?? "") === "true";
|
|
29719
|
+
}
|
|
29720
|
+
return false;
|
|
29721
|
+
}
|
|
29722
|
+
function runtimeEnvMarkedServices(composeConfigJson) {
|
|
29723
|
+
let parsed;
|
|
29724
|
+
try {
|
|
29725
|
+
parsed = JSON.parse(composeConfigJson);
|
|
29726
|
+
} catch {
|
|
29727
|
+
return [];
|
|
29728
|
+
}
|
|
29729
|
+
const services = parsed?.services;
|
|
29730
|
+
if (!services || typeof services !== "object" || Array.isArray(services)) return [];
|
|
29731
|
+
return Object.entries(services).filter(([name, svc]) => SERVICE_NAME_RE.test(name) && labelsMarkRuntimeEnv(svc?.labels)).map(([name]) => name).sort();
|
|
29732
|
+
}
|
|
29733
|
+
function renderRuntimeEnvOverride(services, keys) {
|
|
29734
|
+
const safeServices = services.filter((s) => SERVICE_NAME_RE.test(s)).sort();
|
|
29735
|
+
const safeKeys = [...new Set(keys.filter((k) => ENV_KEY_RE.test(k)))].sort();
|
|
29736
|
+
if (!safeServices.length || !safeKeys.length) return null;
|
|
29737
|
+
const lines2 = [
|
|
29738
|
+
"# Generated by `mmi-cli stage` (#6345) \u2014 do not edit, do not commit.",
|
|
29739
|
+
'# Passes the vault-resolved runtime env through to every service labelled mmi.runtime-env: "true",',
|
|
29740
|
+
"# exactly as the box deploy does. KEY NAMES ONLY: the values live in the compose process env (#2655).",
|
|
29741
|
+
"services:"
|
|
29742
|
+
];
|
|
29743
|
+
for (const service of safeServices) {
|
|
29744
|
+
lines2.push(` ${service}:`, " environment:");
|
|
29745
|
+
for (const key of safeKeys) lines2.push(` ${key}: \${${key}:?}`);
|
|
29746
|
+
}
|
|
29747
|
+
return `${lines2.join("\n")}
|
|
29748
|
+
`;
|
|
29749
|
+
}
|
|
29750
|
+
function stageComposeFileEnv(files) {
|
|
29751
|
+
return { COMPOSE_PATH_SEPARATOR: ",", COMPOSE_FILE: files.join(",") };
|
|
29752
|
+
}
|
|
29753
|
+
|
|
29645
29754
|
// src/stage-runner.ts
|
|
29646
29755
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process11.execFile);
|
|
29647
29756
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
@@ -29994,6 +30103,49 @@ function stageExtraEnv(config, stagePort) {
|
|
|
29994
30103
|
function stageProcessEnv(stagePort, extraEnv) {
|
|
29995
30104
|
return { ...stagePort != null ? { STAGE_PORT: String(stagePort) } : {}, ...extraEnv };
|
|
29996
30105
|
}
|
|
30106
|
+
function composeResolvesPort(cwd) {
|
|
30107
|
+
if (process.env.PORT) return true;
|
|
30108
|
+
const envFile = (0, import_node_path27.join)(cwd, ".env");
|
|
30109
|
+
return (0, import_node_fs28.existsSync)(envFile) && envFileKeys((0, import_node_fs28.readFileSync)(envFile, "utf8")).has("PORT");
|
|
30110
|
+
}
|
|
30111
|
+
function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
|
|
30112
|
+
return {
|
|
30113
|
+
// Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
|
|
30114
|
+
...vaultEnvMerge ?? {},
|
|
30115
|
+
...stageProcessEnv(stagePort, stageExtraEnv(config, stagePort)),
|
|
30116
|
+
...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
|
|
30117
|
+
};
|
|
30118
|
+
}
|
|
30119
|
+
function stageComposeFiles(cwd) {
|
|
30120
|
+
const files = ["docker-compose.yml"];
|
|
30121
|
+
for (const conventional of ["docker-compose.override.yml", "docker-compose.override.yaml"]) {
|
|
30122
|
+
if ((0, import_node_fs28.existsSync)((0, import_node_path27.join)(cwd, conventional))) files.push(conventional);
|
|
30123
|
+
}
|
|
30124
|
+
return [...files, RUNTIME_ENV_OVERRIDE_RELPATH];
|
|
30125
|
+
}
|
|
30126
|
+
async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
|
|
30127
|
+
const keys = Object.keys(vaultEnvMerge ?? {});
|
|
30128
|
+
if (!keys.length || !(0, import_node_fs28.existsSync)((0, import_node_path27.join)(cwd, "docker-compose.yml"))) return {};
|
|
30129
|
+
let configJson;
|
|
30130
|
+
try {
|
|
30131
|
+
const { stdout } = await execFileP3("docker", ["compose", "config", "--format", "json"], {
|
|
30132
|
+
cwd,
|
|
30133
|
+
windowsHide: true,
|
|
30134
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
30135
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
30136
|
+
env: { ...process.env, ...composeEnv }
|
|
30137
|
+
});
|
|
30138
|
+
configJson = stdout;
|
|
30139
|
+
} catch {
|
|
30140
|
+
return {};
|
|
30141
|
+
}
|
|
30142
|
+
const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys);
|
|
30143
|
+
if (!body) return {};
|
|
30144
|
+
const overridePath = (0, import_node_path27.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
|
|
30145
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path27.join)(cwd, "tmp", "stage"), { recursive: true });
|
|
30146
|
+
(0, import_node_fs28.writeFileSync)(overridePath, body, "utf8");
|
|
30147
|
+
return stageComposeFileEnv(stageComposeFiles(cwd));
|
|
30148
|
+
}
|
|
29997
30149
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
29998
30150
|
if (!config.ensureEnv) return;
|
|
29999
30151
|
const target = (0, import_node_path27.join)(cwd, config.ensureEnv.target);
|
|
@@ -30069,10 +30221,19 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
|
30069
30221
|
writeState(statePath, reservation);
|
|
30070
30222
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
|
|
30071
30223
|
}
|
|
30072
|
-
|
|
30224
|
+
function teardownEnv(state, currentEnv) {
|
|
30225
|
+
const env = { ...state.teardown?.env ?? {}, ...currentEnv ?? {} };
|
|
30226
|
+
return Object.keys(env).length ? env : void 0;
|
|
30227
|
+
}
|
|
30228
|
+
async function cleanupStageState(state, paths, timeoutMs, fallbackCwd, currentEnv) {
|
|
30073
30229
|
await killTree(state.pid);
|
|
30074
30230
|
if (state.teardown?.command.trim()) {
|
|
30075
|
-
await shell(
|
|
30231
|
+
await shell(
|
|
30232
|
+
state.teardown.command.trim(),
|
|
30233
|
+
state.teardown.cwd || state.cwd || fallbackCwd,
|
|
30234
|
+
Math.max(timeoutMs, 1e4),
|
|
30235
|
+
teardownEnv(state, currentEnv)
|
|
30236
|
+
);
|
|
30076
30237
|
}
|
|
30077
30238
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
30078
30239
|
(0, import_node_fs28.rmSync)(path2, { force: true });
|
|
@@ -30130,13 +30291,20 @@ async function stopStage(opts = {}) {
|
|
|
30130
30291
|
}
|
|
30131
30292
|
const usingGlobalState = state === globalState;
|
|
30132
30293
|
const recordedStatePath = state.statePath ?? statePath;
|
|
30133
|
-
|
|
30294
|
+
const recordedTeardownEnv = Boolean(state.teardown?.env);
|
|
30295
|
+
await cleanupStageState(
|
|
30296
|
+
state,
|
|
30297
|
+
[statePath, recordedStatePath, usingGlobalState || !opts.requiredIdentityCwd ? globalStatePath : void 0],
|
|
30298
|
+
opts.timeoutMs ?? 6e4,
|
|
30299
|
+
cwd,
|
|
30300
|
+
opts.vaultEnvMerge
|
|
30301
|
+
);
|
|
30134
30302
|
return {
|
|
30135
30303
|
ok: true,
|
|
30136
30304
|
action: "stop",
|
|
30137
30305
|
statePath: recordedStatePath,
|
|
30138
30306
|
pid: state.pid,
|
|
30139
|
-
message: `stopped previous stage pid ${state.pid}
|
|
30307
|
+
message: `stopped previous stage pid ${state.pid}` + (state.teardown?.command.trim() ? ` and ran teardown${recordedTeardownEnv ? "" : " (no recorded teardown env \u2014 used the current environment)"}` : "")
|
|
30140
30308
|
};
|
|
30141
30309
|
}
|
|
30142
30310
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -30156,8 +30324,8 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30156
30324
|
const sub = (s) => substituteStagePort(s, stagePort);
|
|
30157
30325
|
if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30158
30326
|
if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
|
|
30159
|
-
const
|
|
30160
|
-
const
|
|
30327
|
+
const composeEnv = stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd);
|
|
30328
|
+
const upEnv = { ...composeEnv, ...await prepareRuntimeEnvPassthrough(cwd, composeEnv, opts.vaultEnvMerge) };
|
|
30161
30329
|
let up = sub(config.up.trim());
|
|
30162
30330
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
30163
30331
|
const identity = await resolveStageIdentity(cwd);
|
|
@@ -30170,8 +30338,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30170
30338
|
detached: process.platform !== "win32",
|
|
30171
30339
|
windowsHide: true,
|
|
30172
30340
|
stdio: "ignore",
|
|
30173
|
-
|
|
30174
|
-
env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
|
|
30341
|
+
env: { ...process.env, ...upEnv }
|
|
30175
30342
|
});
|
|
30176
30343
|
const state = {
|
|
30177
30344
|
pid: child2.pid ?? 0,
|
|
@@ -30182,7 +30349,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30182
30349
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
30183
30350
|
port: stagePort,
|
|
30184
30351
|
identity,
|
|
30185
|
-
|
|
30352
|
+
// #6343: record the non-secret half of the interpolation env so a LATER `stage stop` can run
|
|
30353
|
+
// `docker compose down` against the same file. `vaultEnvMerge` is omitted deliberately — no secret
|
|
30354
|
+
// value ever reaches disk (#2655); the stopping invocation re-fetches them.
|
|
30355
|
+
// #6345: the passthrough `COMPOSE_FILE` is omitted too. The override only ADDS container env to services
|
|
30356
|
+
// the project file already declares, so `down` removes the same containers without it — and a later
|
|
30357
|
+
// `stage stop` that resolved no secrets would otherwise die on the override's `${KEY:?}`.
|
|
30358
|
+
teardown: config.teardown?.trim() ? { command: sub(config.teardown.trim()), cwd, env: stageComposeEnv(config, stagePort, void 0, cwd) } : void 0
|
|
30186
30359
|
};
|
|
30187
30360
|
writeState(statePath, state);
|
|
30188
30361
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, state);
|
|
@@ -30190,7 +30363,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30190
30363
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
30191
30364
|
else await waitForProcessStability(child2);
|
|
30192
30365
|
} catch (e) {
|
|
30193
|
-
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd);
|
|
30366
|
+
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, upEnv);
|
|
30194
30367
|
throw e;
|
|
30195
30368
|
}
|
|
30196
30369
|
const result = {
|
|
@@ -30221,7 +30394,12 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30221
30394
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
30222
30395
|
const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
|
|
30223
30396
|
const portGuard = resolveStagePortGuard(opts);
|
|
30224
|
-
await stopStage({
|
|
30397
|
+
await stopStage({
|
|
30398
|
+
...opts,
|
|
30399
|
+
cwd,
|
|
30400
|
+
requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd,
|
|
30401
|
+
vaultEnvMerge: stageComposeEnv(config, opts.stagePort, opts.vaultEnvMerge, cwd)
|
|
30402
|
+
});
|
|
30225
30403
|
const reserved = await reservedPortsForWorktree(cwd);
|
|
30226
30404
|
let stagePort = opts.stagePort;
|
|
30227
30405
|
if (stagePort != null) {
|
|
@@ -30233,14 +30411,13 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30233
30411
|
if (stagePort != null) {
|
|
30234
30412
|
writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
|
|
30235
30413
|
}
|
|
30236
|
-
const extraEnv = stageExtraEnv(config, stagePort);
|
|
30237
30414
|
const build = config.build?.trim();
|
|
30238
30415
|
const ranBuild = Boolean(build);
|
|
30239
30416
|
try {
|
|
30240
30417
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30241
30418
|
if (build) {
|
|
30242
30419
|
await shell(sub(build), cwd, timeoutMs, {
|
|
30243
|
-
...
|
|
30420
|
+
...stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd),
|
|
30244
30421
|
...opts.buildEnvMerge ?? {}
|
|
30245
30422
|
});
|
|
30246
30423
|
}
|
|
@@ -35957,7 +36134,17 @@ function registerStageCommands(program3) {
|
|
|
35957
36134
|
return printLine(o.json ? JSON.stringify({ command: "stage stop", steps }, null, 2) : renderSteps("mmi-cli stage stop: dry-run plan", steps));
|
|
35958
36135
|
}
|
|
35959
36136
|
try {
|
|
35960
|
-
|
|
36137
|
+
let vaultEnvMerge;
|
|
36138
|
+
try {
|
|
36139
|
+
const { project: project2, cfg: stageCfg } = await resolveStage();
|
|
36140
|
+
vaultEnvMerge = await fetchStageVaultEnvMerge(project2, stageCfg);
|
|
36141
|
+
} catch {
|
|
36142
|
+
}
|
|
36143
|
+
const result = await stopStage({
|
|
36144
|
+
cwd: process.cwd(),
|
|
36145
|
+
requiredIdentityCwd: process.cwd(),
|
|
36146
|
+
...vaultEnvMerge ? { vaultEnvMerge } : {}
|
|
36147
|
+
});
|
|
35961
36148
|
printLine(o.json ? JSON.stringify(result) : `mmi-cli stage stop: ${result.message}`);
|
|
35962
36149
|
} catch (e) {
|
|
35963
36150
|
fail(`stage stop: ${e.message}`);
|
|
@@ -42461,6 +42648,30 @@ ${recovery.note}; the full required-check wall did not revalidate`);
|
|
|
42461
42648
|
}, `the immutable ${anchors.tag} pushed at ${mergedSha.slice(0, 12)}`);
|
|
42462
42649
|
return checks;
|
|
42463
42650
|
}
|
|
42651
|
+
async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
42652
|
+
let bomText;
|
|
42653
|
+
try {
|
|
42654
|
+
bomText = await deps.run("git", ["show", `${mergedSha}:distribution-bom.json`]);
|
|
42655
|
+
} catch {
|
|
42656
|
+
return;
|
|
42657
|
+
}
|
|
42658
|
+
let stamp;
|
|
42659
|
+
try {
|
|
42660
|
+
stamp = JSON.parse(bomText).sourceCommit;
|
|
42661
|
+
} catch (e) {
|
|
42662
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} is unparseable (${e.message}) \u2014 the updater would refuse this release; fix the BOM on development and rerun from a fresh port`);
|
|
42663
|
+
}
|
|
42664
|
+
if (typeof stamp !== "string" || !/^[0-9a-f]{40}$/.test(stamp)) {
|
|
42665
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} carries no 40-hex sourceCommit \u2014 the updater would refuse this release`);
|
|
42666
|
+
}
|
|
42667
|
+
try {
|
|
42668
|
+
await deps.run("git", ["merge-base", "--is-ancestor", stamp, mergedSha]);
|
|
42669
|
+
} catch {
|
|
42670
|
+
throw new Error(
|
|
42671
|
+
`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} stamps sourceCommit ${stamp.slice(0, 12)}, which the merged main commit does not contain \u2014 every updater would reject ${tag} (gateCandidate ancestry). The fold was stamped on the hotfix branch and squash-merged away: make the consumer stamp a durable merge-base (Jerv-Hub#1084), land that on development, and port it with the fix \u2014 see docs/Guides/train-troubleshooting.md#hotfix-bom-lineage`
|
|
42672
|
+
);
|
|
42673
|
+
}
|
|
42674
|
+
}
|
|
42464
42675
|
async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTrainDoctor) {
|
|
42465
42676
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
42466
42677
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -42494,6 +42705,8 @@ async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTr
|
|
|
42494
42705
|
assertTagAddressableRequiredContexts(deps, required, ctx.repo);
|
|
42495
42706
|
if (deployModel === "hub-serverless") {
|
|
42496
42707
|
await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", version, "--release-commit", mergedSha]);
|
|
42708
|
+
} else {
|
|
42709
|
+
await assertConsumerBomLineage(deps, mergedSha, tag);
|
|
42497
42710
|
}
|
|
42498
42711
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
42499
42712
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
|
@@ -44223,11 +44436,55 @@ function resolveParseHint() {
|
|
|
44223
44436
|
`Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2} --json\` for exact detail, or \`mmi-cli commands\` for the task map.`
|
|
44224
44437
|
].join("\n");
|
|
44225
44438
|
}
|
|
44439
|
+
function positionalCommandEntries(house) {
|
|
44440
|
+
const acc = [];
|
|
44441
|
+
const walk2 = (node) => {
|
|
44442
|
+
if (commandMetadata(node)?.category !== "internal") {
|
|
44443
|
+
const args = node.registeredArguments ?? [];
|
|
44444
|
+
const path2 = commandPath2(node);
|
|
44445
|
+
const canonical = path2 ? canonicalPathFor(path2) ?? path2 : "";
|
|
44446
|
+
if (args[0] && canonical && (house === void 0 || houseForPath(canonical) === house)) {
|
|
44447
|
+
acc.push({
|
|
44448
|
+
path: canonical,
|
|
44449
|
+
names: [node.name(), ...node.aliases()],
|
|
44450
|
+
argName: args[0].name(),
|
|
44451
|
+
ownFlags: commandOwnLongFlags(node)
|
|
44452
|
+
});
|
|
44453
|
+
}
|
|
44454
|
+
}
|
|
44455
|
+
for (const child2 of node.commands) walk2(child2);
|
|
44456
|
+
};
|
|
44457
|
+
for (const child2 of program2.commands) walk2(child2);
|
|
44458
|
+
return acc;
|
|
44459
|
+
}
|
|
44460
|
+
function excessArgumentCorrection(extra, argv) {
|
|
44461
|
+
const invoked = resolveCommandFromArgv(program2, [...argv]);
|
|
44462
|
+
if (!invoked || extra.length !== 1) return void 0;
|
|
44463
|
+
const canonical = canonicalPathFor(commandPath2(invoked)) ?? commandPath2(invoked);
|
|
44464
|
+
const entries = positionalCommandEntries(houseForPath(canonical)).filter((e) => e.path !== canonical);
|
|
44465
|
+
const sibling = siblingTakingPositional(invoked.name(), entries);
|
|
44466
|
+
return sibling ? correctedPositionalCommand(sibling, extra[0], argv) : void 0;
|
|
44467
|
+
}
|
|
44468
|
+
var pendingHumanParseError;
|
|
44469
|
+
function writeHumanParseError(str) {
|
|
44470
|
+
pendingHumanParseError = str;
|
|
44471
|
+
}
|
|
44226
44472
|
function envelopeAwareWriteErr(str) {
|
|
44227
44473
|
const plain = str.replace(/\[[0-9;]*m/g, "");
|
|
44228
44474
|
if (plain.includes(PARSE_HINT_SENTINEL)) {
|
|
44229
|
-
if (unknownFlagJsonHandled && argvWantsJson3())
|
|
44230
|
-
|
|
44475
|
+
if (unknownFlagJsonHandled && argvWantsJson3()) {
|
|
44476
|
+
pendingHumanParseError = void 0;
|
|
44477
|
+
return;
|
|
44478
|
+
}
|
|
44479
|
+
const hint = str.replace(PARSE_HINT_SENTINEL, resolveParseHint());
|
|
44480
|
+
if (lastParseErrorKind === "bad-arguments") {
|
|
44481
|
+
process.stderr.write(hint);
|
|
44482
|
+
if (pendingHumanParseError) process.stderr.write(pendingHumanParseError);
|
|
44483
|
+
} else {
|
|
44484
|
+
if (pendingHumanParseError) process.stderr.write(pendingHumanParseError);
|
|
44485
|
+
process.stderr.write(hint);
|
|
44486
|
+
}
|
|
44487
|
+
pendingHumanParseError = void 0;
|
|
44231
44488
|
return;
|
|
44232
44489
|
}
|
|
44233
44490
|
lastParseErrorKind = classifyParseError(plain);
|
|
@@ -44235,7 +44492,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
44235
44492
|
const badChoice = parseInvalidChoiceError(plain);
|
|
44236
44493
|
if (badChoice) {
|
|
44237
44494
|
if (!argvWantsJson3()) {
|
|
44238
|
-
|
|
44495
|
+
writeHumanParseError(str);
|
|
44239
44496
|
return;
|
|
44240
44497
|
}
|
|
44241
44498
|
process.stderr.write(
|
|
@@ -44248,6 +44505,24 @@ function envelopeAwareWriteErr(str) {
|
|
|
44248
44505
|
unknownFlagJsonHandled = true;
|
|
44249
44506
|
return;
|
|
44250
44507
|
}
|
|
44508
|
+
const excess = parseExcessArgumentsError(plain);
|
|
44509
|
+
if (excess) {
|
|
44510
|
+
const argv = process.argv.slice(2);
|
|
44511
|
+
const corrected = excessArgumentCorrection(excess.extra, argv);
|
|
44512
|
+
const message2 = excessArgumentMessage(excess.extra, corrected);
|
|
44513
|
+
if (argvWantsJson3()) {
|
|
44514
|
+
process.stderr.write(
|
|
44515
|
+
formatErrorEnvelope(message2, {
|
|
44516
|
+
code: ERROR_CODES.ERR_EXCESS_ARGUMENT,
|
|
44517
|
+
...corrected ? { corrected_command: corrected } : {}
|
|
44518
|
+
}) + "\n"
|
|
44519
|
+
);
|
|
44520
|
+
unknownFlagJsonHandled = true;
|
|
44521
|
+
return;
|
|
44522
|
+
}
|
|
44523
|
+
writeHumanParseError(str.replace(/error: too many arguments[^\n]*/, `error: ${message2}`));
|
|
44524
|
+
return;
|
|
44525
|
+
}
|
|
44251
44526
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
44252
44527
|
if (match) {
|
|
44253
44528
|
const flag = match[1];
|
|
@@ -44271,14 +44546,18 @@ function envelopeAwareWriteErr(str) {
|
|
|
44271
44546
|
return;
|
|
44272
44547
|
}
|
|
44273
44548
|
if (positional) {
|
|
44274
|
-
|
|
44549
|
+
writeHumanParseError(str.replace(/unknown option '[^']+'/, unknownTargetFlagMessage(flag, positional)));
|
|
44275
44550
|
return;
|
|
44276
44551
|
}
|
|
44277
|
-
|
|
44552
|
+
writeHumanParseError(str);
|
|
44278
44553
|
return;
|
|
44279
44554
|
}
|
|
44280
44555
|
if (unknownFlagJsonHandled && argvWantsJson3()) return;
|
|
44281
|
-
|
|
44556
|
+
if (argvWantsJson3()) {
|
|
44557
|
+
process.stderr.write(str);
|
|
44558
|
+
return;
|
|
44559
|
+
}
|
|
44560
|
+
writeHumanParseError(str);
|
|
44282
44561
|
}
|
|
44283
44562
|
var INVOKED_ARGV3 = process.argv.slice(2);
|
|
44284
44563
|
var program2 = new Command();
|
package/package.json
CHANGED