@mutmutco/cli 2.38.1 → 2.39.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.
Files changed (2) hide show
  1. package/dist/main.cjs +41 -3
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -8656,6 +8656,10 @@ function gateConfigToVars(gate) {
8656
8656
  if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
8657
8657
  return out;
8658
8658
  }
8659
+ function seedMatchesDeployModel(seed, deployModel) {
8660
+ if (!seed.deployModels?.length) return true;
8661
+ return deployModel != null && seed.deployModels.includes(deployModel);
8662
+ }
8659
8663
  function planSeedAction(seed, exists) {
8660
8664
  if (seed.source === "fanout") {
8661
8665
  return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
@@ -11877,6 +11881,20 @@ async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
11877
11881
  return reconciled;
11878
11882
  }
11879
11883
 
11884
+ // src/git-clean-tree.ts
11885
+ function isAgentScratchPath(path2) {
11886
+ const normalized = path2.replace(/\\/g, "/").trim();
11887
+ return normalized === ".mmi" || normalized.startsWith(".mmi/");
11888
+ }
11889
+ function porcelainHasBlockingChanges(porcelain) {
11890
+ return porcelain.split("\n").some((line) => {
11891
+ const trimmed = line.trim();
11892
+ if (!trimmed) return false;
11893
+ const path2 = trimmed.slice(3).split(" -> ")[0]?.trim() ?? "";
11894
+ return path2 !== "" && !isAgentScratchPath(path2);
11895
+ });
11896
+ }
11897
+
11880
11898
  // src/train-apply.ts
11881
11899
  function resolveDeployModel2(meta, repo) {
11882
11900
  const m = meta?.deployModel;
@@ -12032,7 +12050,7 @@ function commandAuthorityLabel(owner) {
12032
12050
  }
12033
12051
  async function requireCleanTree(deps) {
12034
12052
  const status = await deps.run("git", ["status", "--porcelain"]);
12035
- if (status.trim()) throw new Error("working tree must be clean before train apply");
12053
+ if (porcelainHasBlockingChanges(status)) throw new Error("working tree must be clean before train apply");
12036
12054
  }
12037
12055
  async function requireBranch(deps, branch) {
12038
12056
  const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
@@ -13041,7 +13059,7 @@ async function runHotfixStart(deps, options) {
13041
13059
  const ctx = await buildTrainApplyContext(deps);
13042
13060
  const deployModel = await resolveHotfixDeployModel(deps, ctx);
13043
13061
  const status = await deps.run("git", ["status", "--porcelain"]);
13044
- if (status.trim()) throw new Error("working tree must be clean before hotfix start");
13062
+ if (porcelainHasBlockingChanges(status)) throw new Error("working tree must be clean before hotfix start");
13045
13063
  await deps.run("git", ["fetch", "origin", "--tags"]);
13046
13064
  const { tag, version } = await deriveHotfixVersion(deps);
13047
13065
  const branch = hotfixBranch(tag);
@@ -13154,7 +13172,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13154
13172
  const deployModel = await resolveHotfixDeployModel(deps, ctx);
13155
13173
  const { tag, version } = normalizeHotfixVersion(versionInput);
13156
13174
  const status = await deps.run("git", ["status", "--porcelain"]);
13157
- if (status.trim()) throw new Error("working tree must be clean before hotfix release");
13175
+ if (porcelainHasBlockingChanges(status)) throw new Error("working tree must be clean before hotfix release");
13158
13176
  await deps.run("git", ["fetch", "origin", "--tags"]);
13159
13177
  const pr2 = await findHotfixPr(deps, ctx, tag);
13160
13178
  if (!pr2) throw new Error(`no hotfix PR found for ${tag} (head ${hotfixBranch(tag)}, base main) \u2014 run mmi-cli hotfix start first`);
@@ -14029,6 +14047,15 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
14029
14047
  const trainScript = "scripts/next-version.mjs";
14030
14048
  checks.push({ ok: await contentExists2(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
14031
14049
  }
14050
+ if (repoClass === "deployable" && deps.deployModel === "tenant-container") {
14051
+ for (const path2 of ["docker-compose.yml", "Dockerfile"]) {
14052
+ checks.push({
14053
+ ok: await contentExists2(deps, repo, baseBranch, path2),
14054
+ label: `tenant-container runtime file exists: ${path2}`,
14055
+ detail: `bootstrap seeds ${path2} for deployModel tenant-container \u2014 see docs/Guides/tenant-runtime.md`
14056
+ });
14057
+ }
14058
+ }
14032
14059
  checks.push({ ok: await contentExists2(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
14033
14060
  const readme = await contentText(deps, repo, baseBranch, "README.md");
14034
14061
  checks.push({
@@ -18264,6 +18291,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
18264
18291
  const report = await verifyBootstrap(repo, o.class, {
18265
18292
  client: defaultGitHubClient(),
18266
18293
  projectMeta: meta,
18294
+ deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
18267
18295
  readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
18268
18296
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
18269
18297
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
@@ -18335,8 +18363,18 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
18335
18363
  }
18336
18364
  const actions = [];
18337
18365
  const applied = [];
18366
+ let applyDeployModel;
18367
+ try {
18368
+ applyDeployModel = buildRegisterPayload(repo, o.class, vars, {
18369
+ projectType: o.projectType || void 0,
18370
+ deployModel: o.deployModel || void 0,
18371
+ releaseTrack: o.releaseTrack || void 0
18372
+ }).deployModel;
18373
+ } catch {
18374
+ }
18338
18375
  for (const seed of manifest.seeds) {
18339
18376
  if (!seed.classes.includes(o.class)) continue;
18377
+ if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
18340
18378
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
18341
18379
  let exists = false;
18342
18380
  let sha;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.38.1",
3
+ "version": "2.39.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",