@mutmutco/cli 2.30.0 → 2.31.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 CHANGED
@@ -23,18 +23,28 @@ var init_compat = __esm({
23
23
  });
24
24
 
25
25
  // src/client-version.ts
26
- function resolveClientVersion() {
26
+ function resolveClientVersionManifestCandidates(distDir = __dirname) {
27
+ return [
28
+ (0, import_node_path.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
29
+ (0, import_node_path.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
30
+ (0, import_node_path.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
31
+ (0, import_node_path.join)(distDir, "..", "package.json")
32
+ ];
33
+ }
34
+ function readVersionFromManifest(path) {
27
35
  try {
28
- const manifest = (0, import_node_path.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
29
- return JSON.parse((0, import_node_fs.readFileSync)(manifest, "utf8")).version || "0.0.0";
36
+ const version = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8")).version;
37
+ return typeof version === "string" && version.trim() ? version.trim() : null;
30
38
  } catch {
31
- try {
32
- const pkg = (0, import_node_path.join)(__dirname, "..", "package.json");
33
- return JSON.parse((0, import_node_fs.readFileSync)(pkg, "utf8")).version || "0.0.0";
34
- } catch {
35
- return "0.0.0";
36
- }
39
+ return null;
40
+ }
41
+ }
42
+ function resolveClientVersion() {
43
+ for (const manifest of resolveClientVersionManifestCandidates()) {
44
+ const version = readVersionFromManifest(manifest);
45
+ if (version) return version;
37
46
  }
47
+ return "0.0.0";
38
48
  }
39
49
  var import_node_fs, import_node_path;
40
50
  var init_client_version = __esm({
package/dist/main.cjs CHANGED
@@ -3473,19 +3473,29 @@ function versionAtLeast(v, min) {
3473
3473
  }
3474
3474
 
3475
3475
  // src/client-version.ts
3476
- function resolveClientVersion() {
3476
+ function resolveClientVersionManifestCandidates(distDir = __dirname) {
3477
+ return [
3478
+ (0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
3479
+ (0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
3480
+ (0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
3481
+ (0, import_node_path2.join)(distDir, "..", "package.json")
3482
+ ];
3483
+ }
3484
+ function readVersionFromManifest(path2) {
3477
3485
  try {
3478
- const manifest = (0, import_node_path2.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
3479
- return JSON.parse((0, import_node_fs2.readFileSync)(manifest, "utf8")).version || "0.0.0";
3486
+ const version = JSON.parse((0, import_node_fs2.readFileSync)(path2, "utf8")).version;
3487
+ return typeof version === "string" && version.trim() ? version.trim() : null;
3480
3488
  } catch {
3481
- try {
3482
- const pkg = (0, import_node_path2.join)(__dirname, "..", "package.json");
3483
- return JSON.parse((0, import_node_fs2.readFileSync)(pkg, "utf8")).version || "0.0.0";
3484
- } catch {
3485
- return "0.0.0";
3486
- }
3489
+ return null;
3487
3490
  }
3488
3491
  }
3492
+ function resolveClientVersion() {
3493
+ for (const manifest of resolveClientVersionManifestCandidates()) {
3494
+ const version = readVersionFromManifest(manifest);
3495
+ if (version) return version;
3496
+ }
3497
+ return "0.0.0";
3498
+ }
3489
3499
  function clientVersionHeaders() {
3490
3500
  return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
3491
3501
  }
@@ -7268,10 +7278,59 @@ function buildPanelPlan(input) {
7268
7278
  }
7269
7279
 
7270
7280
  // src/gc.ts
7281
+ var DEFERRED_SWEEP_COMMAND = "mmi-cli gc --apply";
7282
+ var DEFERRED_NOTE = "Worktree cleanup deferred \u2014 close this folder in your editor (or run cleanup from a shell outside it), then rerun mmi-cli gc --apply.";
7271
7283
  var WORKTREE_LOCK_RE = /EPERM|EBUSY|EACCES|ENOTEMPTY|permission denied|access is denied|used by another process|resource busy|directory not empty/i;
7272
7284
  function isWorktreeLockError(error) {
7273
7285
  return WORKTREE_LOCK_RE.test(error instanceof Error ? error.message : String(error));
7274
7286
  }
7287
+ function deferredWorktreesRegistryPath(gitDir) {
7288
+ const base2 = gitDir.replace(/\\/g, "/").replace(/\/+$/, "");
7289
+ return `${base2}/mmi-deferred-worktrees.json`;
7290
+ }
7291
+ function parseDeferredWorktreesFile(text) {
7292
+ const parsed = JSON.parse(text);
7293
+ if (!parsed || !Array.isArray(parsed.entries)) return [];
7294
+ return parsed.entries.filter((e) => Boolean(e) && typeof e === "object" && typeof e.path === "string" && typeof e.branch === "string" && e.reason === "lock-held").map((e) => ({ ...e, registeredAt: e.registeredAt || (/* @__PURE__ */ new Date(0)).toISOString() }));
7295
+ }
7296
+ function serializeDeferredWorktrees(entries) {
7297
+ return `${JSON.stringify({ entries }, null, 2)}
7298
+ `;
7299
+ }
7300
+ function deferredPathKey(path2) {
7301
+ return normPath(path2);
7302
+ }
7303
+ function isPersistentWorktreeLockFailure(outcome) {
7304
+ return outcome.status === "failed" && isWorktreeLockError(outcome.error);
7305
+ }
7306
+ async function registerDeferredWorktree(store, entry) {
7307
+ const existing = await store.read();
7308
+ const key = deferredPathKey(entry.path);
7309
+ const already = existing.some((e) => deferredPathKey(e.path) === key);
7310
+ if (already) return { entries: existing, newlyRegistered: false };
7311
+ const next = {
7312
+ ...entry,
7313
+ registeredAt: entry.registeredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7314
+ reason: "lock-held"
7315
+ };
7316
+ const entries = [...existing, next];
7317
+ await store.write(entries);
7318
+ return { entries, newlyRegistered: true };
7319
+ }
7320
+ async function sweepDeferredWorktrees(store, deps) {
7321
+ if (!store) return { removed: [], stillDeferred: [] };
7322
+ const entries = await store.read();
7323
+ if (!entries.length) return { removed: [], stillDeferred: [] };
7324
+ const removed = [];
7325
+ const stillDeferred = [];
7326
+ for (const entry of entries) {
7327
+ const outcome = await removeWorktreeWithRecovery(entry.path, deps);
7328
+ if (outcome.status === "removed") removed.push(entry.path);
7329
+ else stillDeferred.push(entry);
7330
+ }
7331
+ await store.write(stillDeferred);
7332
+ return { removed, stillDeferred };
7333
+ }
7275
7334
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7276
7335
  async function removeWorktreeWithRecovery(wtPath, deps) {
7277
7336
  const maxAttempts = deps.maxAttempts ?? 3;
@@ -7337,7 +7396,9 @@ function summarizePrMergeCleanupStatus(input) {
7337
7396
  if (input.remoteBranch.status === "failed") return "warnings";
7338
7397
  if (input.localBranch.status === "failed") return "warnings";
7339
7398
  if (input.localBranch.reason === "worktree-removal-failed") return "warnings";
7399
+ if (input.localBranch.reason === "worktree-removal-deferred") return "warnings";
7340
7400
  if (input.worktree?.status === "failed") return "warnings";
7401
+ if (input.worktree?.status === "deferred") return "warnings";
7341
7402
  return "clean";
7342
7403
  }
7343
7404
  function buildPrMergeResultPayload(input) {
@@ -7600,6 +7661,12 @@ async function cleanupPrMergeLocalBranch(branch, options) {
7600
7661
  const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
7601
7662
  const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
7602
7663
  const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
7664
+ const removeDeps = {
7665
+ git,
7666
+ sleep: options.sleep ?? defaultSleep,
7667
+ removeWorktreeDir: options.removeWorktreeDir
7668
+ };
7669
+ await sweepDeferredWorktrees(options.deferredStore, removeDeps).catch(() => void 0);
7603
7670
  const mainWorktreePath = beforeWorktrees[0]?.path ?? afterWorktrees[0]?.path;
7604
7671
  const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
7605
7672
  if (wtPath && mainWorktreeTarget) {
@@ -7620,6 +7687,24 @@ async function cleanupPrMergeLocalBranch(branch, options) {
7620
7687
  });
7621
7688
  if (outcome.status === "removed") {
7622
7689
  report.worktree = { path: wtPath, status: "removed", stageTeardown, recovery: outcome.recovery };
7690
+ } else if (isPersistentWorktreeLockFailure(outcome) && options.deferredStore) {
7691
+ const { newlyRegistered } = await registerDeferredWorktree(options.deferredStore, {
7692
+ path: wtPath,
7693
+ branch,
7694
+ reason: "lock-held"
7695
+ });
7696
+ report.worktree = {
7697
+ path: wtPath,
7698
+ status: "deferred",
7699
+ reason: "lock-held",
7700
+ error: outcome.error,
7701
+ deferredNote: DEFERRED_NOTE,
7702
+ deferredSweepCommand: DEFERRED_SWEEP_COMMAND,
7703
+ ...newlyRegistered ? { safeCleanupCommand: safeWorktreeRemoveCommand(safeCwd, wtPath) } : {},
7704
+ stageTeardown
7705
+ };
7706
+ report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-deferred" };
7707
+ return report;
7623
7708
  } else {
7624
7709
  report.worktree = {
7625
7710
  path: wtPath,
@@ -8419,6 +8504,7 @@ var CURSOR_PLUGIN_INSTALL_LABEL = "Cursor Team Marketplace plugin install";
8419
8504
  var CURSOR_MARKETPLACE_INSTALL_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/cursor-marketplace-install.md";
8420
8505
  var CURSOR_PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
8421
8506
  var CURSOR_HOOKS_JSON_REL = "hooks/hooks.json";
8507
+ var CURSOR_HOOK_CLI_LABEL = "Cursor hook CLI bundle";
8422
8508
  function joinCachePath(root, ...parts) {
8423
8509
  const sep = root.includes("\\") ? "\\" : "/";
8424
8510
  return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
@@ -8474,6 +8560,20 @@ function buildCursorPluginInstallCheck(input) {
8474
8560
  }
8475
8561
  return { ...base2, cacheRoot: input.cacheRoot, pins: input.pins };
8476
8562
  }
8563
+ function buildCursorHookCliCheck(input) {
8564
+ const fix = "update the MMI Team Marketplace plugin (releases ship cli/dist under plugins/mmi/cli/dist) or install mmi-cli on PATH \u2014 Cursor hooks fall back to PATH when the bundled CLI is missing";
8565
+ const base2 = { ok: true, label: CURSOR_HOOK_CLI_LABEL, fix };
8566
+ if (!input.isOrgRepo) return base2;
8567
+ const shouldCheck = input.surface === "cursor" || input.pins.length > 0;
8568
+ if (!shouldCheck) return base2;
8569
+ if (input.pins.length === 0) {
8570
+ if (input.surface === "cursor" && !input.mmiCliOnPath) return { ...base2, ok: false };
8571
+ return base2;
8572
+ }
8573
+ const missingBundle = input.pins.some((p) => !p.hasCliBundle);
8574
+ if (missingBundle && !input.mmiCliOnPath) return { ...base2, ok: false };
8575
+ return base2;
8576
+ }
8477
8577
  var HUB_COMPAT_FIX = "update mmi-cli (npm i -g @mutmutco/cli) / refresh the MMI plugin, then rerun doctor";
8478
8578
  function buildHubCompatCheck(input) {
8479
8579
  const label = "Hub compatibility (client version vs Hub minimum)";
@@ -9502,7 +9602,8 @@ async function runTrainApply(command, deps, options = {}) {
9502
9602
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
9503
9603
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
9504
9604
  await deps.run("git", ["push", "origin", "main"]);
9505
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
9605
+ const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
9606
+ await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
9506
9607
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
9507
9608
  const autoRunSince2 = (deps.now ?? Date.now)();
9508
9609
  const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
@@ -9571,7 +9672,8 @@ async function runTrainApply(command, deps, options = {}) {
9571
9672
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
9572
9673
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
9573
9674
  await deps.run("git", ["push", "origin", "main"]);
9574
- const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
9675
+ const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
9676
+ await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
9575
9677
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
9576
9678
  const autoRunSince2 = (deps.now ?? Date.now)();
9577
9679
  const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
@@ -9987,6 +10089,11 @@ async function deriveHotfixVersion(deps) {
9987
10089
  function hotfixBranch(tag) {
9988
10090
  return `hotfix/${tag}`;
9989
10091
  }
10092
+ async function resolveHotfixDeployModel(deps, ctx) {
10093
+ const load = await loadProjectMeta(deps, ctx);
10094
+ const meta = load.status === "ok" ? load.meta : null;
10095
+ return resolveDeployModel2(meta, ctx.repo);
10096
+ }
9990
10097
  async function findHotfixPr(deps, ctx, tag) {
9991
10098
  const out = await deps.run("gh", [
9992
10099
  "pr",
@@ -10024,6 +10131,7 @@ async function resolveHotfixSource(deps, ctx, from) {
10024
10131
  }
10025
10132
  async function runHotfixStart(deps, options) {
10026
10133
  const ctx = await buildTrainApplyContext(deps);
10134
+ const deployModel = await resolveHotfixDeployModel(deps, ctx);
10027
10135
  const status = await deps.run("git", ["status", "--porcelain"]);
10028
10136
  if (status.trim()) throw new Error("working tree must be clean before hotfix start");
10029
10137
  await deps.run("git", ["fetch", "origin", "--tags"]);
@@ -10061,18 +10169,23 @@ async function runHotfixStart(deps, options) {
10061
10169
  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})`);
10062
10170
  }
10063
10171
  notes.push(`cherry-picked ${label} onto ${branch} (from origin/main, -x trailer recorded)`);
10064
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
10065
- const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
10066
- await deps.run("git", ["add", "--", ...changedFiles]);
10067
- const staged = await deps.run("git", ["diff", "--cached", "--name-only"]);
10068
- if (staged.trim()) {
10069
- await deps.run("git", ["commit", "-m", `hotfix ${tag}: lock plugin set + @mutmutco/cli distribution to ${version}`]);
10070
- notes.push(`distribution prepared + committed for ${version} (${changedFiles.length} locked paths)`);
10172
+ if (deployModel === "hub-serverless") {
10173
+ await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
10174
+ const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
10175
+ await deps.run("git", ["add", "--", ...changedFiles]);
10176
+ const staged = await deps.run("git", ["diff", "--cached", "--name-only"]);
10177
+ if (staged.trim()) {
10178
+ await deps.run("git", ["commit", "-m", `hotfix ${tag}: lock plugin set + @mutmutco/cli distribution to ${version}`]);
10179
+ notes.push(`distribution prepared + committed for ${version} (${changedFiles.length} locked paths)`);
10180
+ } else {
10181
+ notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
10182
+ }
10071
10183
  } else {
10072
- notes.push("distribution prepare produced no changes \u2014 nothing extra committed");
10184
+ notes.push(`distribution bump skipped (deployModel=${deployModel}, Hub-only step)`);
10073
10185
  }
10074
10186
  await deps.run("git", ["push", "-u", "origin", branch]);
10075
10187
  }
10188
+ const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
10076
10189
  const prUrl = clean2(await deps.run("gh", [
10077
10190
  "pr",
10078
10191
  "create",
@@ -10085,7 +10198,7 @@ async function runHotfixStart(deps, options) {
10085
10198
  "--title",
10086
10199
  `[hotfix] ${tag}`,
10087
10200
  "--body",
10088
- `Hotfix ${tag}: cherry-pick of ${label} onto origin/main with the locked distribution bump.
10201
+ `Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
10089
10202
 
10090
10203
  Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
10091
10204
  ]));
@@ -10130,6 +10243,7 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
10130
10243
  }
10131
10244
  async function runHotfixRelease(deps, versionInput, options = {}) {
10132
10245
  const ctx = await buildTrainApplyContext(deps);
10246
+ const deployModel = await resolveHotfixDeployModel(deps, ctx);
10133
10247
  const { tag, version } = normalizeHotfixVersion(versionInput);
10134
10248
  const status = await deps.run("git", ["status", "--porcelain"]);
10135
10249
  if (status.trim()) throw new Error("working tree must be clean before hotfix release");
@@ -10164,33 +10278,74 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
10164
10278
  }
10165
10279
  }
10166
10280
  const runs = [];
10167
- for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
10168
- runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
10281
+ let deployNote;
10282
+ if (deployModel === "hub-serverless") {
10283
+ for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
10284
+ runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
10285
+ }
10286
+ deployNote = "watched release-triggered deploy.yml + publish.yml";
10287
+ } else if (deployModel === "tenant-container" || deployModel === "solo-container") {
10288
+ const dispatch = await dispatchDeploy(
10289
+ deps,
10290
+ ctx,
10291
+ "main",
10292
+ "main",
10293
+ deployModel,
10294
+ true,
10295
+ (deps.now ?? Date.now)(),
10296
+ mergedSha,
10297
+ "report"
10298
+ );
10299
+ deployNote = dispatch.note;
10300
+ runs.push({
10301
+ workflow: "tenant-deploy.yml",
10302
+ url: dispatch.runUrl,
10303
+ conclusion: dispatch.deployStatus === "success" ? "success" : dispatch.deployStatus === "failure" ? "failure" : dispatch.deployStatus ?? "pending"
10304
+ });
10305
+ } else {
10306
+ deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
10169
10307
  }
10170
- const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10171
10308
  let verifyNote;
10172
- const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
10173
- try {
10174
- await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
10175
- const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
10176
- const sleep = sleeper(deps);
10177
- let attempt = 0;
10178
- for (; ; ) {
10179
- attempt++;
10180
- try {
10181
- await deps.run("node", verifyArgs);
10182
- break;
10183
- } catch (err) {
10184
- if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
10185
- await sleep(HOTFIX_VERIFY_RETRY_MS);
10309
+ if (deployModel === "hub-serverless") {
10310
+ const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10311
+ const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
10312
+ try {
10313
+ await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
10314
+ const verifyArgs = ["scripts/release-distribution.mjs", "verify", version, ...publishSucceeded ? [] : ["--skip-npm-view"]];
10315
+ const sleep = sleeper(deps);
10316
+ let attempt = 0;
10317
+ for (; ; ) {
10318
+ attempt++;
10319
+ try {
10320
+ await deps.run("node", verifyArgs);
10321
+ break;
10322
+ } catch (err) {
10323
+ if (attempt >= HOTFIX_VERIFY_ATTEMPTS) throw err;
10324
+ await sleep(HOTFIX_VERIFY_RETRY_MS);
10325
+ }
10186
10326
  }
10327
+ const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
10328
+ verifyNote = `distribution verified at ${tag}${publishSucceeded ? ` (npm included${retried})` : " (npm view skipped \u2014 publish run not confirmed)"}`;
10329
+ } finally {
10330
+ if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
10187
10331
  }
10188
- const retried = attempt > 1 ? `, after ${attempt} attempts (npm propagation lag)` : "";
10189
- verifyNote = `distribution verified at ${tag}${publishSucceeded ? ` (npm included${retried})` : " (npm view skipped \u2014 publish run not confirmed)"}`;
10190
- } finally {
10191
- if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]);
10332
+ } else {
10333
+ verifyNote = `distribution verify skipped (deployModel=${deployModel}, Hub-only step)`;
10192
10334
  }
10193
- return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote, announceNote };
10335
+ return {
10336
+ ...ctx,
10337
+ command: "hotfix-release",
10338
+ tag,
10339
+ mergedSha,
10340
+ deployModel,
10341
+ checks,
10342
+ tagNote,
10343
+ releaseNote,
10344
+ runs,
10345
+ deployNote,
10346
+ verifyNote,
10347
+ announceNote
10348
+ };
10194
10349
  }
10195
10350
  function deriveHotfixState(f) {
10196
10351
  if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
@@ -10226,6 +10381,11 @@ async function runHotfixStatus(deps, versionInput) {
10226
10381
  return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived };
10227
10382
  }
10228
10383
  }
10384
+ const inFlight = await findInFlightHotfixVersion(deps, ctx);
10385
+ if (inFlight) {
10386
+ const facts2 = await gatherHotfixFacts(deps, ctx, inFlight.tag, inFlight.version);
10387
+ return { ...ctx, command: "hotfix-status", ...facts2, ...deriveHotfixState(facts2) };
10388
+ }
10229
10389
  ({ tag, version } = await deriveHotfixVersion(deps));
10230
10390
  }
10231
10391
  const facts = await gatherHotfixFacts(deps, ctx, tag, version);
@@ -10271,6 +10431,47 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
10271
10431
  const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
10272
10432
  return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
10273
10433
  }
10434
+ async function findInFlightHotfixVersion(deps, ctx) {
10435
+ const tags = /* @__PURE__ */ new Set();
10436
+ const out = await deps.run("gh", [
10437
+ "pr",
10438
+ "list",
10439
+ "--repo",
10440
+ ctx.repo,
10441
+ "--base",
10442
+ "main",
10443
+ "--state",
10444
+ "all",
10445
+ "--limit",
10446
+ "50",
10447
+ "--json",
10448
+ "headRefName"
10449
+ ]);
10450
+ for (const row of JSON.parse(out || "[]")) {
10451
+ const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
10452
+ if (m) tags.add(m[1]);
10453
+ }
10454
+ const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
10455
+ for (const line of branchOut.split("\n").filter(Boolean)) {
10456
+ const ref = line.split(/\s+/)[1] ?? "";
10457
+ const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
10458
+ if (m) tags.add(m[1]);
10459
+ }
10460
+ const sorted = [...tags].sort((a, b) => {
10461
+ const pa = a.slice(1).split(".").map(Number);
10462
+ const pb = b.slice(1).split(".").map(Number);
10463
+ for (let i = 0; i < 3; i++) {
10464
+ if (pa[i] !== pb[i]) return pb[i] - pa[i];
10465
+ }
10466
+ return 0;
10467
+ });
10468
+ for (const tag of sorted) {
10469
+ const version = tag.slice(1);
10470
+ const facts = await gatherHotfixFacts(deps, ctx, tag, version);
10471
+ if (deriveHotfixState(facts).state !== "complete") return { tag, version };
10472
+ }
10473
+ return null;
10474
+ }
10274
10475
 
10275
10476
  // src/release-announce.ts
10276
10477
  var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
@@ -10716,6 +10917,9 @@ var requiredIssueTemplates = [
10716
10917
  ".github/ISSUE_TEMPLATE/config.yml"
10717
10918
  ];
10718
10919
  var requiredWorkflows = [];
10920
+ var requiredProductWorkflows = [".github/workflows/gate.yml"];
10921
+ var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
10922
+ var HUB_REPO3 = "mutmutco/MMI-Hub";
10719
10923
  var requiredLabels = ["bug", "feature", "task", "priority:urgent", "priority:high", "priority:medium", "priority:low"];
10720
10924
  var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
10721
10925
  var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
@@ -10728,6 +10932,7 @@ var requiredProjectWorkflows = [
10728
10932
  ];
10729
10933
  var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
10730
10934
  var requiredHubStatusChecks = ["cli", "infra", "docs"];
10935
+ var requiredProductStatusChecks = ["gate"];
10731
10936
  function expectedBranches(repoClass, releaseTrack) {
10732
10937
  if (isReleaseTrack(releaseTrack)) return branchesForTrack(releaseTrack);
10733
10938
  return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
@@ -10880,6 +11085,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
10880
11085
  for (const path2 of requiredWorkflows) {
10881
11086
  checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
10882
11087
  }
11088
+ if (repo !== HUB_REPO3) {
11089
+ for (const path2 of requiredProductWorkflows) {
11090
+ checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
11091
+ }
11092
+ checks.push({
11093
+ ok: await contentExists(deps, repo, baseBranch, requiredProductRulesetRef),
11094
+ label: "product required-check ruleset reference exists",
11095
+ detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
11096
+ });
11097
+ }
10883
11098
  if (repoClass === "deployable") {
10884
11099
  const trainScript = "scripts/next-version.mjs";
10885
11100
  checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
@@ -11015,7 +11230,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11015
11230
  label: "covered by an active org ruleset",
11016
11231
  detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
11017
11232
  });
11018
- if (repo === "mutmutco/MMI-Hub") {
11233
+ if (repo === HUB_REPO3) {
11019
11234
  const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
11020
11235
  const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
11021
11236
  checks.push({
@@ -11023,6 +11238,14 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11023
11238
  label: "Hub required status checks configured",
11024
11239
  detail: optionDetail(missing)
11025
11240
  });
11241
+ } else {
11242
+ const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
11243
+ const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
11244
+ checks.push({
11245
+ ok: missing.length === 0,
11246
+ label: "product required status checks configured",
11247
+ detail: missing.length ? `missing contexts: ${missing.join(", ")} \u2014 apply ${requiredProductRulesetRef} as an active repo ruleset` : void 0
11248
+ });
11026
11249
  }
11027
11250
  const declaredApis = (deps.requiredGcpApis ?? []).filter((a) => a && a.trim());
11028
11251
  if (declaredApis.length > 0) {
@@ -11074,12 +11297,32 @@ function parseOwnerRepo(repo) {
11074
11297
  return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
11075
11298
  }
11076
11299
  var DEFAULT_INSTALL_CMD = "npm ci";
11300
+ var DEFAULT_GATE_CMD = "npm run check";
11301
+ function gateSeedVars(cls) {
11302
+ if (cls === "content") {
11303
+ return {
11304
+ GATE_CMD: DEFAULT_GATE_CMD,
11305
+ GATE_PUSH_BRANCHES_YAML: "[main]",
11306
+ GATE_FULL_RUN_BRANCH: "main",
11307
+ GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/main"]'
11308
+ };
11309
+ }
11310
+ return {
11311
+ GATE_CMD: DEFAULT_GATE_CMD,
11312
+ GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
11313
+ GATE_FULL_RUN_BRANCH: "development",
11314
+ GATE_RULESET_BRANCH_REFS_JSON: '["refs/heads/development", "refs/heads/rc", "refs/heads/main"]'
11315
+ };
11316
+ }
11077
11317
  function withDerivedRepoVars(vars, parsed, cls) {
11078
11318
  const out = { ...vars };
11079
11319
  out.REPO_NAME ??= parsed.name;
11080
11320
  out.REPO_SLUG ??= parsed.slug;
11081
11321
  out.CLASS ??= cls;
11082
11322
  out.INSTALL_CMD ??= DEFAULT_INSTALL_CMD;
11323
+ for (const [key, value] of Object.entries(gateSeedVars(cls))) {
11324
+ out[key] ??= value;
11325
+ }
11083
11326
  return out;
11084
11327
  }
11085
11328
  function planSeedAction(seed, exists) {
@@ -13045,7 +13288,14 @@ program2.command("gc").description("dry-run cleanup for merged/closed PR branche
13045
13288
  const plan2 = await gcPlan(o.remote, limit);
13046
13289
  if (o.apply && !o.json) console.log(formatGcPlan(plan2, false));
13047
13290
  let applyResult;
13048
- if (o.apply) applyResult = await applyGcPlan(plan2, o.remote);
13291
+ if (o.apply) {
13292
+ const deferredStore = await createDeferredWorktreeStore();
13293
+ await sweepDeferredWorktrees(
13294
+ deferredStore,
13295
+ worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout)
13296
+ ).catch(() => void 0);
13297
+ applyResult = await applyGcPlan(plan2, o.remote);
13298
+ }
13049
13299
  if (o.json) {
13050
13300
  console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan: plan2, applyResult }, null, 2));
13051
13301
  } else if (!o.apply) {
@@ -14032,6 +14282,33 @@ async function remoteBranchExists(branch, options = {}) {
14032
14282
  }, options);
14033
14283
  }
14034
14284
  var COMPOSE_TIMEOUT_MS = 12e4;
14285
+ async function createDeferredWorktreeStore() {
14286
+ try {
14287
+ const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
14288
+ const registryPath = deferredWorktreesRegistryPath(stdout.trim());
14289
+ return {
14290
+ read: async () => {
14291
+ try {
14292
+ return parseDeferredWorktreesFile(await (0, import_promises5.readFile)(registryPath, "utf8"));
14293
+ } catch {
14294
+ return [];
14295
+ }
14296
+ },
14297
+ write: async (entries) => {
14298
+ await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
14299
+ }
14300
+ };
14301
+ } catch {
14302
+ return void 0;
14303
+ }
14304
+ }
14305
+ function worktreeRemoveDeps(execGit) {
14306
+ return {
14307
+ git: execGit,
14308
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
14309
+ removeWorktreeDir: async (worktreePath) => (0, import_promises5.rm)(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
14310
+ };
14311
+ }
14035
14312
  function teardownWorktreeStage(worktreePath) {
14036
14313
  return runWorktreeStageTeardown(worktreePath, {
14037
14314
  hasStageState: (wt) => (0, import_node_fs14.existsSync)(stageStatePath(wt)),
@@ -14087,6 +14364,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
14087
14364
  existedBefore: remoteBefore,
14088
14365
  reason: remoteNotAttemptedReason
14089
14366
  });
14367
+ const deferredStore = await createDeferredWorktreeStore();
14090
14368
  const localCleanup = repoArgs.length ? {
14091
14369
  branch: headRef,
14092
14370
  localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
@@ -14096,10 +14374,8 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
14096
14374
  startingPath,
14097
14375
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
14098
14376
  teardownWorktreeStage,
14099
- // Hardened fallback when retried `git worktree remove` still hits a Windows file lock (#967).
14100
- // `fs.rm` recursive removes a junction as a link (it does not traverse into the target) and its
14101
- // own maxRetries/retryDelay rides out a handle that an indexer/antivirus releases a moment later.
14102
- removeWorktreeDir: async (worktreePath) => (0, import_promises5.rm)(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
14377
+ deferredStore,
14378
+ removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
14103
14379
  });
14104
14380
  console.log(JSON.stringify(buildPrMergeResultPayload({
14105
14381
  number,
@@ -14567,10 +14843,11 @@ function renderHotfixStart(r) {
14567
14843
  }
14568
14844
  function renderHotfixRelease(r) {
14569
14845
  return [
14570
- `mmi-cli hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo}`,
14846
+ `mmi-cli hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
14571
14847
  ` - checks: ${r.checks}`,
14572
14848
  ` - ${r.tagNote}`,
14573
14849
  ` - ${r.releaseNote}`,
14850
+ ` - deploy: ${r.deployNote}`,
14574
14851
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
14575
14852
  ` - ${r.verifyNote}`,
14576
14853
  ...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
@@ -14975,6 +15252,7 @@ function cursorPluginCachePinSnapshots() {
14975
15252
  const path2 = (0, import_node_path13.join)(root, entry.name);
14976
15253
  const pluginJson = (0, import_node_path13.join)(path2, ".cursor-plugin", "plugin.json");
14977
15254
  const hooksJson = (0, import_node_path13.join)(path2, "hooks", "hooks.json");
15255
+ const cliBundle = (0, import_node_path13.join)(path2, "cli", "dist", "index.cjs");
14978
15256
  let isEmpty = true;
14979
15257
  try {
14980
15258
  isEmpty = (0, import_node_fs14.readdirSync)(path2).length === 0;
@@ -14986,6 +15264,7 @@ function cursorPluginCachePinSnapshots() {
14986
15264
  path: path2,
14987
15265
  hasPluginJson: (0, import_node_fs14.existsSync)(pluginJson),
14988
15266
  hasHooksJson: (0, import_node_fs14.existsSync)(hooksJson),
15267
+ hasCliBundle: (0, import_node_fs14.existsSync)(cliBundle),
14989
15268
  isEmpty
14990
15269
  };
14991
15270
  });
@@ -15237,6 +15516,15 @@ async function runDoctor(opts, io = consoleIo) {
15237
15516
  hubCheckout: hubCheckoutForCursorSeed()
15238
15517
  })
15239
15518
  );
15519
+ const cursorPins = cursorPluginCachePinSnapshots() ?? [];
15520
+ checks.push(
15521
+ buildCursorHookCliCheck({
15522
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
15523
+ surface,
15524
+ pins: cursorPins,
15525
+ mmiCliOnPath: onPath
15526
+ })
15527
+ );
15240
15528
  const gaps = checks.filter((c) => !c.ok);
15241
15529
  if (opts.banner) {
15242
15530
  if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
package/dist/saga.cjs CHANGED
@@ -3411,18 +3411,28 @@ var import_node_path2 = require("node:path");
3411
3411
  var CLIENT_VERSION_HEADER = "x-client-version";
3412
3412
 
3413
3413
  // src/client-version.ts
3414
- function resolveClientVersion() {
3414
+ function resolveClientVersionManifestCandidates(distDir = __dirname) {
3415
+ return [
3416
+ (0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
3417
+ (0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
3418
+ (0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
3419
+ (0, import_node_path2.join)(distDir, "..", "package.json")
3420
+ ];
3421
+ }
3422
+ function readVersionFromManifest(path2) {
3415
3423
  try {
3416
- const manifest = (0, import_node_path2.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
3417
- return JSON.parse((0, import_node_fs2.readFileSync)(manifest, "utf8")).version || "0.0.0";
3424
+ const version = JSON.parse((0, import_node_fs2.readFileSync)(path2, "utf8")).version;
3425
+ return typeof version === "string" && version.trim() ? version.trim() : null;
3418
3426
  } catch {
3419
- try {
3420
- const pkg = (0, import_node_path2.join)(__dirname, "..", "package.json");
3421
- return JSON.parse((0, import_node_fs2.readFileSync)(pkg, "utf8")).version || "0.0.0";
3422
- } catch {
3423
- return "0.0.0";
3424
- }
3427
+ return null;
3428
+ }
3429
+ }
3430
+ function resolveClientVersion() {
3431
+ for (const manifest of resolveClientVersionManifestCandidates()) {
3432
+ const version = readVersionFromManifest(manifest);
3433
+ if (version) return version;
3425
3434
  }
3435
+ return "0.0.0";
3426
3436
  }
3427
3437
  function clientVersionHeaders() {
3428
3438
  return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.30.0",
3
+ "version": "2.31.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",