@mutmutco/cli 2.45.0 → 2.46.1

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.
Files changed (2) hide show
  1. package/dist/main.cjs +84 -17
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12830,6 +12830,33 @@ function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
12830
12830
  function normPath2(path2) {
12831
12831
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
12832
12832
  }
12833
+ function parseWorktreeListPaths(stdout) {
12834
+ const paths = [];
12835
+ for (const line of stdout.split(/\r?\n/)) {
12836
+ if (line.startsWith("worktree ")) paths.push(line.slice("worktree ".length).trim());
12837
+ }
12838
+ return paths;
12839
+ }
12840
+ function collectReservedStagePorts(worktreePaths, readStateFile = readState) {
12841
+ const ports = /* @__PURE__ */ new Set();
12842
+ for (const wt of worktreePaths) {
12843
+ const state = readStateFile(stageStatePath(wt));
12844
+ if (state?.port != null && Number.isInteger(state.port) && state.port > 0) ports.add(state.port);
12845
+ }
12846
+ return ports;
12847
+ }
12848
+ async function listRepoWorktreePaths(cwd) {
12849
+ const stdout = await gitText(cwd, ["worktree", "list", "--porcelain"]);
12850
+ const paths = stdout ? parseWorktreeListPaths(stdout) : [];
12851
+ return paths.length ? paths : [cwd];
12852
+ }
12853
+ function parseStagePortFlag(raw) {
12854
+ const port = Number(raw);
12855
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
12856
+ throw new Error("stage port must be an integer 1024..65535");
12857
+ }
12858
+ return port;
12859
+ }
12833
12860
  function pathUnder(childPath, parentPath) {
12834
12861
  const child = normPath2(childPath);
12835
12862
  const parent = normPath2(parentPath);
@@ -12976,7 +13003,7 @@ async function ensureStagePortAvailable(port, cwd, deps) {
12976
13003
  );
12977
13004
  }
12978
13005
  }
12979
- async function resolveStagePort(config, guard) {
13006
+ async function resolveStagePort(config, guard, reservedPorts = /* @__PURE__ */ new Set()) {
12980
13007
  if (!config.portRange) return void 0;
12981
13008
  let dockerBusy = /* @__PURE__ */ new Set();
12982
13009
  if (guard) {
@@ -12990,11 +13017,25 @@ async function resolveStagePort(config, guard) {
12990
13017
  const [s, e] = config.portRange;
12991
13018
  const free = /* @__PURE__ */ new Set();
12992
13019
  for (let p = s; p <= e; p++) {
12993
- if (dockerBusy.has(p)) continue;
13020
+ if (dockerBusy.has(p) || reservedPorts.has(p)) continue;
12994
13021
  if (await isPortFree(p)) free.add(p);
12995
13022
  }
12996
13023
  return pickStagePort(config.portRange, (p) => free.has(p));
12997
13024
  }
13025
+ async function reservedPortsForWorktree(cwd) {
13026
+ const self = normPath2(cwd);
13027
+ const siblings = (await listRepoWorktreePaths(cwd)).filter((p) => normPath2(p) !== self);
13028
+ return collectReservedStagePorts(siblings);
13029
+ }
13030
+ async function assertStagePortAvailable(port, cwd, guard, reserved) {
13031
+ if (reserved.has(port)) {
13032
+ throw new Error(`stage port ${port} is reserved by another worktree's active stage \u2014 pick a different port or stop that stage`);
13033
+ }
13034
+ if (!await isPortFree(port)) {
13035
+ throw new Error(`stage port ${port} is already in use on loopback`);
13036
+ }
13037
+ if (guard) await ensureStagePortAvailable(port, cwd, guard);
13038
+ }
12998
13039
  function substituteStagePort(s, stagePort) {
12999
13040
  return s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
13000
13041
  }
@@ -13051,8 +13092,7 @@ async function resolveStageIdentity(cwd) {
13051
13092
  async function resolveGlobalStatePath(cwd, explicit) {
13052
13093
  if (explicit === false) return void 0;
13053
13094
  if (typeof explicit === "string") return explicit;
13054
- const commonDir = await gitText(cwd, ["rev-parse", "--git-common-dir"]);
13055
- return commonDir ? stageGlobalStatePath(cwd, commonDir) : void 0;
13095
+ return void 0;
13056
13096
  }
13057
13097
  function readState(path2) {
13058
13098
  if (!(0, import_node_fs19.existsSync)(path2)) return null;
@@ -13120,11 +13160,12 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
13120
13160
  }
13121
13161
  async function stopStage(opts = {}) {
13122
13162
  const cwd = opts.cwd ?? process.cwd();
13163
+ const scopedCwd = opts.requiredIdentityCwd ?? cwd;
13123
13164
  const statePath = opts.statePath ?? stageStatePath(cwd);
13124
13165
  const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
13125
13166
  const localState = readState(statePath);
13126
13167
  const globalState = globalStatePath && globalStatePath !== statePath ? readState(globalStatePath) : null;
13127
- const state = stageStateMatchesRequiredCwd(globalState, opts.requiredIdentityCwd) ? globalState : stageStateMatchesRequiredCwd(localState, opts.requiredIdentityCwd) ? localState : null;
13168
+ const state = stageStateMatchesRequiredCwd(localState, scopedCwd) ? localState : stageStateMatchesRequiredCwd(globalState, scopedCwd) ? globalState : null;
13128
13169
  if (!state) {
13129
13170
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
13130
13171
  }
@@ -13146,7 +13187,13 @@ async function startStage(config = {}, opts = {}) {
13146
13187
  const statePath = opts.statePath ?? stageStatePath(cwd);
13147
13188
  const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
13148
13189
  const portGuard = resolveStagePortGuard(opts);
13149
- const stagePort = opts.stagePort ?? await resolveStagePort(config, portGuard);
13190
+ const reserved = await reservedPortsForWorktree(cwd);
13191
+ let stagePort = opts.stagePort;
13192
+ if (stagePort != null) {
13193
+ await assertStagePortAvailable(stagePort, cwd, portGuard, reserved);
13194
+ } else {
13195
+ stagePort = await resolveStagePort(config, portGuard, reserved);
13196
+ }
13150
13197
  const sub = (s) => substituteStagePort(s, stagePort);
13151
13198
  if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
13152
13199
  if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
@@ -13204,8 +13251,14 @@ async function runStage(config = {}, opts = {}) {
13204
13251
  const cwd = opts.cwd ?? process.cwd();
13205
13252
  const timeoutMs = opts.timeoutMs ?? 6e4;
13206
13253
  const portGuard = resolveStagePortGuard(opts);
13207
- await stopStage({ ...opts, cwd });
13208
- const stagePort = opts.stagePort ?? await resolveStagePort(config, portGuard);
13254
+ await stopStage({ ...opts, cwd, requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd });
13255
+ const reserved = await reservedPortsForWorktree(cwd);
13256
+ let stagePort = opts.stagePort;
13257
+ if (stagePort != null) {
13258
+ await assertStagePortAvailable(stagePort, cwd, portGuard, reserved);
13259
+ } else {
13260
+ stagePort = await resolveStagePort(config, portGuard, reserved);
13261
+ }
13209
13262
  const sub = (s) => substituteStagePort(s, stagePort);
13210
13263
  await ensureStageRuntimeEnv(config, opts, cwd);
13211
13264
  const extraEnv = stageExtraEnv(config, stagePort);
@@ -19917,6 +19970,22 @@ function rawValues(flag) {
19917
19970
  }
19918
19971
  return out;
19919
19972
  }
19973
+ function stagePortFromArgv() {
19974
+ const raw = rawValue("--port", "");
19975
+ if (!raw) return void 0;
19976
+ return parseStagePortFlag(raw);
19977
+ }
19978
+ function stageScopedRunOpts(o) {
19979
+ const cwd = process.cwd();
19980
+ const stagePort = stagePortFromArgv();
19981
+ return {
19982
+ timeoutMs: Number(o.timeoutMs || 6e4),
19983
+ allowStaleEnv: o.allowStaleEnv,
19984
+ cwd,
19985
+ requiredIdentityCwd: cwd,
19986
+ ...stagePort != null ? { stagePort } : {}
19987
+ };
19988
+ }
19920
19989
  function printLine(value) {
19921
19990
  (0, import_node_fs22.writeSync)(1, `${value}
19922
19991
  `);
@@ -20046,7 +20115,7 @@ async function runStageLiveCommand(o) {
20046
20115
  return failGraceful(`stage --live: ${e.message}`);
20047
20116
  }
20048
20117
  }
20049
- var stage = program2.command("stage").description("plan or run the repo local stage environment; --live = personal IP-gated cloud dev stage").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous, build, start, health-check").option("--live", "personal cloud dev stage: deploy the current branch to the dev runtime, served only to your public IP").option("--down", "with --live: stop the dev runtime and clear the IP allowlist").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
20118
+ var stage = program2.command("stage").description("plan or run the repo local stage environment; --live = personal IP-gated cloud dev stage").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous in this worktree, build, start, health-check").option("--port <port>", "loopback port for this worktree stage (1024..65535; default picks a free port in the registry range)").option("--live", "personal cloud dev stage: deploy the current branch to the dev runtime, served only to your public IP").option("--down", "with --live: stop the dev runtime and clear the IP allowlist").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
20050
20119
  if (o.down && !o.live) return fail("stage: --down applies to --live only (local teardown is `mmi-cli stage stop --apply`)");
20051
20120
  if (o.live) return runStageLiveCommand(o);
20052
20121
  const res = await resolveStage();
@@ -20055,7 +20124,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
20055
20124
  const cfg = res.config ?? res.derived.config;
20056
20125
  const hold = stageKeepAlive();
20057
20126
  try {
20058
- const result = await runStage(cfg, { timeoutMs: Number(o.timeoutMs || 6e4) });
20127
+ const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
20059
20128
  const reportUrl = reportedStageUrl(res, result);
20060
20129
  const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
20061
20130
  return printLine(o.json ? JSON.stringify({ ...result, source: res.source, url: reportUrl }) : `mmi-cli stage: ${result.message}${url}`);
@@ -20078,13 +20147,13 @@ stage.command("stop").description("stop the previous local stage process recorde
20078
20147
  return printLine(o.json ? JSON.stringify({ command: "stage stop", steps }, null, 2) : renderSteps("mmi-cli stage stop: dry-run plan", steps));
20079
20148
  }
20080
20149
  try {
20081
- const result = await stopStage();
20150
+ const result = await stopStage({ cwd: process.cwd(), requiredIdentityCwd: process.cwd() });
20082
20151
  printLine(o.json ? JSON.stringify(result) : `mmi-cli stage stop: ${result.message}`);
20083
20152
  } catch (e) {
20084
20153
  fail(`stage stop: ${e.message}`);
20085
20154
  }
20086
20155
  });
20087
- stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process").option("--timeout-ms <ms>", "bounded health timeout", "60000").option("--allow-stale-env", "start despite a stale ensureEnv target file").action(async () => {
20156
+ stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process").option("--port <port>", "loopback port for this worktree stage (1024..65535)").option("--timeout-ms <ms>", "bounded health timeout", "60000").option("--allow-stale-env", "start despite a stale ensureEnv target file").action(async () => {
20088
20157
  const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000"), allowStaleEnv: rawFlag("--allow-stale-env") };
20089
20158
  const res = await resolveStage();
20090
20159
  if (!o.apply) {
@@ -20102,8 +20171,7 @@ stage.command("start").description("start the configured local stage process and
20102
20171
  let printed = false;
20103
20172
  try {
20104
20173
  const result = await startStage(cfg, {
20105
- timeoutMs: Number(o.timeoutMs || 6e4),
20106
- allowStaleEnv: o.allowStaleEnv,
20174
+ ...stageScopedRunOpts({ timeoutMs: o.timeoutMs, allowStaleEnv: o.allowStaleEnv }),
20107
20175
  vaultEnvMerge,
20108
20176
  onReady: (ready) => {
20109
20177
  printed = true;
@@ -20120,7 +20188,7 @@ stage.command("start").description("start the configured local stage process and
20120
20188
  return failGraceful(`stage start: ${e.message}`);
20121
20189
  }
20122
20190
  });
20123
- stage.command("run").description("force-stop previous stage, build, start, and health-check").option("--json", "machine-readable output").option("--apply", "run the configured stage sequence").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").option("--allow-stale-env", "run despite a stale ensureEnv target file").action(async () => {
20191
+ stage.command("run").description("force-stop previous stage, build, start, and health-check").option("--json", "machine-readable output").option("--apply", "run the configured stage sequence").option("--port <port>", "loopback port for this worktree stage (1024..65535)").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").option("--allow-stale-env", "run despite a stale ensureEnv target file").action(async () => {
20124
20192
  const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000"), allowStaleEnv: rawFlag("--allow-stale-env") };
20125
20193
  const res = await resolveStage();
20126
20194
  if (!o.apply) {
@@ -20138,8 +20206,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
20138
20206
  let printed = false;
20139
20207
  try {
20140
20208
  const result = await runStage(cfg, {
20141
- timeoutMs: Number(o.timeoutMs || 6e4),
20142
- allowStaleEnv: o.allowStaleEnv,
20209
+ ...stageScopedRunOpts({ timeoutMs: o.timeoutMs, allowStaleEnv: o.allowStaleEnv }),
20143
20210
  vaultEnvMerge,
20144
20211
  onReady: (ready) => {
20145
20212
  const reportUrl = reportedStageUrl(res, ready);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.45.0",
3
+ "version": "2.46.1",
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",