@mutmutco/cli 3.25.0 → 3.26.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 +40 -24
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6196,7 +6196,7 @@ function formatGcPlan(plan, apply) {
6196
6196
  }
6197
6197
 
6198
6198
  // src/project-model.ts
6199
- var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
6199
+ var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-app", "desktop-game", "non-deployable", "cli-tool", "worker"];
6200
6200
  var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
6201
6201
  var RELEASE_TRACKS = ["full", "direct", "trunk"];
6202
6202
  var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
@@ -6233,12 +6233,12 @@ function resolveDeployModel(meta, repo) {
6233
6233
  const projectType = resolveProjectType(meta, repo);
6234
6234
  if (projectType === "content" || meta?.class === "content") return "content";
6235
6235
  if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
6236
- if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
6236
+ if (projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable") return "none";
6237
6237
  if (projectType === "cli-tool") return "registry-publish";
6238
6238
  return "tenant-container";
6239
6239
  }
6240
6240
  function projectTypeClearsWebProfile(projectType, deployModel) {
6241
- return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
6241
+ return projectType === "content" || projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
6242
6242
  }
6243
6243
  function inferReleaseTrackFromBranches(hints) {
6244
6244
  if (hints.hasRcBranch) return void 0;
@@ -9608,6 +9608,10 @@ function seedMatchesDeployModel(seed, deployModel) {
9608
9608
  if (!seed.deployModels?.length) return true;
9609
9609
  return deployModel != null && seed.deployModels.includes(deployModel);
9610
9610
  }
9611
+ function seedMatchesProjectType(seed, projectType) {
9612
+ if (!seed.projectTypes?.length) return true;
9613
+ return projectType != null && seed.projectTypes.includes(projectType);
9614
+ }
9611
9615
  function planSeedAction(seed, exists) {
9612
9616
  if (seed.source === "fanout") {
9613
9617
  return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
@@ -9620,18 +9624,25 @@ function planSeedAction(seed, exists) {
9620
9624
  }
9621
9625
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
9622
9626
  }
9623
- function reconcileSeedAction(action, content, isManagedBlock) {
9627
+ function reconcileSeedAction(action, content, isManagedBlock, remoteContent = null) {
9624
9628
  if (action.action === "skip") return action;
9625
9629
  if (content == null) {
9626
9630
  return { ...action, action: "skip", reason: "no resolvable content" };
9627
9631
  }
9628
- if (isManagedBlock) return action;
9629
- const unfilled = missingPlaceholders(content);
9630
- if (unfilled.length) {
9631
- return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
9632
+ if (!isManagedBlock) {
9633
+ const unfilled = missingPlaceholders(content);
9634
+ if (unfilled.length) {
9635
+ return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
9636
+ }
9637
+ }
9638
+ if (action.action === "update" && remoteContent != null && sameIgnoringEol(content, remoteContent)) {
9639
+ return { ...action, action: "skip", reason: "already current" };
9632
9640
  }
9633
9641
  return action;
9634
9642
  }
9643
+ function sameIgnoringEol(a, b) {
9644
+ return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
9645
+ }
9635
9646
  function renderSeedPlan(actions) {
9636
9647
  const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
9637
9648
  for (const a of actions) {
@@ -11927,7 +11938,6 @@ function bootstrapPlan(repo, repoClass) {
11927
11938
  { label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
11928
11939
  { label: "attach GitHub Project v2 and register Hub registry META", gated: true },
11929
11940
  { label: "seed README.md and architecture.md", gated: true },
11930
- { label: "commit .cursor/rules/<repo>.mdc", gated: true },
11931
11941
  { label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
11932
11942
  ];
11933
11943
  }
@@ -14957,9 +14967,6 @@ async function contentText(deps, repo, branch, path2) {
14957
14967
  return null;
14958
14968
  }
14959
14969
  }
14960
- function repoSlugFromFullName(repo) {
14961
- return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
14962
- }
14963
14970
  async function getProtection(deps, repo, branch) {
14964
14971
  try {
14965
14972
  return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
@@ -15109,13 +15116,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
15109
15116
  label: "architecture.md is filled (no template placeholders)",
15110
15117
  detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
15111
15118
  });
15112
- const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
15113
- const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
15114
- checks.push({
15115
- ok: agentRulesOk,
15116
- label: "Cursor agent rules file exists",
15117
- detail: agentRulesOk ? void 0 : `expected: ${agentRulesPath}`
15118
- });
15119
15119
  const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
15120
15120
  const labelNames = new Set(labels.map((l) => l.name));
15121
15121
  for (const label of requiredLabels) {
@@ -15699,10 +15699,16 @@ function attestedLine(att) {
15699
15699
  var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
15700
15700
  function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
15701
15701
  const attested = appAttestationOf(meta);
15702
- const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15702
+ const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-app" && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15703
15703
  const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
15704
15704
  if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
15705
15705
  if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
15706
+ if (projectType === "desktop-app") {
15707
+ return [
15708
+ "Desktop app: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned installer/packaging outside the tenant train.",
15709
+ "Keep README.md and architecture.md explicit about the per-OS build and distribution path (installer, and any registry publish alongside it) so readiness does not guess web infra."
15710
+ ];
15711
+ }
15706
15712
  if (projectType === "desktop-game") {
15707
15713
  return [
15708
15714
  "Desktop game: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned release packaging outside the tenant train.",
@@ -15836,7 +15842,7 @@ function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
15836
15842
  patch.requiredRuntimeSecrets = next;
15837
15843
  }
15838
15844
  }
15839
- const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15845
+ const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-app|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15840
15846
  if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
15841
15847
  return { slug, patch, appOwnedGaps };
15842
15848
  }
@@ -22937,6 +22943,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22937
22943
  const actions = [];
22938
22944
  const applied = [];
22939
22945
  let applyDeployModel;
22946
+ let applyProjectType;
22940
22947
  try {
22941
22948
  const payload = buildRegisterPayload(repo, o.class, vars, {
22942
22949
  projectType: o.projectType || void 0,
@@ -22944,6 +22951,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22944
22951
  releaseTrack: o.releaseTrack || void 0
22945
22952
  });
22946
22953
  applyDeployModel = payload.deployModel;
22954
+ applyProjectType = payload.projectType;
22947
22955
  } catch {
22948
22956
  }
22949
22957
  let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
@@ -22968,6 +22976,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22968
22976
  for (const seed of manifest.seeds) {
22969
22977
  if (!seed.classes.includes(o.class)) continue;
22970
22978
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
22979
+ if (!seedMatchesProjectType(seed, applyProjectType)) continue;
22971
22980
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
22972
22981
  let exists = false;
22973
22982
  let sha;
@@ -22991,7 +23000,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22991
23000
  const planned = planSeedAction(resolved, exists);
22992
23001
  const isBlock = resolved.source === "managed-block";
22993
23002
  const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile4) : null;
22994
- const action = reconcileSeedAction(planned, content, isBlock);
23003
+ const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
22995
23004
  actions.push(action);
22996
23005
  if (o.execute && (action.action === "create" || action.action === "update")) {
22997
23006
  await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
@@ -23024,10 +23033,17 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
23024
23033
  ]);
23025
23034
  seedPrUrl = created.url;
23026
23035
  }
23036
+ let autoMergeEnabled = true;
23027
23037
  await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
23028
- if (!/already/i.test(String(e.message ?? ""))) throw e;
23038
+ const message = String(e.message ?? "");
23039
+ if (/already/i.test(message)) return;
23040
+ if (/clean status|enablePullRequestAutoMerge/i.test(message)) {
23041
+ autoMergeEnabled = false;
23042
+ return;
23043
+ }
23044
+ throw e;
23029
23045
  });
23030
- applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
23046
+ applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
23031
23047
  }
23032
23048
  if (o.execute && o.class === "deployable") {
23033
23049
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.25.0",
3
+ "version": "3.26.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",