@mutmutco/cli 2.14.1 → 2.15.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/dist/index.cjs +736 -95
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4301,6 +4301,7 @@ var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
|
4301
4301
|
var rawExecFileP2 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
|
|
4302
4302
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
4303
4303
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
4304
|
+
var CLAIM_CONCURRENCY = 5;
|
|
4304
4305
|
var execFileP2 = (file, args, options = {}) => (
|
|
4305
4306
|
// encoding 'utf8' guarantees string output at runtime; the cast pins the type (promisify's
|
|
4306
4307
|
// overloads widen to string|Buffer when options is spread in).
|
|
@@ -4675,17 +4676,16 @@ async function showBoardItem(options, deps = {}) {
|
|
|
4675
4676
|
}
|
|
4676
4677
|
return item;
|
|
4677
4678
|
}
|
|
4678
|
-
async function
|
|
4679
|
+
async function prepareClaimContext(options, selectors, deps, collected) {
|
|
4679
4680
|
const cfg = resolveBoardConfig(options.config);
|
|
4680
4681
|
const client = deps.client ?? defaultGitHubClient();
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
collected.items.push(fallback);
|
|
4682
|
+
for (const selector of selectors) {
|
|
4683
|
+
try {
|
|
4684
|
+
findBoardItem(collected.items, selector);
|
|
4685
|
+
} catch {
|
|
4686
|
+
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
4687
|
+
if (fallback) collected.items.push(fallback);
|
|
4688
|
+
}
|
|
4689
4689
|
}
|
|
4690
4690
|
const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
|
|
4691
4691
|
collected.warnings.push(...writable.warnings);
|
|
@@ -4698,8 +4698,12 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4698
4698
|
warnings: collected.warnings,
|
|
4699
4699
|
partial: collected.partial
|
|
4700
4700
|
};
|
|
4701
|
-
|
|
4702
|
-
|
|
4701
|
+
return { cfg, client, items: collected.items, writable: writable.repos, report };
|
|
4702
|
+
}
|
|
4703
|
+
async function claimOneBoardItem(ctx, selector, options) {
|
|
4704
|
+
const { cfg, client, report } = ctx;
|
|
4705
|
+
const flatItem = findBoardItem(ctx.items, selector);
|
|
4706
|
+
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
4703
4707
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
4704
4708
|
}
|
|
4705
4709
|
let item = findClaimableItem(report, selector);
|
|
@@ -4736,6 +4740,49 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
4736
4740
|
partial: false
|
|
4737
4741
|
};
|
|
4738
4742
|
}
|
|
4743
|
+
async function claimBoardIssue(options, deps = {}) {
|
|
4744
|
+
const cfg = resolveBoardConfig(options.config);
|
|
4745
|
+
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
|
|
4746
|
+
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
4747
|
+
const ctx = await prepareClaimContext(options, [selector], deps, collected);
|
|
4748
|
+
return claimOneBoardItem(ctx, selector, options);
|
|
4749
|
+
}
|
|
4750
|
+
async function claimBoardIssues(options, deps = {}) {
|
|
4751
|
+
const cfg = resolveBoardConfig(options.config);
|
|
4752
|
+
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
|
|
4753
|
+
const selectors = [];
|
|
4754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4755
|
+
for (const raw of options.selectors) {
|
|
4756
|
+
const selector = parseIssueSelector(raw, collected.repo);
|
|
4757
|
+
const key = `${selector.repo.toLowerCase()}#${selector.number}`;
|
|
4758
|
+
if (seen.has(key)) continue;
|
|
4759
|
+
seen.add(key);
|
|
4760
|
+
selectors.push(selector);
|
|
4761
|
+
}
|
|
4762
|
+
const ctx = await prepareClaimContext(options, selectors, deps, collected);
|
|
4763
|
+
const results = new Array(selectors.length);
|
|
4764
|
+
let next = 0;
|
|
4765
|
+
const worker = async () => {
|
|
4766
|
+
while (next < selectors.length) {
|
|
4767
|
+
const index = next++;
|
|
4768
|
+
const selector = selectors[index];
|
|
4769
|
+
const ref = `${selector.repo}#${selector.number}`;
|
|
4770
|
+
try {
|
|
4771
|
+
const result = await claimOneBoardItem(ctx, selector, options);
|
|
4772
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning };
|
|
4773
|
+
} catch (e) {
|
|
4774
|
+
results[index] = { ref, claimed: false, reason: e.message };
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
};
|
|
4778
|
+
await Promise.all(Array.from({ length: Math.min(CLAIM_CONCURRENCY, selectors.length) }, () => worker()));
|
|
4779
|
+
return {
|
|
4780
|
+
viewer: ctx.report.viewer,
|
|
4781
|
+
repo: ctx.report.repo,
|
|
4782
|
+
results,
|
|
4783
|
+
failed: results.filter((result) => !result.claimed).length
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4739
4786
|
async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
4740
4787
|
if (!isPriorityFieldConfigured(cfg)) return void 0;
|
|
4741
4788
|
const optionId = resolvePriorityOptionId(cfg, priority);
|
|
@@ -5315,6 +5362,7 @@ function trainPlan(command) {
|
|
|
5315
5362
|
{ label: "verify current branch is rc", gated: true },
|
|
5316
5363
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5317
5364
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5365
|
+
{ label: "verify every main-only hotfix commit is covered by the rc candidate", command: "node scripts/hotfix-coverage.mjs", gated: true },
|
|
5318
5366
|
{ label: "merge rc to main", gated: true },
|
|
5319
5367
|
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5320
5368
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
@@ -5323,10 +5371,10 @@ function trainPlan(command) {
|
|
|
5323
5371
|
];
|
|
5324
5372
|
}
|
|
5325
5373
|
return [
|
|
5326
|
-
{ label: "
|
|
5327
|
-
{ label: "
|
|
5328
|
-
{ label: "deploy prod", gated: true },
|
|
5329
|
-
{ label: "
|
|
5374
|
+
{ label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
|
|
5375
|
+
{ label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
|
|
5376
|
+
{ label: "land on main via PR, tag, deploy prod", gated: true },
|
|
5377
|
+
{ label: "forward-bump development distribution manifests (Hub only; no back-merge)", gated: true }
|
|
5330
5378
|
];
|
|
5331
5379
|
}
|
|
5332
5380
|
function bootstrapPlan(repo, repoClass) {
|
|
@@ -5405,9 +5453,9 @@ function stalePosixFields(config, shell2) {
|
|
|
5405
5453
|
}
|
|
5406
5454
|
function sanitizeLocalStage(local, stale) {
|
|
5407
5455
|
if (!stale.length) return local;
|
|
5408
|
-
const
|
|
5409
|
-
for (const field of stale) delete
|
|
5410
|
-
return
|
|
5456
|
+
const clean3 = { ...local };
|
|
5457
|
+
for (const field of stale) delete clean3[field];
|
|
5458
|
+
return clean3;
|
|
5411
5459
|
}
|
|
5412
5460
|
function staleNote(staleFields, outcome) {
|
|
5413
5461
|
const list = staleFields.join(", ");
|
|
@@ -5679,7 +5727,7 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
5679
5727
|
return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
|
|
5680
5728
|
}
|
|
5681
5729
|
var MMI_PLUGIN_CACHE_CLEANUP_LABEL = "stale MMI plugin cache dirs (Claude/Codex)";
|
|
5682
|
-
var MMI_PLUGIN_CACHE_CLEANUP_FIX = "run `mmi-cli doctor` to quarantine stale MMI-only plugin cache dirs, then reload the affected agent surface";
|
|
5730
|
+
var MMI_PLUGIN_CACHE_CLEANUP_FIX = "run `mmi-cli doctor` (or `mmi-cli doctor --apply --json` for machine callers) to quarantine stale MMI-only plugin cache dirs, then reload the affected agent surface";
|
|
5683
5731
|
function normalizeVersion(v) {
|
|
5684
5732
|
return v?.trim().replace(/^v/, "");
|
|
5685
5733
|
}
|
|
@@ -5714,7 +5762,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
5714
5762
|
...base,
|
|
5715
5763
|
ok: false,
|
|
5716
5764
|
leftovers,
|
|
5717
|
-
|
|
5765
|
+
plannedCount: leftovers.length,
|
|
5718
5766
|
quarantinePlan: leftovers.map((entry) => ({
|
|
5719
5767
|
from: entry.path,
|
|
5720
5768
|
to: cachePathJoin(entry.root, ".mmi-quarantine", stamp, entry.name)
|
|
@@ -5768,6 +5816,16 @@ var INSTALLED_PLUGIN_VERSION_LABEL = "installed MMI plugin version (vs latest re
|
|
|
5768
5816
|
function isSemverVersion(v) {
|
|
5769
5817
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
5770
5818
|
}
|
|
5819
|
+
function staleRecordCommand(surface) {
|
|
5820
|
+
return surface === "codex" ? "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi" : "claude plugin marketplace update mmi && claude plugin update mmi@mmi";
|
|
5821
|
+
}
|
|
5822
|
+
function staleSurfacesFix(stale, releasedVersion) {
|
|
5823
|
+
const parts = stale.map((s) => {
|
|
5824
|
+
const at = s.recordPath ? ` (${s.recordPath})` : "";
|
|
5825
|
+
return `${s.surface} record${at} is at ${s.installedVersion}${releasedVersion ? ` < ${releasedVersion}` : ""} \u2014 run: ${staleRecordCommand(s.surface)}`;
|
|
5826
|
+
});
|
|
5827
|
+
return `stale installed-plugin record on ${stale.map((s) => s.surface).join(" + ")}: ${parts.join("; ")}`;
|
|
5828
|
+
}
|
|
5771
5829
|
function buildInstalledPluginVersionCheck(input) {
|
|
5772
5830
|
const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
|
|
5773
5831
|
const base = {
|
|
@@ -5776,21 +5834,94 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
5776
5834
|
fix: pluginRecoveryFix(input.surface),
|
|
5777
5835
|
pluginId
|
|
5778
5836
|
};
|
|
5779
|
-
if (!input.isOrgRepo) return base;
|
|
5780
|
-
const
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5837
|
+
if (!input.isOrgRepo || !isSemverVersion(input.releasedVersion)) return base;
|
|
5838
|
+
const stale = [];
|
|
5839
|
+
let sawRecord = false;
|
|
5840
|
+
let currentVersion;
|
|
5841
|
+
for (const source of input.sources) {
|
|
5842
|
+
const records = source.installed?.plugins?.[pluginId];
|
|
5843
|
+
if (!Array.isArray(records) || records.length === 0) continue;
|
|
5844
|
+
sawRecord = true;
|
|
5845
|
+
const installedVersion = bestRecord(records).version;
|
|
5846
|
+
if (!isSemverVersion(installedVersion)) continue;
|
|
5847
|
+
if (compareVersions(installedVersion, input.releasedVersion) >= 0) {
|
|
5848
|
+
currentVersion = currentVersion ?? installedVersion;
|
|
5849
|
+
} else {
|
|
5850
|
+
stale.push({ surface: source.surface, installedVersion, ...source.recordPath ? { recordPath: source.recordPath } : {} });
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
if (!sawRecord) return base;
|
|
5854
|
+
if (stale.length === 0) {
|
|
5855
|
+
return { ...base, ...currentVersion ? { installedVersion: currentVersion } : {}, releasedVersion: input.releasedVersion };
|
|
5786
5856
|
}
|
|
5787
5857
|
return {
|
|
5788
5858
|
...base,
|
|
5789
5859
|
ok: false,
|
|
5790
|
-
|
|
5791
|
-
|
|
5860
|
+
fix: staleSurfacesFix(stale, input.releasedVersion),
|
|
5861
|
+
installedVersion: stale[0].installedVersion,
|
|
5862
|
+
releasedVersion: input.releasedVersion,
|
|
5863
|
+
staleSurfaces: stale
|
|
5792
5864
|
};
|
|
5793
5865
|
}
|
|
5866
|
+
var CURSOR_PLUGIN_INSTALL_LABEL = "Cursor Team Marketplace plugin install";
|
|
5867
|
+
var CURSOR_MARKETPLACE_INSTALL_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/cursor-marketplace-install.md";
|
|
5868
|
+
var CURSOR_PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
5869
|
+
var CURSOR_HOOKS_JSON_REL = "hooks/hooks.json";
|
|
5870
|
+
function joinCachePath(root, ...parts) {
|
|
5871
|
+
const sep = root.includes("\\") ? "\\" : "/";
|
|
5872
|
+
return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
|
|
5873
|
+
}
|
|
5874
|
+
function cursorPluginInstallFix(input) {
|
|
5875
|
+
const logHint = "check %APPDATA%\\Cursor\\logs\\<session>\\window*\\exthost\\anysphere.cursor-agent-exec\\Cursor Plugins.*.log for `unable to get password from user`";
|
|
5876
|
+
const authSteps = "ensure GitHub SSH auth works for Cursor's background git (start ssh-agent, add your GitHub key, verify `ssh -T git@github.com`) \u2014 Cursor disables credential.helper during marketplace clone";
|
|
5877
|
+
const marketplaceRefresh = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace, enable MMI, then restart Cursor";
|
|
5878
|
+
const guide = `full recovery: ${CURSOR_MARKETPLACE_INSTALL_GUIDE}`;
|
|
5879
|
+
if (input.reason === "missing-cache") {
|
|
5880
|
+
return `${marketplaceRefresh}; ${authSteps}; ${guide}`;
|
|
5881
|
+
}
|
|
5882
|
+
const pin = input.pinName ?? "<commit-pin>";
|
|
5883
|
+
const cacheDir = joinCachePath(input.cacheRoot, pin);
|
|
5884
|
+
const localSeed = input.hubCheckout ? `temporary fallback: copy ${joinCachePath(input.hubCheckout, "plugins", "mmi")} to ${cacheDir}, then restart Cursor` : `temporary fallback: copy plugins/mmi from a local MMI-Hub checkout to ${cacheDir}, then restart Cursor`;
|
|
5885
|
+
return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL} or ${CURSOR_HOOKS_JSON_REL} \u2014 ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
|
|
5886
|
+
}
|
|
5887
|
+
function buildCursorPluginInstallCheck(input) {
|
|
5888
|
+
const base = {
|
|
5889
|
+
ok: true,
|
|
5890
|
+
label: CURSOR_PLUGIN_INSTALL_LABEL,
|
|
5891
|
+
fix: cursorPluginInstallFix({ reason: "missing-cache", cacheRoot: input.cacheRoot })
|
|
5892
|
+
};
|
|
5893
|
+
if (!input.isOrgRepo) return base;
|
|
5894
|
+
const shouldCheck = input.surface === "cursor" || input.cacheRootExists;
|
|
5895
|
+
if (!shouldCheck) return base;
|
|
5896
|
+
if (input.surface === "cursor" && input.pins.length === 0) {
|
|
5897
|
+
return {
|
|
5898
|
+
...base,
|
|
5899
|
+
ok: false,
|
|
5900
|
+
cacheRoot: input.cacheRoot,
|
|
5901
|
+
pins: [],
|
|
5902
|
+
reason: "missing-cache",
|
|
5903
|
+
fix: cursorPluginInstallFix({ reason: "missing-cache", cacheRoot: input.cacheRoot, hubCheckout: input.hubCheckout })
|
|
5904
|
+
};
|
|
5905
|
+
}
|
|
5906
|
+
for (const pin of input.pins) {
|
|
5907
|
+
if (!pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty) {
|
|
5908
|
+
return {
|
|
5909
|
+
...base,
|
|
5910
|
+
ok: false,
|
|
5911
|
+
cacheRoot: input.cacheRoot,
|
|
5912
|
+
pins: input.pins,
|
|
5913
|
+
reason: "incomplete-cache",
|
|
5914
|
+
fix: cursorPluginInstallFix({
|
|
5915
|
+
reason: "incomplete-cache",
|
|
5916
|
+
cacheRoot: input.cacheRoot,
|
|
5917
|
+
pinName: pin.name,
|
|
5918
|
+
hubCheckout: input.hubCheckout
|
|
5919
|
+
})
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
5924
|
+
}
|
|
5794
5925
|
|
|
5795
5926
|
// src/stage-runner.ts
|
|
5796
5927
|
var import_node_child_process5 = require("node:child_process");
|
|
@@ -6060,9 +6191,9 @@ async function predictMergeConflicts(deps, ours, theirs) {
|
|
|
6060
6191
|
return files;
|
|
6061
6192
|
}
|
|
6062
6193
|
}
|
|
6063
|
-
async function
|
|
6194
|
+
async function mergeWithSpineResolution(deps, sourceRef, label, resolve) {
|
|
6064
6195
|
try {
|
|
6065
|
-
await deps.run("git", ["merge",
|
|
6196
|
+
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
6066
6197
|
return;
|
|
6067
6198
|
} catch {
|
|
6068
6199
|
}
|
|
@@ -6071,10 +6202,10 @@ async function mergeRcWithSpineResolution(deps) {
|
|
|
6071
6202
|
if (unmerged.length === 0 || nonSpine.length > 0) {
|
|
6072
6203
|
await deps.run("git", ["merge", "--abort"]);
|
|
6073
6204
|
throw new Error(
|
|
6074
|
-
unmerged.length === 0 ?
|
|
6205
|
+
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${nonSpine.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
|
|
6075
6206
|
);
|
|
6076
6207
|
}
|
|
6077
|
-
await deps.run("git", ["checkout",
|
|
6208
|
+
await deps.run("git", ["checkout", `--${resolve}`, "--", ...unmerged]);
|
|
6078
6209
|
await deps.run("git", ["add", "--", ...unmerged]);
|
|
6079
6210
|
await deps.run("git", ["commit", "--no-edit"]);
|
|
6080
6211
|
}
|
|
@@ -6115,9 +6246,10 @@ var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
|
6115
6246
|
var CORRELATE_ATTEMPTS = 5;
|
|
6116
6247
|
var CORRELATE_DELAY_MS = 1500;
|
|
6117
6248
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
6118
|
-
var
|
|
6119
|
-
var
|
|
6120
|
-
var
|
|
6249
|
+
var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
|
|
6250
|
+
var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
|
|
6251
|
+
var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
6252
|
+
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
6121
6253
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
6122
6254
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
6123
6255
|
async function correlateTenantRun(deps, since) {
|
|
@@ -6157,51 +6289,112 @@ async function watchTenantRun(deps, runId) {
|
|
|
6157
6289
|
return "failure";
|
|
6158
6290
|
}
|
|
6159
6291
|
}
|
|
6160
|
-
function
|
|
6292
|
+
function isNotFoundError(e) {
|
|
6293
|
+
const msg = `${e.message ?? e} ${String(e.stderr ?? "")}`;
|
|
6294
|
+
return /HTTP 404|Not Found|\(404\)/i.test(msg);
|
|
6295
|
+
}
|
|
6296
|
+
function parseStringArray(out, label) {
|
|
6161
6297
|
const parsed = JSON.parse(out);
|
|
6162
|
-
if (!Array.isArray(parsed)) throw new Error(
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6298
|
+
if (!Array.isArray(parsed)) throw new Error(`${label} response was not an array`);
|
|
6299
|
+
return parsed.filter((v) => typeof v === "string");
|
|
6300
|
+
}
|
|
6301
|
+
async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
6302
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
6303
|
+
try {
|
|
6304
|
+
const out = await deps.run("gh", ["api", `repos/${ctx.repo}/branches/${branch}/protection/required_status_checks`, "--jq", TRAIN_PROTECTION_CONTEXTS_JQ]);
|
|
6305
|
+
for (const c of parseStringArray(out, "branch protection required_status_checks")) contexts.add(c);
|
|
6306
|
+
} catch (e) {
|
|
6307
|
+
if (!isNotFoundError(e)) throw new Error(`could not read branch protection for ${ctx.repo}@${branch}: ${e.message ?? e}`);
|
|
6170
6308
|
}
|
|
6171
|
-
|
|
6309
|
+
try {
|
|
6310
|
+
const out = await deps.run("gh", ["api", `repos/${ctx.repo}/rules/branches/${branch}`, "--jq", TRAIN_RULES_CONTEXTS_JQ]);
|
|
6311
|
+
for (const c of parseStringArray(out, "branch rules required_status_checks")) contexts.add(c);
|
|
6312
|
+
} catch (e) {
|
|
6313
|
+
if (!isNotFoundError(e)) throw new Error(`could not read branch rules for ${ctx.repo}@${branch}: ${e.message ?? e}`);
|
|
6314
|
+
}
|
|
6315
|
+
return [...contexts];
|
|
6172
6316
|
}
|
|
6173
|
-
function
|
|
6174
|
-
|
|
6317
|
+
function resolveContextState(context, checkRuns, statuses) {
|
|
6318
|
+
let sawFailure = false;
|
|
6319
|
+
for (const r of checkRuns) {
|
|
6320
|
+
if (r.name !== context) continue;
|
|
6321
|
+
if (r.status === "completed") {
|
|
6322
|
+
if (r.conclusion === "success") return "success";
|
|
6323
|
+
sawFailure = true;
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
for (const s of statuses) {
|
|
6327
|
+
if (s.context !== context) continue;
|
|
6328
|
+
if (s.state === "success") return "success";
|
|
6329
|
+
if (s.state === "failure" || s.state === "error") sawFailure = true;
|
|
6330
|
+
}
|
|
6331
|
+
return sawFailure ? "failed" : "pending";
|
|
6175
6332
|
}
|
|
6176
|
-
async function waitForRequiredTrainChecks(deps, ctx, sha) {
|
|
6333
|
+
async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
6334
|
+
if (required.length === 0) {
|
|
6335
|
+
return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
|
|
6336
|
+
}
|
|
6177
6337
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6178
6338
|
let lastStatus = "not checked";
|
|
6179
6339
|
let lastError;
|
|
6180
6340
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
6181
6341
|
if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
|
|
6182
|
-
let
|
|
6342
|
+
let checkRuns;
|
|
6343
|
+
let statuses;
|
|
6183
6344
|
try {
|
|
6184
|
-
const
|
|
6185
|
-
|
|
6345
|
+
const runsOut = await deps.run("gh", ["api", `repos/${ctx.repo}/commits/${sha}/check-runs`, "--jq", TRAIN_CHECK_RUNS_JQ]);
|
|
6346
|
+
const parsedRuns = JSON.parse(runsOut);
|
|
6347
|
+
if (!Array.isArray(parsedRuns)) throw new Error("check-runs response was not an array");
|
|
6348
|
+
checkRuns = parsedRuns;
|
|
6349
|
+
const statusOut = await deps.run("gh", ["api", `repos/${ctx.repo}/commits/${sha}/status`, "--jq", TRAIN_COMMIT_STATUS_JQ]);
|
|
6350
|
+
const parsedStatuses = JSON.parse(statusOut);
|
|
6351
|
+
if (!Array.isArray(parsedStatuses)) throw new Error("commit status response was not an array");
|
|
6352
|
+
statuses = parsedStatuses;
|
|
6186
6353
|
lastError = void 0;
|
|
6187
6354
|
} catch (e) {
|
|
6188
6355
|
lastError = e.message || String(e);
|
|
6189
6356
|
continue;
|
|
6190
6357
|
}
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
return conclusion != null && conclusion !== "success";
|
|
6195
|
-
});
|
|
6358
|
+
const states = required.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
|
|
6359
|
+
lastStatus = states.map(([c, s]) => `${c}=${s}`).join(", ");
|
|
6360
|
+
const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
|
|
6196
6361
|
if (failed.length > 0) {
|
|
6197
6362
|
throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus})`);
|
|
6198
6363
|
}
|
|
6199
|
-
if (
|
|
6364
|
+
if (states.every(([, s]) => s === "success")) {
|
|
6365
|
+
return `required checks passed: ${required.join(", ")}`;
|
|
6366
|
+
}
|
|
6200
6367
|
}
|
|
6201
6368
|
throw new Error(
|
|
6202
6369
|
`timed out waiting for required train checks on ${sha}: ${lastError ? `last error: ${lastError}` : lastStatus}`
|
|
6203
6370
|
);
|
|
6204
6371
|
}
|
|
6372
|
+
async function ensureTagPushed(deps, tag, sha) {
|
|
6373
|
+
const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
6374
|
+
const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
|
|
6375
|
+
let localSha = "";
|
|
6376
|
+
try {
|
|
6377
|
+
localSha = clean(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
6378
|
+
} catch {
|
|
6379
|
+
}
|
|
6380
|
+
if (remoteSha) {
|
|
6381
|
+
if (remoteSha !== sha) {
|
|
6382
|
+
throw new Error(
|
|
6383
|
+
`tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha} \u2014 refusing to touch a pushed tag. Repair manually: either release the existing tagged commit (reset the local branch to ${remoteSha} and rerun), or mint the next candidate version so a fresh tag is used. Never force-move or delete a pushed tag.`
|
|
6384
|
+
);
|
|
6385
|
+
}
|
|
6386
|
+
if (localSha && localSha !== sha) {
|
|
6387
|
+
throw new Error(`local tag ${tag} points at ${localSha} but origin has it at ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
|
|
6388
|
+
}
|
|
6389
|
+
return `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`;
|
|
6390
|
+
}
|
|
6391
|
+
if (localSha && localSha !== sha) {
|
|
6392
|
+
throw new Error(`local tag ${tag} already points at ${localSha}, not the intended ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
|
|
6393
|
+
}
|
|
6394
|
+
if (!localSha) await deps.run("git", ["tag", tag, sha]);
|
|
6395
|
+
await deps.run("git", ["push", "origin", tag]);
|
|
6396
|
+
return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
|
|
6397
|
+
}
|
|
6205
6398
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
6206
6399
|
if (model === "tenant-container") {
|
|
6207
6400
|
const since = (deps.now ?? Date.now)();
|
|
@@ -6267,13 +6460,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6267
6460
|
await deps.run("git", ["checkout", "rc"]);
|
|
6268
6461
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
6269
6462
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6270
|
-
await deps.run("git", ["tag", tag2]);
|
|
6271
|
-
await deps.run("git", ["push", "origin", tag2]);
|
|
6272
6463
|
const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
6273
|
-
await
|
|
6464
|
+
await ensureTagPushed(deps, tag2, rcSha);
|
|
6465
|
+
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
6466
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
|
|
6274
6467
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
6275
6468
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6276
|
-
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6469
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6277
6470
|
}
|
|
6278
6471
|
await requireBranch(deps, "rc");
|
|
6279
6472
|
ensurePositiveCount(
|
|
@@ -6294,14 +6487,14 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6294
6487
|
if (predicted.length === 0) {
|
|
6295
6488
|
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
6296
6489
|
} else {
|
|
6297
|
-
await
|
|
6490
|
+
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs");
|
|
6298
6491
|
}
|
|
6299
6492
|
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
6300
6493
|
await verifyHubDistributionVersion(deps, deployModel, tag);
|
|
6301
|
-
await deps.run("git", ["tag", tag]);
|
|
6302
|
-
await deps.run("git", ["push", "origin", tag]);
|
|
6303
6494
|
const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
6304
|
-
await
|
|
6495
|
+
await ensureTagPushed(deps, tag, releaseSha);
|
|
6496
|
+
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6497
|
+
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
6305
6498
|
await deps.run("git", ["push", "origin", "main"]);
|
|
6306
6499
|
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
6307
6500
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
|
|
@@ -6318,6 +6511,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6318
6511
|
tag,
|
|
6319
6512
|
deployModel,
|
|
6320
6513
|
promoted: true,
|
|
6514
|
+
checks,
|
|
6321
6515
|
dispatch: d.note,
|
|
6322
6516
|
runId: d.runId,
|
|
6323
6517
|
runUrl: d.runUrl,
|
|
@@ -6388,6 +6582,326 @@ async function runTenantRedeploy(deps, options) {
|
|
|
6388
6582
|
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, deployStatus: d.deployStatus };
|
|
6389
6583
|
}
|
|
6390
6584
|
|
|
6585
|
+
// src/hotfix-apply.ts
|
|
6586
|
+
var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
|
|
6587
|
+
var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
6588
|
+
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
6589
|
+
function clean2(out) {
|
|
6590
|
+
return out.trim();
|
|
6591
|
+
}
|
|
6592
|
+
function sleeper(deps) {
|
|
6593
|
+
return deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6594
|
+
}
|
|
6595
|
+
function normalizeHotfixVersion(input) {
|
|
6596
|
+
const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
|
|
6597
|
+
if (!m) throw new Error(`hotfix version must be vX.Y.Z (PATCH release), got "${input}"`);
|
|
6598
|
+
return { tag: `v${m[1]}`, version: m[1] };
|
|
6599
|
+
}
|
|
6600
|
+
async function deriveHotfixVersion(deps) {
|
|
6601
|
+
const out = await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--merged", "origin/main", "--sort=-v:refname"]);
|
|
6602
|
+
const baseTag = out.split("\n").map((s) => s.trim()).find((t) => /^v\d+\.\d+\.\d+$/.test(t));
|
|
6603
|
+
if (!baseTag) throw new Error("no vX.Y.Z release tag found on origin/main \u2014 cannot derive the hotfix PATCH version");
|
|
6604
|
+
const [major, minor, patch] = baseTag.slice(1).split(".").map(Number);
|
|
6605
|
+
const version = `${major}.${minor}.${patch + 1}`;
|
|
6606
|
+
return { tag: `v${version}`, version, baseTag };
|
|
6607
|
+
}
|
|
6608
|
+
function hotfixBranch(tag) {
|
|
6609
|
+
return `hotfix/${tag}`;
|
|
6610
|
+
}
|
|
6611
|
+
async function findHotfixPr(deps, ctx, tag) {
|
|
6612
|
+
const out = await deps.run("gh", [
|
|
6613
|
+
"pr",
|
|
6614
|
+
"list",
|
|
6615
|
+
"--repo",
|
|
6616
|
+
ctx.repo,
|
|
6617
|
+
"--head",
|
|
6618
|
+
hotfixBranch(tag),
|
|
6619
|
+
"--base",
|
|
6620
|
+
"main",
|
|
6621
|
+
"--state",
|
|
6622
|
+
"all",
|
|
6623
|
+
"--limit",
|
|
6624
|
+
"1",
|
|
6625
|
+
"--json",
|
|
6626
|
+
"number,state,url,mergeCommit"
|
|
6627
|
+
]);
|
|
6628
|
+
const rows = JSON.parse(out || "[]");
|
|
6629
|
+
return rows[0] ?? null;
|
|
6630
|
+
}
|
|
6631
|
+
async function resolveHotfixSource(deps, ctx, from) {
|
|
6632
|
+
const prMatch = /^#?(\d+)$/.exec(from.trim());
|
|
6633
|
+
if (prMatch) {
|
|
6634
|
+
const num = prMatch[1];
|
|
6635
|
+
const out = await deps.run("gh", ["pr", "view", num, "--repo", ctx.repo, "--json", "state,baseRefName,mergeCommit"]);
|
|
6636
|
+
const pr2 = JSON.parse(out);
|
|
6637
|
+
if (pr2.state !== "MERGED") throw new Error(`PR #${num} is ${pr2.state ?? "unknown"}, not MERGED \u2014 hotfix start cherry-picks an already-merged development fix`);
|
|
6638
|
+
if (pr2.baseRefName !== "development") throw new Error(`PR #${num} merged into ${pr2.baseRefName ?? "unknown"}, not development \u2014 name the commit SHA explicitly if this is intentional`);
|
|
6639
|
+
const sha2 = pr2.mergeCommit?.oid;
|
|
6640
|
+
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
6641
|
+
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
6642
|
+
}
|
|
6643
|
+
const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
6644
|
+
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
6645
|
+
return { sha, label: sha.slice(0, 7) };
|
|
6646
|
+
}
|
|
6647
|
+
async function runHotfixStart(deps, options) {
|
|
6648
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
6649
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
6650
|
+
if (status.trim()) throw new Error("working tree must be clean before hotfix start");
|
|
6651
|
+
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
6652
|
+
const { tag, version } = await deriveHotfixVersion(deps);
|
|
6653
|
+
const branch = hotfixBranch(tag);
|
|
6654
|
+
const notes = [];
|
|
6655
|
+
const existingPr = await findHotfixPr(deps, ctx, tag);
|
|
6656
|
+
if (existingPr) {
|
|
6657
|
+
return {
|
|
6658
|
+
...ctx,
|
|
6659
|
+
command: "hotfix-start",
|
|
6660
|
+
tag,
|
|
6661
|
+
version,
|
|
6662
|
+
branch,
|
|
6663
|
+
source: options.from,
|
|
6664
|
+
prUrl: existingPr.url,
|
|
6665
|
+
reused: true,
|
|
6666
|
+
notes: [`hotfix PR for ${tag} already exists (#${existingPr.number}, ${existingPr.state}) \u2014 reused; next: merge it, then mmi-cli hotfix release ${tag}`]
|
|
6667
|
+
};
|
|
6668
|
+
}
|
|
6669
|
+
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
6670
|
+
const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
6671
|
+
if (remoteBranch) {
|
|
6672
|
+
await deps.run("git", ["checkout", branch]);
|
|
6673
|
+
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
6674
|
+
notes.push(`branch ${branch} already on origin \u2014 reused (cherry-pick/bump assumed present; PR step resumes)`);
|
|
6675
|
+
} else {
|
|
6676
|
+
await deps.run("git", ["checkout", "-B", branch, "origin/main"]);
|
|
6677
|
+
try {
|
|
6678
|
+
await deps.run("git", ["cherry-pick", "-x", sha]);
|
|
6679
|
+
} catch (e) {
|
|
6680
|
+
await deps.run("git", ["cherry-pick", "--abort"]);
|
|
6681
|
+
throw new Error(`cherry-pick of ${label} onto ${branch} conflicted \u2014 aborted; resolve by hand on a manual hotfix branch, keeping the -x trailer (${e.message ?? e})`);
|
|
6682
|
+
}
|
|
6683
|
+
notes.push(`cherry-picked ${label} onto ${branch} (from origin/main, -x trailer recorded)`);
|
|
6684
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
|
|
6685
|
+
const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
6686
|
+
await deps.run("git", ["add", "--", ...changedFiles]);
|
|
6687
|
+
const staged = await deps.run("git", ["diff", "--cached", "--name-only"]);
|
|
6688
|
+
if (staged.trim()) {
|
|
6689
|
+
await deps.run("git", ["commit", "-m", `hotfix ${tag}: lock plugin set + @mutmutco/cli distribution to ${version}`]);
|
|
6690
|
+
notes.push(`distribution prepared + committed for ${version} (${changedFiles.length} locked paths)`);
|
|
6691
|
+
} else {
|
|
6692
|
+
notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
|
|
6693
|
+
}
|
|
6694
|
+
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
6695
|
+
}
|
|
6696
|
+
const prUrl = clean2(await deps.run("gh", [
|
|
6697
|
+
"pr",
|
|
6698
|
+
"create",
|
|
6699
|
+
"--repo",
|
|
6700
|
+
ctx.repo,
|
|
6701
|
+
"--base",
|
|
6702
|
+
"main",
|
|
6703
|
+
"--head",
|
|
6704
|
+
branch,
|
|
6705
|
+
"--title",
|
|
6706
|
+
`[hotfix] ${tag}`,
|
|
6707
|
+
"--body",
|
|
6708
|
+
`Hotfix ${tag}: cherry-pick of ${label} onto origin/main with the locked distribution bump.
|
|
6709
|
+
|
|
6710
|
+
Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
|
|
6711
|
+
]));
|
|
6712
|
+
notes.push(`opened hotfix PR ${prUrl} \u2014 merge it (human-initiated), then: mmi-cli hotfix release ${tag}`);
|
|
6713
|
+
return { ...ctx, command: "hotfix-start", tag, version, branch, source: label, prUrl, reused: Boolean(remoteBranch), notes };
|
|
6714
|
+
}
|
|
6715
|
+
async function watchReleaseRun(deps, ctx, workflow, sha) {
|
|
6716
|
+
const sleep = sleeper(deps);
|
|
6717
|
+
for (let attempt = 0; attempt < HOTFIX_RUN_FIND_ATTEMPTS; attempt++) {
|
|
6718
|
+
if (attempt > 0) await sleep(HOTFIX_RUN_FIND_DELAY_MS);
|
|
6719
|
+
let rows;
|
|
6720
|
+
try {
|
|
6721
|
+
const out = await deps.run("gh", [
|
|
6722
|
+
"run",
|
|
6723
|
+
"list",
|
|
6724
|
+
"--repo",
|
|
6725
|
+
ctx.repo,
|
|
6726
|
+
"--workflow",
|
|
6727
|
+
workflow,
|
|
6728
|
+
"--event",
|
|
6729
|
+
"release",
|
|
6730
|
+
"--limit",
|
|
6731
|
+
"10",
|
|
6732
|
+
"--json",
|
|
6733
|
+
"databaseId,url,headSha,status,conclusion"
|
|
6734
|
+
]);
|
|
6735
|
+
rows = JSON.parse(out);
|
|
6736
|
+
} catch {
|
|
6737
|
+
continue;
|
|
6738
|
+
}
|
|
6739
|
+
const run = rows.find((r) => r.headSha === sha && typeof r.databaseId === "number");
|
|
6740
|
+
if (!run) continue;
|
|
6741
|
+
if (run.status === "completed") return { workflow, url: run.url, conclusion: run.conclusion ?? "unknown" };
|
|
6742
|
+
try {
|
|
6743
|
+
await deps.run("gh", ["run", "watch", String(run.databaseId), "--repo", ctx.repo, "--exit-status"]);
|
|
6744
|
+
return { workflow, url: run.url, conclusion: "success" };
|
|
6745
|
+
} catch {
|
|
6746
|
+
return { workflow, url: run.url, conclusion: "failure" };
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
return { workflow, conclusion: "not-found" };
|
|
6750
|
+
}
|
|
6751
|
+
async function runHotfixRelease(deps, versionInput) {
|
|
6752
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
6753
|
+
const { tag, version } = normalizeHotfixVersion(versionInput);
|
|
6754
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
6755
|
+
if (status.trim()) throw new Error("working tree must be clean before hotfix release");
|
|
6756
|
+
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
6757
|
+
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
6758
|
+
if (!pr2) throw new Error(`no hotfix PR found for ${tag} (head ${hotfixBranch(tag)}, base main) \u2014 run mmi-cli hotfix start first`);
|
|
6759
|
+
if (pr2.state !== "MERGED") throw new Error(`hotfix PR #${pr2.number} for ${tag} is ${pr2.state}, not MERGED \u2014 merge it (human-initiated), then rerun`);
|
|
6760
|
+
const mergedSha = pr2.mergeCommit?.oid ?? "";
|
|
6761
|
+
if (!mergedSha) throw new Error(`hotfix PR #${pr2.number} reports no merge commit \u2014 cannot pin the release SHA`);
|
|
6762
|
+
await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
|
|
6763
|
+
throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
|
|
6764
|
+
});
|
|
6765
|
+
const required = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6766
|
+
const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required);
|
|
6767
|
+
const tagNote = await ensureTagPushed(deps, tag, mergedSha);
|
|
6768
|
+
let releaseNote;
|
|
6769
|
+
let releaseExists = false;
|
|
6770
|
+
try {
|
|
6771
|
+
await deps.run("gh", ["release", "view", tag, "--repo", ctx.repo, "--json", "tagName"]);
|
|
6772
|
+
releaseExists = true;
|
|
6773
|
+
} catch {
|
|
6774
|
+
}
|
|
6775
|
+
if (releaseExists) {
|
|
6776
|
+
releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
|
|
6777
|
+
} else {
|
|
6778
|
+
const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
|
|
6779
|
+
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
|
|
6780
|
+
releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
|
|
6781
|
+
}
|
|
6782
|
+
const runs = [];
|
|
6783
|
+
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
6784
|
+
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
6785
|
+
}
|
|
6786
|
+
const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
6787
|
+
let verifyNote;
|
|
6788
|
+
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
6789
|
+
try {
|
|
6790
|
+
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
6791
|
+
const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
|
|
6792
|
+
await deps.run("node", verifyArgs);
|
|
6793
|
+
verifyNote = `distribution verified at ${tag}${publishSucceeded ? " (npm included)" : " (npm view skipped \u2014 publish run not confirmed)"}`;
|
|
6794
|
+
} finally {
|
|
6795
|
+
if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
|
|
6796
|
+
}
|
|
6797
|
+
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote };
|
|
6798
|
+
}
|
|
6799
|
+
function versionAtLeast(actual, wanted) {
|
|
6800
|
+
const pa = actual.split(".").map(Number);
|
|
6801
|
+
const pw = wanted.split(".").map(Number);
|
|
6802
|
+
if (pa.length < 3 || pa.some(Number.isNaN) || pw.length < 3 || pw.some(Number.isNaN)) return false;
|
|
6803
|
+
for (let i = 0; i < 3; i += 1) {
|
|
6804
|
+
if (pa[i] !== pw[i]) return pa[i] > pw[i];
|
|
6805
|
+
}
|
|
6806
|
+
return true;
|
|
6807
|
+
}
|
|
6808
|
+
function deriveHotfixState(f) {
|
|
6809
|
+
if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
|
|
6810
|
+
return { state: "not-started", next: `mmi-cli hotfix start --from <pr#|sha> (would mint ${f.tag})` };
|
|
6811
|
+
}
|
|
6812
|
+
if (!f.pr) {
|
|
6813
|
+
return { state: "branch-pushed (no PR)", next: `mmi-cli hotfix start --from <pr#|sha> (resumes at the PR step for ${f.tag})` };
|
|
6814
|
+
}
|
|
6815
|
+
if (f.pr.state === "OPEN") {
|
|
6816
|
+
return { state: "pr-open", next: `merge hotfix PR #${f.pr.number} (human-initiated), then mmi-cli hotfix release ${f.tag}` };
|
|
6817
|
+
}
|
|
6818
|
+
if (f.pr.state !== "MERGED") {
|
|
6819
|
+
return { state: `pr-${(f.pr.state ?? "unknown").toLowerCase()}`, next: `hotfix PR #${f.pr.number} is ${f.pr.state} \u2014 restart with mmi-cli hotfix start if the fix is still needed` };
|
|
6820
|
+
}
|
|
6821
|
+
if (!f.tagPushed || !f.releaseExists) {
|
|
6822
|
+
return { state: "pr-merged (not released)", next: `mmi-cli hotfix release ${f.tag}` };
|
|
6823
|
+
}
|
|
6824
|
+
if (!f.devDistribution.aligned) {
|
|
6825
|
+
return {
|
|
6826
|
+
state: `UNFINISHED \u2014 released but development distribution manifests behind (dev ${f.devDistribution.version} < ${f.version})`,
|
|
6827
|
+
next: `forward-bump development (never a back-merge): branch from origin/development, node scripts/release-distribution.mjs prepare ${f.version}, land by PR (/hotfix Step 5)`
|
|
6828
|
+
};
|
|
6829
|
+
}
|
|
6830
|
+
return { state: "complete", next: "nothing \u2014 pipeline complete (rc absorbs the fix at the next /rcand; /release guards coverage)" };
|
|
6831
|
+
}
|
|
6832
|
+
async function runHotfixStatus(deps, versionInput) {
|
|
6833
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
6834
|
+
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
6835
|
+
let tag;
|
|
6836
|
+
let version;
|
|
6837
|
+
if (versionInput) {
|
|
6838
|
+
({ tag, version } = normalizeHotfixVersion(versionInput));
|
|
6839
|
+
} else {
|
|
6840
|
+
const latest = await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--merged", "origin/main", "--sort=-v:refname"]).then((out) => out.split("\n").map((s) => s.trim()).find((t) => /^v\d+\.\d+\.\d+$/.test(t)));
|
|
6841
|
+
if (latest) {
|
|
6842
|
+
const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
|
|
6843
|
+
const latestDerived = deriveHotfixState(latestFacts);
|
|
6844
|
+
if (latestDerived.state !== "complete") {
|
|
6845
|
+
return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
({ tag, version } = await deriveHotfixVersion(deps));
|
|
6849
|
+
}
|
|
6850
|
+
const facts = await gatherHotfixFacts(deps, ctx, tag, version);
|
|
6851
|
+
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
6852
|
+
}
|
|
6853
|
+
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
6854
|
+
const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
6855
|
+
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
6856
|
+
const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
6857
|
+
const tagPushed = Boolean(remoteTag);
|
|
6858
|
+
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
6859
|
+
let releaseExists = false;
|
|
6860
|
+
try {
|
|
6861
|
+
await deps.run("gh", ["release", "view", tag, "--repo", ctx.repo, "--json", "tagName"]);
|
|
6862
|
+
releaseExists = true;
|
|
6863
|
+
} catch {
|
|
6864
|
+
}
|
|
6865
|
+
const runs = [];
|
|
6866
|
+
if (releaseExists && tagSha) {
|
|
6867
|
+
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
6868
|
+
try {
|
|
6869
|
+
const out = await deps.run("gh", [
|
|
6870
|
+
"run",
|
|
6871
|
+
"list",
|
|
6872
|
+
"--repo",
|
|
6873
|
+
ctx.repo,
|
|
6874
|
+
"--workflow",
|
|
6875
|
+
workflow,
|
|
6876
|
+
"--event",
|
|
6877
|
+
"release",
|
|
6878
|
+
"--limit",
|
|
6879
|
+
"10",
|
|
6880
|
+
"--json",
|
|
6881
|
+
"databaseId,url,headSha,status,conclusion"
|
|
6882
|
+
]);
|
|
6883
|
+
const run = JSON.parse(out).find((r) => r.headSha === tagSha);
|
|
6884
|
+
runs.push(run ? { workflow, url: run.url, conclusion: run.status === "completed" ? run.conclusion ?? "unknown" : run.status ?? "unknown" } : { workflow, conclusion: "not-found" });
|
|
6885
|
+
} catch {
|
|
6886
|
+
runs.push({ workflow, conclusion: "unknown" });
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
|
|
6891
|
+
const devVersion = await deps.run("git", ["show", "origin/development:cli/package.json"]).then(
|
|
6892
|
+
(out) => {
|
|
6893
|
+
try {
|
|
6894
|
+
return JSON.parse(out).version ?? "unknown";
|
|
6895
|
+
} catch {
|
|
6896
|
+
return "unknown";
|
|
6897
|
+
}
|
|
6898
|
+
},
|
|
6899
|
+
() => "unknown"
|
|
6900
|
+
);
|
|
6901
|
+
const devDistribution = { version: devVersion, aligned: versionAtLeast(devVersion, version) };
|
|
6902
|
+
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion, devDistribution };
|
|
6903
|
+
}
|
|
6904
|
+
|
|
6391
6905
|
// src/port-registry.ts
|
|
6392
6906
|
var import_node_fs4 = require("node:fs");
|
|
6393
6907
|
|
|
@@ -7738,8 +8252,8 @@ function resolveKbSource(rawBase) {
|
|
|
7738
8252
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
7739
8253
|
}
|
|
7740
8254
|
function buildKbGetArgs(src, path2) {
|
|
7741
|
-
const
|
|
7742
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
8255
|
+
const clean3 = path2.replace(/^\/+/, "");
|
|
8256
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
7743
8257
|
}
|
|
7744
8258
|
function buildKbTreeArgs(src) {
|
|
7745
8259
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -9827,17 +10341,40 @@ async function runBoardRead(o) {
|
|
|
9827
10341
|
}
|
|
9828
10342
|
var board = program2.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
9829
10343
|
board.command("read", { isDefault: true }).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("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--allow-partial", "return partial board results when later page/detail reads fail").action((o) => runBoardRead(o));
|
|
9830
|
-
board.command("claim <
|
|
10344
|
+
board.command("claim <issues...>").description("assign Todo issues and move their Project v2 Status to In Progress (one or more refs)").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("--allow-partial", "return success JSON if assignment succeeds but the status move fails").action(async (issueRefs, o) => {
|
|
10345
|
+
if (issueRefs.length === 1) {
|
|
10346
|
+
const issueRef = issueRefs[0];
|
|
10347
|
+
try {
|
|
10348
|
+
const result = await claimBoardIssue({
|
|
10349
|
+
config: await loadConfigForBoardSelector(issueRef, o.repo),
|
|
10350
|
+
selector: issueRef,
|
|
10351
|
+
repo: o.repo,
|
|
10352
|
+
assignee: o.for,
|
|
10353
|
+
allowPartial: o.allowPartial
|
|
10354
|
+
});
|
|
10355
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
10356
|
+
console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : `Claimed ${result.item.ref} - In Progress`);
|
|
10357
|
+
} catch (e) {
|
|
10358
|
+
fail(`board claim failed: ${e.message}`);
|
|
10359
|
+
}
|
|
10360
|
+
return;
|
|
10361
|
+
}
|
|
9831
10362
|
try {
|
|
9832
|
-
const
|
|
9833
|
-
config: await loadConfigForBoardSelector(
|
|
9834
|
-
|
|
10363
|
+
const bulk = await claimBoardIssues({
|
|
10364
|
+
config: await loadConfigForBoardSelector(issueRefs[0], o.repo),
|
|
10365
|
+
selectors: issueRefs,
|
|
9835
10366
|
repo: o.repo,
|
|
9836
10367
|
assignee: o.for,
|
|
9837
10368
|
allowPartial: o.allowPartial
|
|
9838
10369
|
});
|
|
9839
|
-
if (o.json)
|
|
9840
|
-
|
|
10370
|
+
if (o.json) {
|
|
10371
|
+
console.log(JSON.stringify(bulk.results));
|
|
10372
|
+
} else {
|
|
10373
|
+
for (const result of bulk.results) {
|
|
10374
|
+
console.log(result.claimed ? result.partial ? `Partially claimed ${result.ref}: ${result.warning}` : `Claimed ${result.ref} - In Progress` : `Skipped ${result.ref}: ${result.reason}`);
|
|
10375
|
+
}
|
|
10376
|
+
}
|
|
10377
|
+
if (bulk.failed > 0) process.exitCode = 1;
|
|
9841
10378
|
} catch (e) {
|
|
9842
10379
|
fail(`board claim failed: ${e.message}`);
|
|
9843
10380
|
}
|
|
@@ -10078,10 +10615,11 @@ program2.command("stage-live").description("explain that remote rc/live environm
|
|
|
10078
10615
|
});
|
|
10079
10616
|
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
10080
10617
|
var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
|
|
10618
|
+
var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
10081
10619
|
function trainApplyDeps() {
|
|
10082
10620
|
return {
|
|
10083
10621
|
run: async (file, args) => {
|
|
10084
|
-
const timeout = file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
10622
|
+
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
10085
10623
|
return (await execFileP4(file, args, { timeout })).stdout;
|
|
10086
10624
|
},
|
|
10087
10625
|
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
@@ -10106,15 +10644,14 @@ function renderTrainApply(commandName, r) {
|
|
|
10106
10644
|
function renderTenantRedeploy(r) {
|
|
10107
10645
|
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10108
10646
|
}
|
|
10109
|
-
for (const commandName of ["rcand", "release"
|
|
10110
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply",
|
|
10647
|
+
for (const commandName of ["rcand", "release"]) {
|
|
10648
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply", "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
10111
10649
|
try {
|
|
10112
10650
|
await requireFreshTrainCli(commandName);
|
|
10113
10651
|
} catch (e) {
|
|
10114
10652
|
return fail(`${commandName}: ${e.message}`);
|
|
10115
10653
|
}
|
|
10116
10654
|
if (o.apply) {
|
|
10117
|
-
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
10118
10655
|
try {
|
|
10119
10656
|
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch });
|
|
10120
10657
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
@@ -10126,6 +10663,51 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
|
10126
10663
|
console.log(o.json ? JSON.stringify({ command: commandName, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps));
|
|
10127
10664
|
});
|
|
10128
10665
|
}
|
|
10666
|
+
function renderHotfixStart(r) {
|
|
10667
|
+
return [`mmi-cli hotfix start: ${r.tag} (${r.branch}, from ${r.source})${r.reused ? " [reused]" : ""}`, ...r.notes.map((n) => ` - ${n}`)].join("\n");
|
|
10668
|
+
}
|
|
10669
|
+
function renderHotfixRelease(r) {
|
|
10670
|
+
return [
|
|
10671
|
+
`mmi-cli hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo}`,
|
|
10672
|
+
` - checks: ${r.checks}`,
|
|
10673
|
+
` - ${r.tagNote}`,
|
|
10674
|
+
` - ${r.releaseNote}`,
|
|
10675
|
+
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
10676
|
+
` - ${r.verifyNote}`,
|
|
10677
|
+
` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; align its distribution manifests by PR if status says behind)`
|
|
10678
|
+
].join("\n");
|
|
10679
|
+
}
|
|
10680
|
+
function renderHotfixStatus(r) {
|
|
10681
|
+
return [
|
|
10682
|
+
`mmi-cli hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
|
|
10683
|
+
` - branch: ${r.branchExists ? "pushed" : "absent"} \xB7 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \xB7 tag: ${r.tagPushed ? "pushed" : "absent"} \xB7 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
10684
|
+
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
10685
|
+
` - npm @mutmutco/cli: ${r.npmVersion} \xB7 development manifests: ${r.devDistribution.version}${r.devDistribution.aligned ? " (aligned)" : " (behind)"}`,
|
|
10686
|
+
` - next: ${r.next}`
|
|
10687
|
+
].join("\n");
|
|
10688
|
+
}
|
|
10689
|
+
async function runHotfixSub(sub, body, json, render) {
|
|
10690
|
+
try {
|
|
10691
|
+
await requireFreshTrainCli("hotfix");
|
|
10692
|
+
const result = await body();
|
|
10693
|
+
printLine(json ? JSON.stringify(result, null, 2) : render(result));
|
|
10694
|
+
} catch (e) {
|
|
10695
|
+
fail(`hotfix ${sub}: ${e.message}`);
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestrator: start \u2192 release, with status (bare command prints the dry-run plan; no back-merge \u2014 #839)").option("--json", "machine-readable output").option("--apply", "not a verb; use the hotfix subcommands (start/release/status)").action(async (o) => {
|
|
10699
|
+
try {
|
|
10700
|
+
await requireFreshTrainCli("hotfix");
|
|
10701
|
+
} catch (e) {
|
|
10702
|
+
return fail(`hotfix: ${e.message}`);
|
|
10703
|
+
}
|
|
10704
|
+
if (o.apply) return fail("hotfix: use the stepwise subcommands \u2014 mmi-cli hotfix start --from <pr#|sha> \xB7 release <vX.Y.Z> \xB7 status [vX.Y.Z]");
|
|
10705
|
+
const steps = trainPlan("hotfix");
|
|
10706
|
+
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
|
|
10707
|
+
});
|
|
10708
|
+
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
10709
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version), o.json, renderHotfixRelease));
|
|
10710
|
+
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
10129
10711
|
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
10130
10712
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
10131
10713
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
@@ -10341,6 +10923,16 @@ function readInstalledPlugins() {
|
|
|
10341
10923
|
return null;
|
|
10342
10924
|
}
|
|
10343
10925
|
}
|
|
10926
|
+
function installedPluginSources() {
|
|
10927
|
+
return ["claude", "codex"].map((surface) => {
|
|
10928
|
+
const recordPath = (0, import_node_path7.join)((0, import_node_os3.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
10929
|
+
try {
|
|
10930
|
+
return { surface, installed: JSON.parse((0, import_node_fs6.readFileSync)(recordPath, "utf8")), recordPath };
|
|
10931
|
+
} catch {
|
|
10932
|
+
return { surface, installed: null, recordPath };
|
|
10933
|
+
}
|
|
10934
|
+
});
|
|
10935
|
+
}
|
|
10344
10936
|
function readClaudeSettings() {
|
|
10345
10937
|
try {
|
|
10346
10938
|
return JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
@@ -10387,6 +10979,38 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
10387
10979
|
return false;
|
|
10388
10980
|
}
|
|
10389
10981
|
}
|
|
10982
|
+
function cursorPluginCacheRoot() {
|
|
10983
|
+
return (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
10984
|
+
}
|
|
10985
|
+
function cursorPluginCachePinSnapshots() {
|
|
10986
|
+
const root = cursorPluginCacheRoot();
|
|
10987
|
+
try {
|
|
10988
|
+
return (0, import_node_fs6.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
10989
|
+
const path2 = (0, import_node_path7.join)(root, entry.name);
|
|
10990
|
+
const pluginJson = (0, import_node_path7.join)(path2, ".cursor-plugin", "plugin.json");
|
|
10991
|
+
const hooksJson = (0, import_node_path7.join)(path2, "hooks", "hooks.json");
|
|
10992
|
+
let isEmpty = true;
|
|
10993
|
+
try {
|
|
10994
|
+
isEmpty = (0, import_node_fs6.readdirSync)(path2).length === 0;
|
|
10995
|
+
} catch {
|
|
10996
|
+
isEmpty = true;
|
|
10997
|
+
}
|
|
10998
|
+
return {
|
|
10999
|
+
name: entry.name,
|
|
11000
|
+
path: path2,
|
|
11001
|
+
hasPluginJson: (0, import_node_fs6.existsSync)(pluginJson),
|
|
11002
|
+
hasHooksJson: (0, import_node_fs6.existsSync)(hooksJson),
|
|
11003
|
+
isEmpty
|
|
11004
|
+
};
|
|
11005
|
+
});
|
|
11006
|
+
} catch {
|
|
11007
|
+
return [];
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
function hubCheckoutForCursorSeed() {
|
|
11011
|
+
const manifest = (0, import_node_path7.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
11012
|
+
return (0, import_node_fs6.existsSync)(manifest) ? process.cwd() : void 0;
|
|
11013
|
+
}
|
|
10390
11014
|
function mmiPluginCacheRootSnapshots() {
|
|
10391
11015
|
const roots = [
|
|
10392
11016
|
{ surface: "claude", root: (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
@@ -10449,6 +11073,8 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10449
11073
|
else io.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
|
|
10450
11074
|
return;
|
|
10451
11075
|
}
|
|
11076
|
+
const repairLocal = !opts.json || Boolean(opts.apply);
|
|
11077
|
+
const repairFull = !opts.json && !opts.banner || Boolean(opts.apply);
|
|
10452
11078
|
const checks = [];
|
|
10453
11079
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
10454
11080
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -10483,13 +11109,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10483
11109
|
repoVersion: readRepoVersion(),
|
|
10484
11110
|
releasedVersion
|
|
10485
11111
|
});
|
|
10486
|
-
if (
|
|
11112
|
+
if (repairFull) versionReport = await applyVersionAutoUpdate(versionReport, (m) => io.err(m));
|
|
10487
11113
|
if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
|
|
10488
11114
|
checks.push(versionReport);
|
|
10489
11115
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
|
|
10490
11116
|
checks.push(buildAwsCrossAccountCheck({ callerArn }));
|
|
10491
11117
|
let cloneOk = cloneProbe;
|
|
10492
|
-
if (!cloneOk &&
|
|
11118
|
+
if (!cloneOk && repairFull) {
|
|
10493
11119
|
try {
|
|
10494
11120
|
await execFileP4("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
10495
11121
|
cloneOk = true;
|
|
@@ -10507,7 +11133,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10507
11133
|
mirrorFrom: existingMirrorRecord(installed),
|
|
10508
11134
|
surface
|
|
10509
11135
|
});
|
|
10510
|
-
if (!pluginCheck.ok && pluginCheck.recordToInsert &&
|
|
11136
|
+
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
10511
11137
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
10512
11138
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
10513
11139
|
io.err(` \u21BB repaired: registered mmi@mmi project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
@@ -10515,7 +11141,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10515
11141
|
}
|
|
10516
11142
|
checks.push(pluginCheck);
|
|
10517
11143
|
let gitignoreCheck = buildGitignoreManagedBlockCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), content: readGitignore() });
|
|
10518
|
-
if (!gitignoreCheck.ok && gitignoreCheck.contentToWrite &&
|
|
11144
|
+
if (!gitignoreCheck.ok && gitignoreCheck.contentToWrite && repairFull) {
|
|
10519
11145
|
if (writeGitignore(gitignoreCheck.contentToWrite)) {
|
|
10520
11146
|
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
10521
11147
|
const drift = gitignoreCheck.seeded ? "inserted the org-managed block" : [
|
|
@@ -10528,7 +11154,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10528
11154
|
}
|
|
10529
11155
|
checks.push(gitignoreCheck);
|
|
10530
11156
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), installed, surface });
|
|
10531
|
-
if (!driftCheck.ok && driftCheck.recordsToWrite &&
|
|
11157
|
+
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
10532
11158
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
10533
11159
|
driftCheck = { ...driftCheck, ok: true };
|
|
10534
11160
|
io.err(` \u21BB repaired: collapsed mmi@mmi to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
|
|
@@ -10537,15 +11163,16 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10537
11163
|
checks.push(driftCheck);
|
|
10538
11164
|
let installedVersionCheck = buildInstalledPluginVersionCheck({
|
|
10539
11165
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
10540
|
-
|
|
11166
|
+
sources: installedPluginSources(),
|
|
10541
11167
|
releasedVersion,
|
|
10542
11168
|
surface
|
|
10543
11169
|
});
|
|
10544
|
-
if (!installedVersionCheck.ok &&
|
|
10545
|
-
|
|
11170
|
+
if (!installedVersionCheck.ok && repairFull) {
|
|
11171
|
+
const claudeStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "claude") ?? false;
|
|
11172
|
+
if (claudeStale && await applyClaudePluginHeal(surface, (m) => io.err(m))) {
|
|
10546
11173
|
const healed = buildInstalledPluginVersionCheck({
|
|
10547
11174
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
10548
|
-
|
|
11175
|
+
sources: installedPluginSources(),
|
|
10549
11176
|
releasedVersion,
|
|
10550
11177
|
surface
|
|
10551
11178
|
});
|
|
@@ -10563,21 +11190,35 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10563
11190
|
releasedVersion,
|
|
10564
11191
|
installedVersions: installedPluginVersions(installed)
|
|
10565
11192
|
});
|
|
10566
|
-
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan &&
|
|
11193
|
+
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
|
|
10567
11194
|
const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
10568
11195
|
if (moved > 0) {
|
|
10569
11196
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
10570
11197
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
10571
11198
|
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014 ${reloadHint} to load MMI commands`);
|
|
10572
11199
|
}
|
|
10573
|
-
cacheCleanupCheck =
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
11200
|
+
cacheCleanupCheck = {
|
|
11201
|
+
...buildMmiPluginCacheCleanupCheck({
|
|
11202
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11203
|
+
roots: mmiPluginCacheRootSnapshots(),
|
|
11204
|
+
activeVersion: resolveVersion(),
|
|
11205
|
+
releasedVersion
|
|
11206
|
+
}),
|
|
11207
|
+
...moved > 0 ? { cleanedCount: moved } : {}
|
|
11208
|
+
};
|
|
10579
11209
|
}
|
|
10580
11210
|
checks.push(cacheCleanupCheck);
|
|
11211
|
+
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
11212
|
+
checks.push(
|
|
11213
|
+
buildCursorPluginInstallCheck({
|
|
11214
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
11215
|
+
surface,
|
|
11216
|
+
cacheRoot: cursorCacheRoot,
|
|
11217
|
+
cacheRootExists: (0, import_node_fs6.existsSync)(cursorCacheRoot),
|
|
11218
|
+
pins: cursorPluginCachePinSnapshots() ?? [],
|
|
11219
|
+
hubCheckout: hubCheckoutForCursorSeed()
|
|
11220
|
+
})
|
|
11221
|
+
);
|
|
10581
11222
|
const gaps = checks.filter((c) => !c.ok);
|
|
10582
11223
|
const resources = doctorResourcesForGaps(gaps);
|
|
10583
11224
|
if (opts.json) {
|
|
@@ -10594,7 +11235,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
10594
11235
|
io.log(gaps.length ? `
|
|
10595
11236
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
10596
11237
|
}
|
|
10597
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action((opts) => runDoctor(opts));
|
|
11238
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").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)").action((opts) => runDoctor(opts));
|
|
10598
11239
|
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, doctor) in one process; docs sync runs detached").action(async () => {
|
|
10599
11240
|
try {
|
|
10600
11241
|
const hook = parseHookInput(await readStdin());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|