@mnci/cli 1.14.0 → 1.15.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.
package/README.md CHANGED
@@ -24,7 +24,7 @@ first-party (or established community) Nx equivalent:
24
24
  | Hand-written Azure Function templates | `@nx/node:application` (plain Node app) + a thin Azure Functions v4 overlay |
25
25
  | doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |
26
26
 
27
- ## Commands (deliberately just three)
27
+ ## Commands (deliberately just four)
28
28
 
29
29
  ```sh
30
30
  mnci new my-repo # create a monorepo (prompts scope + registry)
@@ -61,8 +61,30 @@ mnci add flutter-internal-lib core # private shared package -> libs/
61
61
 
62
62
  mnci upgrade # re-apply the latest overlay (see below)
63
63
  mnci upgrade --agent windows-latest # ...with an explicit override
64
+
65
+ mnci doctor # check this workspace's invariants (read-only)
64
66
  ```
65
67
 
68
+ ## `mnci doctor`: checking the invariants actually hold
69
+
70
+ Read-only — it never edits the workspace. Every failing finding names the command
71
+ that fixes it (usually `mnci upgrade`), and it exits non-zero when anything failed,
72
+ so it works as a CI step as well as a local command.
73
+
74
+ Every check corresponds to an invariant that has **actually** been violated, in
75
+ this repo or in a workspace it generated. None are hypothetical; a check nobody has
76
+ ever needed is noise that trains people to ignore the output.
77
+
78
+ | Check | The failure it catches |
79
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
80
+ | Exactly one root ESLint config, and no per-project ones | The config fragmenting — every `@nx/*` generator writes one, so each project ends up linting against whichever config sits nearest |
81
+ | No stray `.prettierrc` | It outranks `.prettierrc.json`, so the whole formatting opinion is silently discarded while both files look fine |
82
+ | `@nx/eslint/plugin` registered in `nx.json` | Without it `npm run lint` exits 0 while linting nothing |
83
+ | The **resolved** `eslint` major | A declared range and an installed version are different things — manifests once said `^10` while the pin said 9 |
84
+ | `.npmrc` matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed |
85
+ | `versionActions` on publishable Dart/Python packages | Its absence aborts `nx release` for the **whole** workspace, not just that project |
86
+ | `nx sync:check` | A stale TypeScript project reference that was never committed |
87
+
66
88
  Everything else is plain Nx, surfaced as a small curated set of root scripts —
67
89
  each a single cross-platform command:
68
90
 
@@ -343,8 +365,12 @@ default `azure`): `azure` writes `azure-pipelines.yml`, `github` writes
343
365
  GitHub-hosted repo, or `both` while migrating between the two. Whichever
344
366
  provider(s), the pipeline does the **exact same thing**: both files are built
345
367
  from the same shared guard scripts (`overlay.ts`'s `PYTHON_INSTALL_GUARD`,
346
- `PACK_APPS_GUARD`, `releaseGuard`), so they can never drift on what CI
347
- actually runs — only the provider's own syntax differs.
368
+ `PACK_APPS_GUARD`, `releaseGuard`, `AFFECTED_OR_ALL_GUARD`), so they can never
369
+ drift on what CI actually runs — only the provider's own syntax differs. That
370
+ matters most for the last of those: the two providers detect a pull request
371
+ through different environment variables, so the guard reads both, and a
372
+ provider-specific copy would change _what CI verifies_ rather than merely how it
373
+ is spelled.
348
374
 
349
375
  The pipeline contains **no bash and no PowerShell**: every step is a built-in
350
376
  task/action or a single-line `git`/`npm`/`npx`/`node` command that `cmd.exe`
@@ -372,8 +398,29 @@ vulnerable transitive package) is exactly what this monorepo's own
372
398
  attempts automatically.
373
399
 
374
400
  Then `nx sync:check` (fails fast and clearly if the workspace wasn't
375
- synced+committed locally — see above), then one `nx run-many -t
376
- lint,test,build`. Pushes to `main` then:
401
+ synced+committed locally — see above), then `npm run format:check`, then **one
402
+ verify step** running `lint,typecheck,test,build`. `typecheck` is in that list
403
+ because a bundler-built project strips types without reading them, so `build`
404
+ passing proves nothing about type correctness.
405
+
406
+ That step verifies the **affected** projects on a pull request and **every**
407
+ project on anything else — including a push to `main`, so a release is always
408
+ verified in full. There is deliberately no separate `npm run lint` step: that is
409
+ `nx run-many -t lint`, a strict subset of the list above, and on an
410
+ affected-scoped PR it would re-lint every project and throw the benefit away.
411
+ `format:check` does stay workspace-wide, because `prettier --check .` is one
412
+ invocation over the whole tree rather than a per-project target, so there is
413
+ nothing to narrow.
414
+
415
+ Every fallback in that step verifies **everything**: no PR target branch, an
416
+ unresolvable merge-base (shallow clone, absent remote branch), any non-PR run.
417
+ That direction is deliberate — resolving the base too wide costs a few minutes,
418
+ while resolving it too narrow means CI runs almost nothing, reports green and has
419
+ verified nothing. The base is a `git merge-base`, not either provider's "base SHA"
420
+ field and not the GitHub-only `nrwl/nx-set-shas`, so one mechanism serves both
421
+ providers and is correct in each by construction.
422
+
423
+ Pushes to `main` then:
377
424
 
378
425
  - **Pack all apps** — each app's `package` target zips its build output into
379
426
  `dist/drop/<type>-<name>.zip` (e.g. `node-function-app-api.zip`,
package/dist/cli.js CHANGED
@@ -1234,11 +1234,8 @@ function readWorkspaceStack(workspaceRoot) {
1234
1234
  };
1235
1235
  }
1236
1236
 
1237
- // src/commands/interactive.ts
1238
- var import_node_path14 = require("path");
1239
- var import_prompts8 = require("@inquirer/prompts");
1240
-
1241
- // src/commands/new.ts
1237
+ // src/commands/doctor.ts
1238
+ var import_node_fs7 = require("fs");
1242
1239
  var import_node_path12 = require("path");
1243
1240
 
1244
1241
  // src/overlay.ts
@@ -1509,6 +1506,8 @@ var FLUTTER_TOOL_PATH_AZURE = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}console.log(
1509
1506
  var FLUTTER_TOOL_PATH_GITHUB = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1510
1507
  var FLUTTER_PUB_GET_GUARD = `node -e "const fs=require('node:fs');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('flutter',['pub','get'],{stdio:'inherit',shell:process.platform==='win32'}).status ?? 1)"`;
1511
1508
  var NPM_AUDIT_STEP = "npm audit --audit-level=high || echo npm audit found vulnerabilities, see log above - non-blocking, run npm audit locally to inspect";
1509
+ var VERIFY_TARGETS = "lint,typecheck,test,build";
1510
+ var AFFECTED_OR_ALL_GUARD = `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T,{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base,{stdio:'inherit',shell:true}).status ?? 1)"`;
1512
1511
  var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});if(fs.globSync('apps/*/project.json').length===0){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
1513
1512
  function releaseGuard(pythonPublishEnv) {
1514
1513
  return `node -e "const fs=require('node:fs'),cp=require('node:child_process');const hasNpm=fs.globSync('packages/*/package.json').length>0;const hasPython=fs.globSync('python-packages/*/pyproject.toml').length>0;if(!hasNpm&&!hasPython){console.log('Nothing to release - skipping.');process.exit(0)}const env={...process.env};${pythonPublishEnv}process.exit(cp.spawnSync('npx nx release --yes',{stdio:'inherit',shell:true,env}).status ?? 1)"`;
@@ -1677,27 +1676,28 @@ steps:
1677
1676
  - script: npx nx sync:check
1678
1677
  displayName: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1679
1678
 
1680
- # One verify for every run (PR and main). Nx cache makes unchanged test/build
1681
- # projects instant.
1682
- - script: npm run lint
1683
- displayName: Lint
1684
-
1685
1679
  # Prettier owns ALL formatting in this workspace, so it needs a gate of its
1686
1680
  # own: ESLint is configured for correctness only and deliberately reports
1687
1681
  # nothing about formatting. Without this step the entire formatting opinion is
1688
1682
  # advisory, and a workspace drifts out of compliance with no signal anywhere.
1683
+ #
1684
+ # Stays workspace-wide even on a PR, unlike the verify step below: it is one
1685
+ # Prettier invocation over the whole tree, not a per-project Nx target, so
1686
+ # there is nothing to scope down and formatting is never partially checked.
1689
1687
  - script: npm run format:check
1690
1688
  displayName: Check formatting (run 'npm run format' locally to fix)
1691
- # \`lint\` here also runs any Nx lint targets \`npm run lint\` does not cover \u2014
1692
- # notably Python's ruff (every Python project owns a hand-authored \`lint\`
1693
- # target). The JS lint runs twice, but Nx caches the repeat instantly.
1689
+
1690
+ # The one verify step, and deliberately the only one: affected projects on a
1691
+ # pull request, EVERY project on anything else (a push to main included, so a
1692
+ # release is always verified in full). Every fallback \u2014 no target branch, an
1693
+ # unresolvable merge-base \u2014 takes the full path, because a run that verifies
1694
+ # too little still reports green.
1694
1695
  #
1695
- # \`typecheck\` is load-bearing and not covered by \`build\`: a bundler-built
1696
- # project (esbuild, swc) strips types without reading them, so a workspace can
1697
- # build, test and lint green while carrying real type errors. --preset=ts
1698
- # infers a typecheck target for every project, so this costs one target name.
1699
- - script: npx nx run-many -t lint,typecheck,test,build
1700
- displayName: Lint, typecheck, test and build everything
1696
+ # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1697
+ # below, so adding it back as its own step would only duplicate work \u2014 and on
1698
+ # a pull request it would re-lint every project, discarding the point of this.
1699
+ - script: ${AFFECTED_OR_ALL_GUARD}
1700
+ displayName: Verify (affected on a PR, every project on main)
1701
1701
 
1702
1702
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1703
1703
  # target. Portable guard: skip cleanly when the workspace has no apps yet.
@@ -1876,29 +1876,31 @@ jobs:
1876
1876
  - run: npx nx sync:check
1877
1877
  name: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1878
1878
 
1879
- # One verify for every run (PR and main). Nx cache makes unchanged
1880
- # test/build projects instant.
1881
- - run: npm run lint
1882
- name: Lint
1883
-
1884
1879
  # Prettier owns ALL formatting in this workspace, so it needs a gate of
1885
1880
  # its own: ESLint is configured for correctness only and deliberately
1886
1881
  # reports nothing about formatting. Without this step the entire
1887
1882
  # formatting opinion is advisory, and a workspace drifts out of
1888
1883
  # compliance with no signal anywhere.
1884
+ #
1885
+ # Stays workspace-wide even on a PR, unlike the verify step below: it is
1886
+ # one Prettier invocation over the whole tree, not a per-project Nx
1887
+ # target, so there is nothing to scope down and formatting is never
1888
+ # partially checked.
1889
1889
  - run: npm run format:check
1890
1890
  name: Check formatting (run 'npm run format' locally to fix)
1891
- # \`lint\` here also runs any Nx lint targets \`npm run lint\` does not cover \u2014
1892
- # notably Python's ruff (every Python project owns a hand-authored \`lint\`
1893
- # target). The JS lint runs twice, but Nx caches the repeat instantly.
1891
+
1892
+ # The one verify step, and deliberately the only one: affected projects on
1893
+ # a pull request, EVERY project on anything else (a push to main included,
1894
+ # so a release is always verified in full). Every fallback \u2014 no target
1895
+ # branch, an unresolvable merge-base \u2014 takes the full path, because a run
1896
+ # that verifies too little still reports green.
1894
1897
  #
1895
- # \`typecheck\` is load-bearing and not covered by \`build\`: a bundler-built
1896
- # project (esbuild, swc) strips types without reading them, so a workspace
1897
- # can build, test and lint green while carrying real type errors.
1898
- # --preset=ts infers a typecheck target for every project, so this costs
1899
- # one target name.
1900
- - run: npx nx run-many -t lint,typecheck,test,build
1901
- name: Lint, typecheck, test and build everything
1898
+ # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1899
+ # below, so adding it back as its own step would only duplicate work \u2014 and
1900
+ # on a pull request it would re-lint every project, discarding the point
1901
+ # of this.
1902
+ - run: ${AFFECTED_OR_ALL_GUARD}
1903
+ name: Verify (affected on a PR, every project on main)
1902
1904
 
1903
1905
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1904
1906
  # target. Portable guard: skip cleanly when the workspace has no apps yet.
@@ -2053,7 +2055,166 @@ function applyOverlay(workspaceRoot, options) {
2053
2055
  }
2054
2056
  }
2055
2057
 
2058
+ // src/commands/doctor.ts
2059
+ var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".")[0];
2060
+ function checkEslintConfigs(workspaceRoot) {
2061
+ const rootConfigs = (0, import_node_fs7.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
2062
+ const projectConfigs = (0, import_node_fs7.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2063
+ cwd: workspaceRoot
2064
+ });
2065
+ return [
2066
+ {
2067
+ check: "root ESLint config",
2068
+ ok: rootConfigs.length === 1,
2069
+ detail: rootConfigs.length === 0 ? "no eslint.config.* at the workspace root" : `${rootConfigs.length} root configs: ${rootConfigs.join(", ")}`,
2070
+ remedy: "run `mnci upgrade` to rewrite the root config"
2071
+ },
2072
+ {
2073
+ check: "no per-project ESLint configs",
2074
+ ok: projectConfigs.length === 0,
2075
+ detail: `found ${projectConfigs.length}: ${projectConfigs.join(", ")}`,
2076
+ remedy: "run `mnci upgrade`, which sweeps {apps,libs,packages}/*/eslint.config.*"
2077
+ }
2078
+ ];
2079
+ }
2080
+ function checkPrettierConfig(workspaceRoot) {
2081
+ const strayExists = fileExists((0, import_node_path12.join)(workspaceRoot, ".prettierrc"));
2082
+ return {
2083
+ check: "Prettier config is mnci\u2019s",
2084
+ ok: !strayExists,
2085
+ detail: ".prettierrc exists and outranks .prettierrc.json, so mnci\u2019s config is ignored",
2086
+ remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
2087
+ };
2088
+ }
2089
+ function checkEslintPlugin(nxJson) {
2090
+ const plugins = nxJson.plugins ?? [];
2091
+ const registered = plugins.some(
2092
+ (entry) => typeof entry === "string" ? entry === "@nx/eslint/plugin" : entry.plugin === "@nx/eslint/plugin"
2093
+ );
2094
+ return {
2095
+ check: "@nx/eslint/plugin registered",
2096
+ ok: registered,
2097
+ detail: "not in nx.json plugins \u2014 every project silently loses its lint target",
2098
+ remedy: "run `mnci upgrade`"
2099
+ };
2100
+ }
2101
+ function checkResolvedEslint(workspaceRoot) {
2102
+ const manifestPath = (0, import_node_path12.join)(workspaceRoot, "node_modules/eslint/package.json");
2103
+ if (!fileExists(manifestPath)) {
2104
+ return void 0;
2105
+ }
2106
+ try {
2107
+ const { version } = readJson(manifestPath);
2108
+ const major = version.split(".")[0];
2109
+ return {
2110
+ check: `resolved eslint is ${SUPPORTED_ESLINT_MAJOR}.x`,
2111
+ ok: major === SUPPORTED_ESLINT_MAJOR,
2112
+ detail: `node_modules/eslint is ${version}, but this stack supports ${SUPPORTED_ESLINT_MAJOR}.x (eslint-plugin-react has no release beyond it)`,
2113
+ remedy: `pin eslint to ${ESLINT_VERSION} in every package manifest, then reinstall`
2114
+ };
2115
+ } catch {
2116
+ return void 0;
2117
+ }
2118
+ }
2119
+ function checkNpmrc(workspaceRoot, registry, scope) {
2120
+ const npmrcPath = (0, import_node_path12.join)(workspaceRoot, ".npmrc");
2121
+ if (!registry || !fileExists(npmrcPath)) {
2122
+ return void 0;
2123
+ }
2124
+ const npmrc = (0, import_node_fs7.readFileSync)(npmrcPath, "utf8");
2125
+ if (registry.kind === "npm") {
2126
+ return {
2127
+ check: ".npmrc authenticates the public registry",
2128
+ ok: npmrc.includes("//registry.npmjs.org/:_authToken="),
2129
+ detail: "no npmjs.org token line \u2014 `npm publish` cannot authenticate",
2130
+ remedy: "run `mnci upgrade`"
2131
+ };
2132
+ }
2133
+ return {
2134
+ check: ".npmrc routes the scope to the feed",
2135
+ ok: scope !== void 0 && npmrc.includes(`${scope}:registry=`),
2136
+ detail: `no '${scope ?? "@scope"}:registry=' line \u2014 a scoped package could publish to npmjs.org instead of the feed`,
2137
+ remedy: "run `mnci upgrade`"
2138
+ };
2139
+ }
2140
+ function checkVersionActions(workspaceRoot) {
2141
+ const candidates = [
2142
+ ...(0, import_node_fs7.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
2143
+ ...(0, import_node_fs7.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
2144
+ ];
2145
+ return candidates.flatMap((manifest) => {
2146
+ const projectRoot = manifest.slice(0, manifest.lastIndexOf("/"));
2147
+ const projectJsonPath = (0, import_node_path12.join)(workspaceRoot, projectRoot, "project.json");
2148
+ let hasOverride = false;
2149
+ try {
2150
+ const projectJson = readJson(projectJsonPath);
2151
+ hasOverride = Boolean(projectJson.release?.version?.versionActions);
2152
+ } catch {
2153
+ hasOverride = false;
2154
+ }
2155
+ return [
2156
+ {
2157
+ check: `${projectRoot} keeps its versionActions override`,
2158
+ ok: hasOverride,
2159
+ detail: "missing \u2014 nx release aborts for the ENTIRE workspace, not just this project",
2160
+ remedy: `add release.version.versionActions to ${projectRoot}/project.json`
2161
+ }
2162
+ ];
2163
+ });
2164
+ }
2165
+ function checkSync(workspaceRoot) {
2166
+ return {
2167
+ check: "TypeScript project references synced",
2168
+ ok: runShell("npx", ["nx", "sync:check"], workspaceRoot) === 0,
2169
+ detail: "nx sync:check failed \u2014 a stale project reference was never committed",
2170
+ remedy: "run `npx nx sync` and commit the result"
2171
+ };
2172
+ }
2173
+ function collectFindings(workspaceRoot) {
2174
+ const nxJsonPath = (0, import_node_path12.join)(workspaceRoot, "nx.json");
2175
+ if (!fileExists(nxJsonPath)) {
2176
+ throw new Error(
2177
+ `No nx.json found in ${workspaceRoot} \u2014 run 'mnci doctor' from the workspace root.`
2178
+ );
2179
+ }
2180
+ const nxJson = readJson(nxJsonPath);
2181
+ return [
2182
+ ...checkEslintConfigs(workspaceRoot),
2183
+ checkPrettierConfig(workspaceRoot),
2184
+ checkEslintPlugin(nxJson),
2185
+ checkResolvedEslint(workspaceRoot),
2186
+ checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
2187
+ ...checkVersionActions(workspaceRoot),
2188
+ checkSync(workspaceRoot)
2189
+ ].filter((finding) => finding !== void 0);
2190
+ }
2191
+ function runDoctor(workspaceRoot) {
2192
+ const findings = collectFindings(workspaceRoot);
2193
+ const failed = findings.filter((finding) => !finding.ok);
2194
+ for (const finding of findings) {
2195
+ if (finding.ok) {
2196
+ logger.success(finding.check);
2197
+ } else {
2198
+ logger.error(`${finding.check} \u2014 ${finding.detail ?? "failed"}`);
2199
+ if (finding.remedy) {
2200
+ logger.info(` fix: ${finding.remedy}`);
2201
+ }
2202
+ }
2203
+ }
2204
+ if (failed.length === 0) {
2205
+ logger.success(`All ${findings.length} checks passed.`);
2206
+ return;
2207
+ }
2208
+ logger.error(`${failed.length} of ${findings.length} checks failed.`);
2209
+ process.exitCode = 1;
2210
+ }
2211
+
2212
+ // src/commands/interactive.ts
2213
+ var import_node_path15 = require("path");
2214
+ var import_prompts8 = require("@inquirer/prompts");
2215
+
2056
2216
  // src/commands/new.ts
2217
+ var import_node_path13 = require("path");
2057
2218
  function nxCloudProviderValue(ci) {
2058
2219
  return ci === "azure" ? "azure" : "github";
2059
2220
  }
@@ -2118,7 +2279,7 @@ async function runNew(name, options) {
2118
2279
  ],
2119
2280
  process.cwd()
2120
2281
  );
2121
- const workspaceRoot = (0, import_node_path12.join)(process.cwd(), workspaceName);
2282
+ const workspaceRoot = (0, import_node_path13.join)(process.cwd(), workspaceName);
2122
2283
  logger.step(
2123
2284
  "Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
2124
2285
  );
@@ -2152,8 +2313,8 @@ async function runNew(name, options) {
2152
2313
  }
2153
2314
 
2154
2315
  // src/commands/upgrade.ts
2155
- var import_node_fs7 = require("fs");
2156
- var import_node_path13 = require("path");
2316
+ var import_node_fs8 = require("fs");
2317
+ var import_node_path14 = require("path");
2157
2318
  function resolveRegistry2(options, persisted) {
2158
2319
  if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
2159
2320
  const persistedAzure = persisted?.kind === "azure-artifacts" ? persisted : void 0;
@@ -2182,7 +2343,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
2182
2343
  return persisted.workspaceName;
2183
2344
  }
2184
2345
  try {
2185
- const existing = (0, import_node_fs7.readdirSync)(workspaceRoot).find(
2346
+ const existing = (0, import_node_fs8.readdirSync)(workspaceRoot).find(
2186
2347
  (file) => file.endsWith(".code-workspace") && file !== "undefined.code-workspace"
2187
2348
  );
2188
2349
  if (existing) {
@@ -2190,7 +2351,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
2190
2351
  }
2191
2352
  } catch {
2192
2353
  }
2193
- return (0, import_node_path13.basename)(workspaceRoot);
2354
+ return (0, import_node_path14.basename)(workspaceRoot);
2194
2355
  }
2195
2356
  function resolveOverlayOptions(workspaceRoot, options, persisted) {
2196
2357
  const scope = options.scope ?? persisted.scope;
@@ -2228,7 +2389,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
2228
2389
  };
2229
2390
  }
2230
2391
  function runUpgrade(workspaceRoot, options) {
2231
- if (!fileExists((0, import_node_path13.join)(workspaceRoot, "nx.json"))) {
2392
+ if (!fileExists((0, import_node_path14.join)(workspaceRoot, "nx.json"))) {
2232
2393
  throw new Error(
2233
2394
  `No nx.json found in ${workspaceRoot} \u2014 this does not look like an Nx workspace. Run 'mnci upgrade' from the workspace root.`
2234
2395
  );
@@ -2245,7 +2406,7 @@ function runUpgrade(workspaceRoot, options) {
2245
2406
 
2246
2407
  // src/commands/interactive.ts
2247
2408
  async function runInteractive() {
2248
- const inWorkspace = fileExists((0, import_node_path14.join)(process.cwd(), "nx.json"));
2409
+ const inWorkspace = fileExists((0, import_node_path15.join)(process.cwd(), "nx.json"));
2249
2410
  const newChoice = { name: "Create a new monorepo", value: "new" };
2250
2411
  const addChoice = { name: "Add a project to this workspace", value: "add" };
2251
2412
  const upgradeChoice = {
@@ -2266,8 +2427,8 @@ async function runInteractive() {
2266
2427
  }
2267
2428
 
2268
2429
  // src/util/versionChecker.ts
2269
- var import_node_fs8 = require("fs");
2270
- var import_node_path15 = require("path");
2430
+ var import_node_fs9 = require("fs");
2431
+ var import_node_path16 = require("path");
2271
2432
  var import_cross_spawn2 = __toESM(require("cross-spawn"));
2272
2433
  var PACKAGE_NAME = "@mnci/cli";
2273
2434
  var REGISTRY_TIMEOUT_MS = 2e3;
@@ -2323,8 +2484,8 @@ function releaseTriple(version) {
2323
2484
  }
2324
2485
  function readCliVersion(packageDirectory) {
2325
2486
  try {
2326
- const manifestPath = (0, import_node_path15.join)(packageDirectory, "..", "package.json");
2327
- const manifest = JSON.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf8"));
2487
+ const manifestPath = (0, import_node_path16.join)(packageDirectory, "..", "package.json");
2488
+ const manifest = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
2328
2489
  return manifest.version ?? "0.0.0";
2329
2490
  } catch {
2330
2491
  return "0.0.0";
@@ -2349,6 +2510,11 @@ function buildProgram(cliVersion) {
2349
2510
  ).action((options) => {
2350
2511
  runUpgrade(process.cwd(), options);
2351
2512
  });
2513
+ program.command("doctor").description(
2514
+ "Check this workspace against the invariants mnci maintains (one ESLint config, no stray .prettierrc, the eslint plugin registered, the resolved eslint major, .npmrc vs the recorded registry, versionActions overrides, nx sync) \u2014 read-only; exits non-zero if anything failed"
2515
+ ).action(() => {
2516
+ runDoctor(process.cwd());
2517
+ });
2352
2518
  program.command("add").addArgument(new import_commander.Argument("[kind]", "project kind").choices(PROJECT_KINDS)).argument("[name]", "project name").description("Add a project by delegating to the matching Nx plugin generator").option("--scope <scope>", "npm scope for a publishable lib (defaults to @<workspace name>)").option(
2353
2519
  "--framework <framework>",
2354
2520
  "node-app only: express | fastify | koa | nest | none (default: none)"
@@ -2382,7 +2548,7 @@ async function main() {
2382
2548
  }
2383
2549
  }
2384
2550
  if (require.main === module) {
2385
- main();
2551
+ void main();
2386
2552
  }
2387
2553
  // Annotate the CommonJS export names for ESM import in node:
2388
2554
  0 && (module.exports = {