@mnci/cli 1.13.0 → 1.15.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/README.md +64 -9
- package/dist/cli.js +265 -23
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
|
@@ -126,7 +148,7 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
|
|
|
126
148
|
is ever pushed to `main`; future runs resolve versions from tag names.
|
|
127
149
|
3. Writes `eslint.config.mjs` (three lines importing `@mnci/eslint-config` —
|
|
128
150
|
the whole linting opinion, in one root config), `.prettierrc.json` +
|
|
129
|
-
`.prettierignore`, `.npmrc` (
|
|
151
|
+
`.prettierignore`, `.npmrc` (publish auth — see **Publish auth** below),
|
|
130
152
|
`commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
|
|
131
153
|
pipeline file(s)
|
|
132
154
|
(`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
|
|
@@ -404,6 +426,42 @@ lint,test,build`. Pushes to `main` then:
|
|
|
404
426
|
_only_ configured provider, since that's the one case a `GITHUB_TOKEN` is
|
|
405
427
|
guaranteed to exist.
|
|
406
428
|
|
|
429
|
+
### Publish auth
|
|
430
|
+
|
|
431
|
+
The generated `.npmrc` differs by registry kind, because the honest answer does.
|
|
432
|
+
|
|
433
|
+
**`--registry azure-artifacts`** routes the workspace's own scope to the feed and
|
|
434
|
+
supplies the feed's credentials:
|
|
435
|
+
|
|
436
|
+
```ini
|
|
437
|
+
@my:registry=https://pkgs.dev.azure.com/<org>/<proj>/_packaging/<feed>/npm/registry/
|
|
438
|
+
//pkgs.dev.azure.com/.../npm/registry/:username=AzureArtifacts
|
|
439
|
+
//pkgs.dev.azure.com/.../npm/registry/:_password=${PAT}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Scope routing is **real protection** here, not decoration: npm prefers a scope's
|
|
443
|
+
registry over the global one when publishing a scoped package, so `@my/*` cannot
|
|
444
|
+
reach npmjs.org by accident. Verified against a real registry — with only the
|
|
445
|
+
scope line set, npm reports `Publishing to <feed>` — and again from a generated
|
|
446
|
+
workspace, whose `npm publish --dry-run` targets the feed.
|
|
447
|
+
|
|
448
|
+
Only the scope is routed, deliberately. A global `registry=` would push every
|
|
449
|
+
install through the feed as well, so `npm ci` would need feed auth just to fetch
|
|
450
|
+
public packages; as generated, public dependencies still come from npmjs.org and
|
|
451
|
+
a developer with no `PAT` set can install normally.
|
|
452
|
+
|
|
453
|
+
**`--registry npm`** gets the auth line and nothing else:
|
|
454
|
+
|
|
455
|
+
```ini
|
|
456
|
+
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
There is no `@scope:registry` line, and the generated file explains why: npmjs.org
|
|
460
|
+
is already the default, so routing the scope there changes nothing — and calling
|
|
461
|
+
it protection against an accidental public publish would be false, because the
|
|
462
|
+
public registry is the intended target. Worth stating plainly because this file
|
|
463
|
+
previously claimed exactly that protection while emitting no routing line at all.
|
|
464
|
+
|
|
407
465
|
**npm auth** is the base64 `PAT`, read the same way on both providers but from
|
|
408
466
|
a different place: on Azure Pipelines, a **variable group**
|
|
409
467
|
(`--variable-group`, default `Build`) exposes it as `$(PAT)`; on GitHub
|
|
@@ -516,13 +574,10 @@ than a surprise:
|
|
|
516
574
|
|
|
517
575
|
## Known gaps (accepted for the experiment)
|
|
518
576
|
|
|
519
|
-
- **
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
than ship half-wired auth, the CI token export (`NODE_AUTH_TOKEN`/`PAT`) is
|
|
524
|
-
left in place so wiring it later is one line, and the generated `.npmrc`
|
|
525
|
-
carries a comment saying so. Publish-path e2e assertions are relaxed to match.
|
|
577
|
+
- **A scoped package on public npm has no accidental-publish protection**, and
|
|
578
|
+
cannot: npmjs.org is the intended target there, so no `.npmrc` line could
|
|
579
|
+
prevent it. Generate with `--registry azure-artifacts` if you need a scope kept
|
|
580
|
+
off the public registry — that variant routes it (see **Publish auth** above).
|
|
526
581
|
- No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
|
|
527
582
|
- **Flutter apps build for web only.** Android would require the Android SDK and
|
|
528
583
|
NDK on every build agent; iOS is impossible on a Linux agent regardless. Add
|
package/dist/cli.js
CHANGED
|
@@ -1234,11 +1234,8 @@ function readWorkspaceStack(workspaceRoot) {
|
|
|
1234
1234
|
};
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
-
// src/commands/
|
|
1238
|
-
var
|
|
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
|
|
@@ -1250,15 +1247,49 @@ var TS_COMPILER_DEPENDENCIES = {
|
|
|
1250
1247
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
1251
1248
|
typescript: "npm:@typescript/typescript6@^6.0.2"
|
|
1252
1249
|
};
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1250
|
+
function registryUrl(registry) {
|
|
1251
|
+
if (registry.kind === "azure-artifacts") {
|
|
1252
|
+
return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
|
|
1253
|
+
}
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
function npmrcContent(registry, scope) {
|
|
1257
|
+
if (registry.kind === "npm") {
|
|
1258
|
+
return `; Publish authentication for the public npm registry.
|
|
1255
1259
|
;
|
|
1256
|
-
;
|
|
1257
|
-
;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
+
; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
|
|
1261
|
+
; is harmless locally, so no token is needed for day-to-day work.
|
|
1262
|
+
//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
|
|
1263
|
+
|
|
1264
|
+
; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
|
|
1265
|
+
; the default registry, so routing the scope to it would change nothing \u2014 and
|
|
1266
|
+
; presenting that as protection against an accidental public publish would be
|
|
1267
|
+
; false, because the public registry is the intended target. (An earlier version
|
|
1268
|
+
; of this file made exactly that claim while emitting no routing line at all.)
|
|
1269
|
+
; To keep a scoped package off npmjs.org, use a private feed: generate with
|
|
1270
|
+
; --registry azure-artifacts, which does route the scope.
|
|
1271
|
+
`;
|
|
1272
|
+
}
|
|
1273
|
+
const feedUrl = registryUrl(registry);
|
|
1274
|
+
const feedKey = feedUrl.replace(/^https:/, "");
|
|
1275
|
+
return `; Publish + resolution routing for this workspace's own scope.
|
|
1276
|
+
;
|
|
1277
|
+
; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
|
|
1278
|
+
; feed: npm prefers a scope's registry over the global one when publishing a
|
|
1279
|
+
; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
|
|
1260
1280
|
;
|
|
1261
|
-
;
|
|
1281
|
+
; Only the scope is routed, on purpose. A global 'registry=' would send every
|
|
1282
|
+
; install through the feed as well, so 'npm ci' would need feed auth just to fetch
|
|
1283
|
+
; public packages.
|
|
1284
|
+
${scope}:registry=${feedUrl}
|
|
1285
|
+
|
|
1286
|
+
; Feed credentials. PAT is the BASE64 value Azure Artifacts' "Connect to feed"
|
|
1287
|
+
; instructions give you, which is exactly what _password expects, so it is used
|
|
1288
|
+
; as-is. (twine wants the RAW token; the CI release step decodes it there.)
|
|
1289
|
+
; Azure ignores the username, and npm requires an email it never uses.
|
|
1290
|
+
${feedKey}:username=AzureArtifacts
|
|
1291
|
+
${feedKey}:_password=\${PAT}
|
|
1292
|
+
${feedKey}:email=npm-requires-this-and-never-uses-it
|
|
1262
1293
|
`;
|
|
1263
1294
|
}
|
|
1264
1295
|
function releaseConfig(ci) {
|
|
@@ -1502,9 +1533,18 @@ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKin
|
|
|
1502
1533
|
# runs unchanged on Linux, macOS and Windows agents.
|
|
1503
1534
|
|
|
1504
1535
|
trigger:
|
|
1536
|
+
# While a run is in progress, queue further pushes and run them together rather
|
|
1537
|
+
# than concurrently. This is the closest thing Azure Pipelines YAML has to
|
|
1538
|
+
# GitHub's concurrency group, and it is load-bearing on main for the same
|
|
1539
|
+
# reason: two concurrent 'nx release' runs would race to create the same tag.
|
|
1540
|
+
batch: true
|
|
1505
1541
|
branches:
|
|
1506
1542
|
include: [main]
|
|
1507
1543
|
|
|
1544
|
+
# Note: cancelling a superseded PR validation run is NOT a YAML setting on Azure
|
|
1545
|
+
# \u2014 it lives in the branch policy ("Build validation -> automatically cancel").
|
|
1546
|
+
# Nothing here can express it, so it is left to whoever configures the policy
|
|
1547
|
+
# rather than faked with a batch setting that would only delay PR runs.
|
|
1508
1548
|
pr:
|
|
1509
1549
|
branches:
|
|
1510
1550
|
include: [main]
|
|
@@ -1517,6 +1557,12 @@ variables:
|
|
|
1517
1557
|
# for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
|
|
1518
1558
|
# for public npm. Mark it secret in Library. Add app build vars here too if needed.
|
|
1519
1559
|
- group: ${variableGroup}
|
|
1560
|
+
# Relocates npm's cache inside the pipeline workspace so Cache@2 can restore it
|
|
1561
|
+
# (the default ~/.npm is outside the cacheable area on hosted agents). npm reads
|
|
1562
|
+
# this as an ordinary env var, so it needs no config file and works on every
|
|
1563
|
+
# agent OS.
|
|
1564
|
+
- name: npm_config_cache
|
|
1565
|
+
value: $(Pipeline.Workspace)/.npm
|
|
1520
1566
|
|
|
1521
1567
|
steps:
|
|
1522
1568
|
- checkout: self
|
|
@@ -1541,6 +1587,20 @@ steps:
|
|
|
1541
1587
|
inputs:
|
|
1542
1588
|
version: 24.x
|
|
1543
1589
|
|
|
1590
|
+
# The Azure counterpart of \`cache: npm\` on actions/setup-node: restores npm's
|
|
1591
|
+
# download cache so \`npm ci\` does not re-fetch every tarball on every run.
|
|
1592
|
+
# Keyed on the lockfile, so a dependency change misses and anything else hits;
|
|
1593
|
+
# restoreKeys falls back to the newest cache for this OS on a miss, which still
|
|
1594
|
+
# avoids a fully cold install. Agent.OS is in the key because a cached native
|
|
1595
|
+
# module built for one OS is not reusable on another.
|
|
1596
|
+
- task: Cache@2
|
|
1597
|
+
displayName: Cache npm packages
|
|
1598
|
+
inputs:
|
|
1599
|
+
key: 'npm | "$(Agent.OS)" | package-lock.json'
|
|
1600
|
+
restoreKeys: |
|
|
1601
|
+
npm | "$(Agent.OS)"
|
|
1602
|
+
path: $(npm_config_cache)
|
|
1603
|
+
|
|
1544
1604
|
- script: npm ci
|
|
1545
1605
|
displayName: Install dependencies
|
|
1546
1606
|
env:
|
|
@@ -1698,6 +1758,19 @@ on:
|
|
|
1698
1758
|
pull_request:
|
|
1699
1759
|
branches: [main]
|
|
1700
1760
|
|
|
1761
|
+
concurrency:
|
|
1762
|
+
# One run at a time per ref, but \`cancel-in-progress\` is an **expression** on
|
|
1763
|
+
# purpose rather than a flat \`true\`.
|
|
1764
|
+
#
|
|
1765
|
+
# A superseded PR run is pure waste, so cancel it. A run on \`main\` must never
|
|
1766
|
+
# be cancelled: it publishes packages and pushes release tags, and killing it
|
|
1767
|
+
# part-way can leave a tag pushed with the publish only half done \u2014 a state no
|
|
1768
|
+
# rerun repairs cleanly, because the version is then already tagged. Those runs
|
|
1769
|
+
# queue instead, which also stops two \`nx release\` invocations racing to create
|
|
1770
|
+
# the same tag when two commits land close together.
|
|
1771
|
+
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
1772
|
+
cancel-in-progress: \${{ github.event_name == 'pull_request' }}
|
|
1773
|
+
|
|
1701
1774
|
permissions:
|
|
1702
1775
|
# Lets the release step push the version tag nx release creates back to main,
|
|
1703
1776
|
# and (github-only provider) create the GitHub Release itself.
|
|
@@ -1721,6 +1794,11 @@ jobs:
|
|
|
1721
1794
|
- uses: actions/setup-node@v4
|
|
1722
1795
|
with:
|
|
1723
1796
|
node-version: 24
|
|
1797
|
+
# Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
|
|
1798
|
+
# the local cache instead of re-downloading every tarball on every run.
|
|
1799
|
+
# Nothing to invalidate by hand: the action keys on the lockfile, so a
|
|
1800
|
+
# dependency change misses the cache and a no-op change hits it.
|
|
1801
|
+
cache: npm
|
|
1724
1802
|
|
|
1725
1803
|
- run: npm ci
|
|
1726
1804
|
name: Install dependencies
|
|
@@ -1972,7 +2050,166 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
1972
2050
|
}
|
|
1973
2051
|
}
|
|
1974
2052
|
|
|
2053
|
+
// src/commands/doctor.ts
|
|
2054
|
+
var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".")[0];
|
|
2055
|
+
function checkEslintConfigs(workspaceRoot) {
|
|
2056
|
+
const rootConfigs = (0, import_node_fs7.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
|
|
2057
|
+
const projectConfigs = (0, import_node_fs7.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
|
|
2058
|
+
cwd: workspaceRoot
|
|
2059
|
+
});
|
|
2060
|
+
return [
|
|
2061
|
+
{
|
|
2062
|
+
check: "root ESLint config",
|
|
2063
|
+
ok: rootConfigs.length === 1,
|
|
2064
|
+
detail: rootConfigs.length === 0 ? "no eslint.config.* at the workspace root" : `${rootConfigs.length} root configs: ${rootConfigs.join(", ")}`,
|
|
2065
|
+
remedy: "run `mnci upgrade` to rewrite the root config"
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
check: "no per-project ESLint configs",
|
|
2069
|
+
ok: projectConfigs.length === 0,
|
|
2070
|
+
detail: `found ${projectConfigs.length}: ${projectConfigs.join(", ")}`,
|
|
2071
|
+
remedy: "run `mnci upgrade`, which sweeps {apps,libs,packages}/*/eslint.config.*"
|
|
2072
|
+
}
|
|
2073
|
+
];
|
|
2074
|
+
}
|
|
2075
|
+
function checkPrettierConfig(workspaceRoot) {
|
|
2076
|
+
const strayExists = fileExists((0, import_node_path12.join)(workspaceRoot, ".prettierrc"));
|
|
2077
|
+
return {
|
|
2078
|
+
check: "Prettier config is mnci\u2019s",
|
|
2079
|
+
ok: !strayExists,
|
|
2080
|
+
detail: ".prettierrc exists and outranks .prettierrc.json, so mnci\u2019s config is ignored",
|
|
2081
|
+
remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function checkEslintPlugin(nxJson) {
|
|
2085
|
+
const plugins = nxJson.plugins ?? [];
|
|
2086
|
+
const registered = plugins.some(
|
|
2087
|
+
(entry) => typeof entry === "string" ? entry === "@nx/eslint/plugin" : entry.plugin === "@nx/eslint/plugin"
|
|
2088
|
+
);
|
|
2089
|
+
return {
|
|
2090
|
+
check: "@nx/eslint/plugin registered",
|
|
2091
|
+
ok: registered,
|
|
2092
|
+
detail: "not in nx.json plugins \u2014 every project silently loses its lint target",
|
|
2093
|
+
remedy: "run `mnci upgrade`"
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
function checkResolvedEslint(workspaceRoot) {
|
|
2097
|
+
const manifestPath = (0, import_node_path12.join)(workspaceRoot, "node_modules/eslint/package.json");
|
|
2098
|
+
if (!fileExists(manifestPath)) {
|
|
2099
|
+
return void 0;
|
|
2100
|
+
}
|
|
2101
|
+
try {
|
|
2102
|
+
const { version } = readJson(manifestPath);
|
|
2103
|
+
const major = version.split(".")[0];
|
|
2104
|
+
return {
|
|
2105
|
+
check: `resolved eslint is ${SUPPORTED_ESLINT_MAJOR}.x`,
|
|
2106
|
+
ok: major === SUPPORTED_ESLINT_MAJOR,
|
|
2107
|
+
detail: `node_modules/eslint is ${version}, but this stack supports ${SUPPORTED_ESLINT_MAJOR}.x (eslint-plugin-react has no release beyond it)`,
|
|
2108
|
+
remedy: `pin eslint to ${ESLINT_VERSION} in every package manifest, then reinstall`
|
|
2109
|
+
};
|
|
2110
|
+
} catch {
|
|
2111
|
+
return void 0;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function checkNpmrc(workspaceRoot, registry, scope) {
|
|
2115
|
+
const npmrcPath = (0, import_node_path12.join)(workspaceRoot, ".npmrc");
|
|
2116
|
+
if (!registry || !fileExists(npmrcPath)) {
|
|
2117
|
+
return void 0;
|
|
2118
|
+
}
|
|
2119
|
+
const npmrc = (0, import_node_fs7.readFileSync)(npmrcPath, "utf8");
|
|
2120
|
+
if (registry.kind === "npm") {
|
|
2121
|
+
return {
|
|
2122
|
+
check: ".npmrc authenticates the public registry",
|
|
2123
|
+
ok: npmrc.includes("//registry.npmjs.org/:_authToken="),
|
|
2124
|
+
detail: "no npmjs.org token line \u2014 `npm publish` cannot authenticate",
|
|
2125
|
+
remedy: "run `mnci upgrade`"
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
return {
|
|
2129
|
+
check: ".npmrc routes the scope to the feed",
|
|
2130
|
+
ok: scope !== void 0 && npmrc.includes(`${scope}:registry=`),
|
|
2131
|
+
detail: `no '${scope ?? "@scope"}:registry=' line \u2014 a scoped package could publish to npmjs.org instead of the feed`,
|
|
2132
|
+
remedy: "run `mnci upgrade`"
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function checkVersionActions(workspaceRoot) {
|
|
2136
|
+
const candidates = [
|
|
2137
|
+
...(0, import_node_fs7.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
|
|
2138
|
+
...(0, import_node_fs7.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
|
|
2139
|
+
];
|
|
2140
|
+
return candidates.flatMap((manifest) => {
|
|
2141
|
+
const projectRoot = manifest.slice(0, manifest.lastIndexOf("/"));
|
|
2142
|
+
const projectJsonPath = (0, import_node_path12.join)(workspaceRoot, projectRoot, "project.json");
|
|
2143
|
+
let hasOverride = false;
|
|
2144
|
+
try {
|
|
2145
|
+
const projectJson = readJson(projectJsonPath);
|
|
2146
|
+
hasOverride = Boolean(projectJson.release?.version?.versionActions);
|
|
2147
|
+
} catch {
|
|
2148
|
+
hasOverride = false;
|
|
2149
|
+
}
|
|
2150
|
+
return [
|
|
2151
|
+
{
|
|
2152
|
+
check: `${projectRoot} keeps its versionActions override`,
|
|
2153
|
+
ok: hasOverride,
|
|
2154
|
+
detail: "missing \u2014 nx release aborts for the ENTIRE workspace, not just this project",
|
|
2155
|
+
remedy: `add release.version.versionActions to ${projectRoot}/project.json`
|
|
2156
|
+
}
|
|
2157
|
+
];
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
function checkSync(workspaceRoot) {
|
|
2161
|
+
return {
|
|
2162
|
+
check: "TypeScript project references synced",
|
|
2163
|
+
ok: runShell("npx", ["nx", "sync:check"], workspaceRoot) === 0,
|
|
2164
|
+
detail: "nx sync:check failed \u2014 a stale project reference was never committed",
|
|
2165
|
+
remedy: "run `npx nx sync` and commit the result"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function collectFindings(workspaceRoot) {
|
|
2169
|
+
const nxJsonPath = (0, import_node_path12.join)(workspaceRoot, "nx.json");
|
|
2170
|
+
if (!fileExists(nxJsonPath)) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`No nx.json found in ${workspaceRoot} \u2014 run 'mnci doctor' from the workspace root.`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
const nxJson = readJson(nxJsonPath);
|
|
2176
|
+
return [
|
|
2177
|
+
...checkEslintConfigs(workspaceRoot),
|
|
2178
|
+
checkPrettierConfig(workspaceRoot),
|
|
2179
|
+
checkEslintPlugin(nxJson),
|
|
2180
|
+
checkResolvedEslint(workspaceRoot),
|
|
2181
|
+
checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
|
|
2182
|
+
...checkVersionActions(workspaceRoot),
|
|
2183
|
+
checkSync(workspaceRoot)
|
|
2184
|
+
].filter((finding) => finding !== void 0);
|
|
2185
|
+
}
|
|
2186
|
+
function runDoctor(workspaceRoot) {
|
|
2187
|
+
const findings = collectFindings(workspaceRoot);
|
|
2188
|
+
const failed = findings.filter((finding) => !finding.ok);
|
|
2189
|
+
for (const finding of findings) {
|
|
2190
|
+
if (finding.ok) {
|
|
2191
|
+
logger.success(finding.check);
|
|
2192
|
+
} else {
|
|
2193
|
+
logger.error(`${finding.check} \u2014 ${finding.detail ?? "failed"}`);
|
|
2194
|
+
if (finding.remedy) {
|
|
2195
|
+
logger.info(` fix: ${finding.remedy}`);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (failed.length === 0) {
|
|
2200
|
+
logger.success(`All ${findings.length} checks passed.`);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
logger.error(`${failed.length} of ${findings.length} checks failed.`);
|
|
2204
|
+
process.exitCode = 1;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/commands/interactive.ts
|
|
2208
|
+
var import_node_path15 = require("path");
|
|
2209
|
+
var import_prompts8 = require("@inquirer/prompts");
|
|
2210
|
+
|
|
1975
2211
|
// src/commands/new.ts
|
|
2212
|
+
var import_node_path13 = require("path");
|
|
1976
2213
|
function nxCloudProviderValue(ci) {
|
|
1977
2214
|
return ci === "azure" ? "azure" : "github";
|
|
1978
2215
|
}
|
|
@@ -2037,7 +2274,7 @@ async function runNew(name, options) {
|
|
|
2037
2274
|
],
|
|
2038
2275
|
process.cwd()
|
|
2039
2276
|
);
|
|
2040
|
-
const workspaceRoot = (0,
|
|
2277
|
+
const workspaceRoot = (0, import_node_path13.join)(process.cwd(), workspaceName);
|
|
2041
2278
|
logger.step(
|
|
2042
2279
|
"Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
|
|
2043
2280
|
);
|
|
@@ -2071,8 +2308,8 @@ async function runNew(name, options) {
|
|
|
2071
2308
|
}
|
|
2072
2309
|
|
|
2073
2310
|
// src/commands/upgrade.ts
|
|
2074
|
-
var
|
|
2075
|
-
var
|
|
2311
|
+
var import_node_fs8 = require("fs");
|
|
2312
|
+
var import_node_path14 = require("path");
|
|
2076
2313
|
function resolveRegistry2(options, persisted) {
|
|
2077
2314
|
if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
|
|
2078
2315
|
const persistedAzure = persisted?.kind === "azure-artifacts" ? persisted : void 0;
|
|
@@ -2101,7 +2338,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
|
|
|
2101
2338
|
return persisted.workspaceName;
|
|
2102
2339
|
}
|
|
2103
2340
|
try {
|
|
2104
|
-
const existing = (0,
|
|
2341
|
+
const existing = (0, import_node_fs8.readdirSync)(workspaceRoot).find(
|
|
2105
2342
|
(file) => file.endsWith(".code-workspace") && file !== "undefined.code-workspace"
|
|
2106
2343
|
);
|
|
2107
2344
|
if (existing) {
|
|
@@ -2109,7 +2346,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
|
|
|
2109
2346
|
}
|
|
2110
2347
|
} catch {
|
|
2111
2348
|
}
|
|
2112
|
-
return (0,
|
|
2349
|
+
return (0, import_node_path14.basename)(workspaceRoot);
|
|
2113
2350
|
}
|
|
2114
2351
|
function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
2115
2352
|
const scope = options.scope ?? persisted.scope;
|
|
@@ -2147,7 +2384,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
2147
2384
|
};
|
|
2148
2385
|
}
|
|
2149
2386
|
function runUpgrade(workspaceRoot, options) {
|
|
2150
|
-
if (!fileExists((0,
|
|
2387
|
+
if (!fileExists((0, import_node_path14.join)(workspaceRoot, "nx.json"))) {
|
|
2151
2388
|
throw new Error(
|
|
2152
2389
|
`No nx.json found in ${workspaceRoot} \u2014 this does not look like an Nx workspace. Run 'mnci upgrade' from the workspace root.`
|
|
2153
2390
|
);
|
|
@@ -2164,7 +2401,7 @@ function runUpgrade(workspaceRoot, options) {
|
|
|
2164
2401
|
|
|
2165
2402
|
// src/commands/interactive.ts
|
|
2166
2403
|
async function runInteractive() {
|
|
2167
|
-
const inWorkspace = fileExists((0,
|
|
2404
|
+
const inWorkspace = fileExists((0, import_node_path15.join)(process.cwd(), "nx.json"));
|
|
2168
2405
|
const newChoice = { name: "Create a new monorepo", value: "new" };
|
|
2169
2406
|
const addChoice = { name: "Add a project to this workspace", value: "add" };
|
|
2170
2407
|
const upgradeChoice = {
|
|
@@ -2185,8 +2422,8 @@ async function runInteractive() {
|
|
|
2185
2422
|
}
|
|
2186
2423
|
|
|
2187
2424
|
// src/util/versionChecker.ts
|
|
2188
|
-
var
|
|
2189
|
-
var
|
|
2425
|
+
var import_node_fs9 = require("fs");
|
|
2426
|
+
var import_node_path16 = require("path");
|
|
2190
2427
|
var import_cross_spawn2 = __toESM(require("cross-spawn"));
|
|
2191
2428
|
var PACKAGE_NAME = "@mnci/cli";
|
|
2192
2429
|
var REGISTRY_TIMEOUT_MS = 2e3;
|
|
@@ -2242,8 +2479,8 @@ function releaseTriple(version) {
|
|
|
2242
2479
|
}
|
|
2243
2480
|
function readCliVersion(packageDirectory) {
|
|
2244
2481
|
try {
|
|
2245
|
-
const manifestPath = (0,
|
|
2246
|
-
const manifest = JSON.parse((0,
|
|
2482
|
+
const manifestPath = (0, import_node_path16.join)(packageDirectory, "..", "package.json");
|
|
2483
|
+
const manifest = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
|
|
2247
2484
|
return manifest.version ?? "0.0.0";
|
|
2248
2485
|
} catch {
|
|
2249
2486
|
return "0.0.0";
|
|
@@ -2268,6 +2505,11 @@ function buildProgram(cliVersion) {
|
|
|
2268
2505
|
).action((options) => {
|
|
2269
2506
|
runUpgrade(process.cwd(), options);
|
|
2270
2507
|
});
|
|
2508
|
+
program.command("doctor").description(
|
|
2509
|
+
"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"
|
|
2510
|
+
).action(() => {
|
|
2511
|
+
runDoctor(process.cwd());
|
|
2512
|
+
});
|
|
2271
2513
|
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(
|
|
2272
2514
|
"--framework <framework>",
|
|
2273
2515
|
"node-app only: express | fastify | koa | nest | none (default: none)"
|