@mnci/cli 1.12.2 → 1.14.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 CHANGED
@@ -38,6 +38,8 @@ mnci add node-app api --framework express # ...or fastify | koa | nest
38
38
  mnci add node-function-app api # @nx/node + an Azure Functions v4 overlay
39
39
  mnci add npm-lib sdk # @nx/js publishable lib -> packages/
40
40
  mnci add internal-lib utils # @nx/js private lib -> libs/
41
+ mnci add react-lib ui # @nx/react publishable component lib -> packages/
42
+ mnci add react-internal-lib design # @nx/react private component lib -> libs/
41
43
 
42
44
  # Python (@mnci/nx-python-pip — pip + Ruff + pytest + PyPA build/twine, no uv)
43
45
  mnci add python-app svc # app -> apps/ (wheel, zipped into the drop)
@@ -124,7 +126,7 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
124
126
  is ever pushed to `main`; future runs resolve versions from tag names.
125
127
  3. Writes `eslint.config.mjs` (three lines importing `@mnci/eslint-config` —
126
128
  the whole linting opinion, in one root config), `.prettierrc.json` +
127
- `.prettierignore`, `.npmrc` (comment-only — see **Known gaps** below),
129
+ `.prettierignore`, `.npmrc` (publish auth — see **Publish auth** below),
128
130
  `commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
129
131
  pipeline file(s)
130
132
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
@@ -402,6 +404,42 @@ lint,test,build`. Pushes to `main` then:
402
404
  _only_ configured provider, since that's the one case a `GITHUB_TOKEN` is
403
405
  guaranteed to exist.
404
406
 
407
+ ### Publish auth
408
+
409
+ The generated `.npmrc` differs by registry kind, because the honest answer does.
410
+
411
+ **`--registry azure-artifacts`** routes the workspace's own scope to the feed and
412
+ supplies the feed's credentials:
413
+
414
+ ```ini
415
+ @my:registry=https://pkgs.dev.azure.com/<org>/<proj>/_packaging/<feed>/npm/registry/
416
+ //pkgs.dev.azure.com/.../npm/registry/:username=AzureArtifacts
417
+ //pkgs.dev.azure.com/.../npm/registry/:_password=${PAT}
418
+ ```
419
+
420
+ Scope routing is **real protection** here, not decoration: npm prefers a scope's
421
+ registry over the global one when publishing a scoped package, so `@my/*` cannot
422
+ reach npmjs.org by accident. Verified against a real registry — with only the
423
+ scope line set, npm reports `Publishing to <feed>` — and again from a generated
424
+ workspace, whose `npm publish --dry-run` targets the feed.
425
+
426
+ Only the scope is routed, deliberately. A global `registry=` would push every
427
+ install through the feed as well, so `npm ci` would need feed auth just to fetch
428
+ public packages; as generated, public dependencies still come from npmjs.org and
429
+ a developer with no `PAT` set can install normally.
430
+
431
+ **`--registry npm`** gets the auth line and nothing else:
432
+
433
+ ```ini
434
+ //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
435
+ ```
436
+
437
+ There is no `@scope:registry` line, and the generated file explains why: npmjs.org
438
+ is already the default, so routing the scope there changes nothing — and calling
439
+ it protection against an accidental public publish would be false, because the
440
+ public registry is the intended target. Worth stating plainly because this file
441
+ previously claimed exactly that protection while emitting no routing line at all.
442
+
405
443
  **npm auth** is the base64 `PAT`, read the same way on both providers but from
406
444
  a different place: on Azure Pipelines, a **variable group**
407
445
  (`--variable-group`, default `Build`) exposes it as `$(PAT)`; on GitHub
@@ -514,13 +552,10 @@ than a surprise:
514
552
 
515
553
  ## Known gaps (accepted for the experiment)
516
554
 
517
- - **The generated `.npmrc` is empty (comments only), so `npm publish` will not
518
- authenticate.** This is a deliberate deferral, not an oversight: the previous
519
- file claimed here that scope routing made accidental public publishes
520
- impossible, when in fact no `@scope:registry` line was ever emitted. Rather
521
- than ship half-wired auth, the CI token export (`NODE_AUTH_TOKEN`/`PAT`) is
522
- left in place so wiring it later is one line, and the generated `.npmrc`
523
- carries a comment saying so. Publish-path e2e assertions are relaxed to match.
555
+ - **A scoped package on public npm has no accidental-publish protection**, and
556
+ cannot: npmjs.org is the intended target there, so no `.npmrc` line could
557
+ prevent it. Generate with `--registry azure-artifacts` if you need a scope kept
558
+ off the public registry — that variant routes it (see **Publish auth** above).
524
559
  - No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
525
560
  - **Flutter apps build for web only.** Android would require the Android SDK and
526
561
  NDK on every build agent; iOS is impossible on a Linux agent regardless. Add
package/dist/cli.js CHANGED
@@ -38,8 +38,8 @@ module.exports = __toCommonJS(cli_exports);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/add.ts
41
- var import_node_path9 = require("path");
42
- var import_prompts4 = require("@inquirer/prompts");
41
+ var import_node_path10 = require("path");
42
+ var import_prompts5 = require("@inquirer/prompts");
43
43
 
44
44
  // src/nx.ts
45
45
  var import_cross_spawn = __toESM(require("cross-spawn"));
@@ -192,6 +192,21 @@ function hasPlugin(workspaceRoot, packageName) {
192
192
  const installed = { ...manifest.dependencies, ...manifest.devDependencies };
193
193
  return Object.hasOwn(installed, packageName);
194
194
  }
195
+ function ensurePlugin(workspaceRoot, packageName) {
196
+ if (hasPlugin(workspaceRoot, packageName)) {
197
+ return;
198
+ }
199
+ logger.step(`Installing Nx plugin ${packageName}`);
200
+ runNx(["add", packageName], workspaceRoot);
201
+ }
202
+ function markPrivate(manifestPath) {
203
+ const manifest = readJson(manifestPath);
204
+ writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
205
+ }
206
+ function markPublic(manifestPath) {
207
+ const manifest = readJson(manifestPath);
208
+ writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
209
+ }
195
210
  function ensureAdmZip(workspaceRoot) {
196
211
  if (hasPlugin(workspaceRoot, "adm-zip")) {
197
212
  return;
@@ -525,7 +540,7 @@ function addGoInternalLib(workspaceRoot, name) {
525
540
 
526
541
  // src/commands/add/node.ts
527
542
  var import_node_path5 = require("path");
528
- function ensurePlugin(workspaceRoot, packageName) {
543
+ function ensurePlugin2(workspaceRoot, packageName) {
529
544
  if (hasPlugin(workspaceRoot, packageName)) {
530
545
  return;
531
546
  }
@@ -533,7 +548,7 @@ function ensurePlugin(workspaceRoot, packageName) {
533
548
  runNx(["add", packageName], workspaceRoot);
534
549
  }
535
550
  function runNodeApp(workspaceRoot, name, stack, framework) {
536
- ensurePlugin(workspaceRoot, "@nx/node");
551
+ ensurePlugin2(workspaceRoot, "@nx/node");
537
552
  runNx(
538
553
  [
539
554
  "g",
@@ -699,10 +714,6 @@ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
699
714
  removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
700
715
  registerProjectCommands(workspaceRoot, name, { build: true });
701
716
  }
702
- function markPublic(manifestPath) {
703
- const manifest = readJson(manifestPath);
704
- writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
705
- }
706
717
 
707
718
  // src/commands/add/python.ts
708
719
  var import_node_fs4 = require("fs");
@@ -926,13 +937,6 @@ async function addPythonVendor(workspaceRoot, consumer, options, kindProvided) {
926
937
  // src/commands/add/reactApp.ts
927
938
  var import_node_fs5 = require("fs");
928
939
  var import_node_path8 = require("path");
929
- function ensurePlugin2(workspaceRoot, packageName) {
930
- if (hasPlugin(workspaceRoot, packageName)) {
931
- return;
932
- }
933
- logger.step(`Installing Nx plugin ${packageName}`);
934
- runNx(["add", packageName], workspaceRoot);
935
- }
936
940
  var REACT_ENVIRONMENTS = ["dev", "uat", "prod"];
937
941
  function reactEnvironmentFile(environment) {
938
942
  return `# Vite compiles VITE_-prefixed vars into the '${environment}' bundle at build time.
@@ -985,7 +989,7 @@ ${allow}
985
989
  `);
986
990
  }
987
991
  function addReactApp(workspaceRoot, name, stack) {
988
- ensurePlugin2(workspaceRoot, "@nx/react");
992
+ ensurePlugin(workspaceRoot, "@nx/react");
989
993
  runNx(
990
994
  [
991
995
  "g",
@@ -1011,9 +1015,73 @@ function addReactApp(workspaceRoot, name, stack) {
1011
1015
  registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
1012
1016
  }
1013
1017
 
1018
+ // src/commands/add/reactLib.ts
1019
+ var import_node_path9 = require("path");
1020
+ var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
1021
+ var ACTUAL_TYPES_PATH = "./dist/index.d.ts";
1022
+ function repairTypesPath(manifestPath) {
1023
+ const manifest = readJson(manifestPath);
1024
+ if (manifest.types === WRONG_TYPES_PATH) {
1025
+ manifest.types = ACTUAL_TYPES_PATH;
1026
+ }
1027
+ const dot = manifest.exports?.["."];
1028
+ if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
1029
+ dot.types = ACTUAL_TYPES_PATH;
1030
+ }
1031
+ writeFileEnsured(manifestPath, toJson(manifest));
1032
+ }
1033
+ var REACT_LIB_BUNDLER = "rollup";
1034
+ async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
1035
+ ensurePlugin(workspaceRoot, "@nx/react");
1036
+ const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
1037
+ runNx(
1038
+ [
1039
+ "g",
1040
+ "@nx/react:library",
1041
+ `packages/${name}`,
1042
+ `--name=${name}`,
1043
+ "--publishable",
1044
+ `--importPath=${scope}/${name}`,
1045
+ `--bundler=${REACT_LIB_BUNDLER}`,
1046
+ `--unitTestRunner=${stack.testRunner}`,
1047
+ "--linter=none",
1048
+ "--no-interactive"
1049
+ ],
1050
+ workspaceRoot
1051
+ );
1052
+ const publishableManifest = (0, import_node_path9.join)(workspaceRoot, "packages", name, "package.json");
1053
+ markPublic(publishableManifest);
1054
+ repairTypesPath(publishableManifest);
1055
+ removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
1056
+ registerProjectCommands(workspaceRoot, name, { build: true });
1057
+ }
1058
+ function addReactInternalLib(workspaceRoot, name, stack) {
1059
+ ensurePlugin(workspaceRoot, "@nx/react");
1060
+ runNx(
1061
+ [
1062
+ "g",
1063
+ "@nx/react:library",
1064
+ `libs/${name}`,
1065
+ `--name=${name}`,
1066
+ `--bundler=${REACT_LIB_BUNDLER}`,
1067
+ `--unitTestRunner=${stack.testRunner}`,
1068
+ "--linter=none",
1069
+ "--no-interactive"
1070
+ ],
1071
+ workspaceRoot
1072
+ );
1073
+ const privateManifest = (0, import_node_path9.join)(workspaceRoot, "libs", name, "package.json");
1074
+ markPrivate(privateManifest);
1075
+ repairTypesPath(privateManifest);
1076
+ removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
1077
+ registerProjectCommands(workspaceRoot, name, { build: true });
1078
+ }
1079
+
1014
1080
  // src/commands/add.ts
1015
1081
  var PROJECT_KINDS = [
1016
1082
  "react-app",
1083
+ "react-lib",
1084
+ "react-internal-lib",
1017
1085
  "node-app",
1018
1086
  "node-function-app",
1019
1087
  "npm-lib",
@@ -1033,12 +1101,12 @@ var PROJECT_KINDS = [
1033
1101
  ];
1034
1102
  async function runAdd(kind, name, options) {
1035
1103
  const workspaceRoot = process.cwd();
1036
- if (!fileExists((0, import_node_path9.join)(workspaceRoot, "nx.json"))) {
1104
+ if (!fileExists((0, import_node_path10.join)(workspaceRoot, "nx.json"))) {
1037
1105
  throw new Error("No nx.json found here. Run `add` from the workspace root.");
1038
1106
  }
1039
1107
  const stack = readWorkspaceStack(workspaceRoot);
1040
1108
  const kindProvided = kind !== void 0;
1041
- const resolvedKind = kind ?? await (0, import_prompts4.select)({
1109
+ const resolvedKind = kind ?? await (0, import_prompts5.select)({
1042
1110
  message: "What kind of project?",
1043
1111
  choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
1044
1112
  });
@@ -1049,6 +1117,14 @@ async function runAdd(kind, name, options) {
1049
1117
  addReactApp(workspaceRoot, resolvedName, stack);
1050
1118
  break;
1051
1119
  }
1120
+ case "react-lib": {
1121
+ await addReactLib(workspaceRoot, resolvedName, options, kindProvided, stack);
1122
+ break;
1123
+ }
1124
+ case "react-internal-lib": {
1125
+ addReactInternalLib(workspaceRoot, resolvedName, stack);
1126
+ break;
1127
+ }
1052
1128
  case "node-app": {
1053
1129
  addNodeApp(workspaceRoot, resolvedName, stack, options.framework);
1054
1130
  break;
@@ -1074,7 +1150,7 @@ async function runAdd(kind, name, options) {
1074
1150
  ],
1075
1151
  workspaceRoot
1076
1152
  );
1077
- markPrivate((0, import_node_path9.join)(workspaceRoot, "libs", resolvedName, "package.json"));
1153
+ markPrivate((0, import_node_path10.join)(workspaceRoot, "libs", resolvedName, "package.json"));
1078
1154
  removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
1079
1155
  registerProjectCommands(workspaceRoot, resolvedName, { build: true });
1080
1156
  break;
@@ -1150,43 +1226,73 @@ function syncProjectReferences(workspaceRoot) {
1150
1226
  }
1151
1227
  function readWorkspaceStack(workspaceRoot) {
1152
1228
  const nxJson = readJson(
1153
- (0, import_node_path9.join)(workspaceRoot, "nx.json")
1229
+ (0, import_node_path10.join)(workspaceRoot, "nx.json")
1154
1230
  );
1155
1231
  const stack = nxJson.mnci?.stack;
1156
1232
  return {
1157
1233
  testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
1158
1234
  };
1159
1235
  }
1160
- function markPrivate(manifestPath) {
1161
- const manifest = readJson(manifestPath);
1162
- writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
1163
- }
1164
1236
 
1165
1237
  // src/commands/interactive.ts
1166
- var import_node_path13 = require("path");
1167
- var import_prompts7 = require("@inquirer/prompts");
1238
+ var import_node_path14 = require("path");
1239
+ var import_prompts8 = require("@inquirer/prompts");
1168
1240
 
1169
1241
  // src/commands/new.ts
1170
- var import_node_path11 = require("path");
1242
+ var import_node_path12 = require("path");
1171
1243
 
1172
1244
  // src/overlay.ts
1173
1245
  var import_node_fs6 = require("fs");
1174
- var import_node_path10 = require("path");
1246
+ var import_node_path11 = require("path");
1175
1247
  var DEFAULT_STACK = { testRunner: "jest" };
1176
1248
  var PRETTIER_VERSION = "^3.8.1";
1177
1249
  var TS_COMPILER_DEPENDENCIES = {
1178
1250
  "@typescript/native": "npm:typescript@^7.0.2",
1179
1251
  typescript: "npm:@typescript/typescript6@^6.0.2"
1180
1252
  };
1181
- function npmrcContent(_registry, _scope) {
1182
- return `; Intentionally empty.
1253
+ function registryUrl(registry) {
1254
+ if (registry.kind === "azure-artifacts") {
1255
+ return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
1256
+ }
1257
+ return void 0;
1258
+ }
1259
+ function npmrcContent(registry, scope) {
1260
+ if (registry.kind === "npm") {
1261
+ return `; Publish authentication for the public npm registry.
1183
1262
  ;
1184
- ; Publish authentication is not configured yet. The generated CI exports a
1185
- ; token for the release step (NODE_AUTH_TOKEN for public npm, PAT for Azure
1186
- ; Artifacts), but nothing here consumes it, so 'npm publish' will not
1187
- ; authenticate until this is wired up.
1263
+ ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
1264
+ ; is harmless locally, so no token is needed for day-to-day work.
1265
+ //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
1266
+
1267
+ ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
1268
+ ; the default registry, so routing the scope to it would change nothing \u2014 and
1269
+ ; presenting that as protection against an accidental public publish would be
1270
+ ; false, because the public registry is the intended target. (An earlier version
1271
+ ; of this file made exactly that claim while emitting no routing line at all.)
1272
+ ; To keep a scoped package off npmjs.org, use a private feed: generate with
1273
+ ; --registry azure-artifacts, which does route the scope.
1274
+ `;
1275
+ }
1276
+ const feedUrl = registryUrl(registry);
1277
+ const feedKey = feedUrl.replace(/^https:/, "");
1278
+ return `; Publish + resolution routing for this workspace's own scope.
1279
+ ;
1280
+ ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
1281
+ ; feed: npm prefers a scope's registry over the global one when publishing a
1282
+ ; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
1188
1283
  ;
1189
- ; See "Known gaps" in the @mnci/cli README.
1284
+ ; Only the scope is routed, on purpose. A global 'registry=' would send every
1285
+ ; install through the feed as well, so 'npm ci' would need feed auth just to fetch
1286
+ ; public packages.
1287
+ ${scope}:registry=${feedUrl}
1288
+
1289
+ ; Feed credentials. PAT is the BASE64 value Azure Artifacts' "Connect to feed"
1290
+ ; instructions give you, which is exactly what _password expects, so it is used
1291
+ ; as-is. (twine wants the RAW token; the CI release step decodes it there.)
1292
+ ; Azure ignores the username, and npm requires an email it never uses.
1293
+ ${feedKey}:username=AzureArtifacts
1294
+ ${feedKey}:_password=\${PAT}
1295
+ ${feedKey}:email=npm-requires-this-and-never-uses-it
1190
1296
  `;
1191
1297
  }
1192
1298
  function releaseConfig(ci) {
@@ -1378,7 +1484,7 @@ function mnciConfig(options) {
1378
1484
  };
1379
1485
  }
1380
1486
  function readMnciConfig(workspaceRoot) {
1381
- const nxJson = readJson((0, import_node_path10.join)(workspaceRoot, "nx.json"));
1487
+ const nxJson = readJson((0, import_node_path11.join)(workspaceRoot, "nx.json"));
1382
1488
  return nxJson.mnci ?? {};
1383
1489
  }
1384
1490
  function pythonPublishUrl(registry) {
@@ -1430,9 +1536,18 @@ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKin
1430
1536
  # runs unchanged on Linux, macOS and Windows agents.
1431
1537
 
1432
1538
  trigger:
1539
+ # While a run is in progress, queue further pushes and run them together rather
1540
+ # than concurrently. This is the closest thing Azure Pipelines YAML has to
1541
+ # GitHub's concurrency group, and it is load-bearing on main for the same
1542
+ # reason: two concurrent 'nx release' runs would race to create the same tag.
1543
+ batch: true
1433
1544
  branches:
1434
1545
  include: [main]
1435
1546
 
1547
+ # Note: cancelling a superseded PR validation run is NOT a YAML setting on Azure
1548
+ # \u2014 it lives in the branch policy ("Build validation -> automatically cancel").
1549
+ # Nothing here can express it, so it is left to whoever configures the policy
1550
+ # rather than faked with a batch setting that would only delay PR runs.
1436
1551
  pr:
1437
1552
  branches:
1438
1553
  include: [main]
@@ -1445,6 +1560,12 @@ variables:
1445
1560
  # for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
1446
1561
  # for public npm. Mark it secret in Library. Add app build vars here too if needed.
1447
1562
  - group: ${variableGroup}
1563
+ # Relocates npm's cache inside the pipeline workspace so Cache@2 can restore it
1564
+ # (the default ~/.npm is outside the cacheable area on hosted agents). npm reads
1565
+ # this as an ordinary env var, so it needs no config file and works on every
1566
+ # agent OS.
1567
+ - name: npm_config_cache
1568
+ value: $(Pipeline.Workspace)/.npm
1448
1569
 
1449
1570
  steps:
1450
1571
  - checkout: self
@@ -1469,6 +1590,20 @@ steps:
1469
1590
  inputs:
1470
1591
  version: 24.x
1471
1592
 
1593
+ # The Azure counterpart of \`cache: npm\` on actions/setup-node: restores npm's
1594
+ # download cache so \`npm ci\` does not re-fetch every tarball on every run.
1595
+ # Keyed on the lockfile, so a dependency change misses and anything else hits;
1596
+ # restoreKeys falls back to the newest cache for this OS on a miss, which still
1597
+ # avoids a fully cold install. Agent.OS is in the key because a cached native
1598
+ # module built for one OS is not reusable on another.
1599
+ - task: Cache@2
1600
+ displayName: Cache npm packages
1601
+ inputs:
1602
+ key: 'npm | "$(Agent.OS)" | package-lock.json'
1603
+ restoreKeys: |
1604
+ npm | "$(Agent.OS)"
1605
+ path: $(npm_config_cache)
1606
+
1472
1607
  - script: npm ci
1473
1608
  displayName: Install dependencies
1474
1609
  env:
@@ -1626,6 +1761,19 @@ on:
1626
1761
  pull_request:
1627
1762
  branches: [main]
1628
1763
 
1764
+ concurrency:
1765
+ # One run at a time per ref, but \`cancel-in-progress\` is an **expression** on
1766
+ # purpose rather than a flat \`true\`.
1767
+ #
1768
+ # A superseded PR run is pure waste, so cancel it. A run on \`main\` must never
1769
+ # be cancelled: it publishes packages and pushes release tags, and killing it
1770
+ # part-way can leave a tag pushed with the publish only half done \u2014 a state no
1771
+ # rerun repairs cleanly, because the version is then already tagged. Those runs
1772
+ # queue instead, which also stops two \`nx release\` invocations racing to create
1773
+ # the same tag when two commits land close together.
1774
+ group: \${{ github.workflow }}-\${{ github.ref }}
1775
+ cancel-in-progress: \${{ github.event_name == 'pull_request' }}
1776
+
1629
1777
  permissions:
1630
1778
  # Lets the release step push the version tag nx release creates back to main,
1631
1779
  # and (github-only provider) create the GitHub Release itself.
@@ -1649,6 +1797,11 @@ jobs:
1649
1797
  - uses: actions/setup-node@v4
1650
1798
  with:
1651
1799
  node-version: 24
1800
+ # Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
1801
+ # the local cache instead of re-downloading every tarball on every run.
1802
+ # Nothing to invalidate by hand: the action keys on the lockfile, so a
1803
+ # dependency change misses the cache and a no-op change hits it.
1804
+ cache: npm
1652
1805
 
1653
1806
  - run: npm ci
1654
1807
  name: Install dependencies
@@ -1828,7 +1981,7 @@ updates:
1828
1981
  var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".vscode"];
1829
1982
  function removeNxScaffolding(workspaceRoot) {
1830
1983
  for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
1831
- (0, import_node_fs6.rmSync)((0, import_node_path10.join)(workspaceRoot, entry), { recursive: true, force: true });
1984
+ (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, entry), { recursive: true, force: true });
1832
1985
  }
1833
1986
  removeProjectEslintConfigs(workspaceRoot);
1834
1987
  }
@@ -1837,11 +1990,11 @@ function removeProjectEslintConfigs(workspaceRoot) {
1837
1990
  cwd: workspaceRoot
1838
1991
  });
1839
1992
  for (const match of matches) {
1840
- (0, import_node_fs6.rmSync)((0, import_node_path10.join)(workspaceRoot, match), { force: true });
1993
+ (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, match), { force: true });
1841
1994
  }
1842
1995
  }
1843
1996
  function applyOverlay(workspaceRoot, options) {
1844
- const nxJsonPath = (0, import_node_path10.join)(workspaceRoot, "nx.json");
1997
+ const nxJsonPath = (0, import_node_path11.join)(workspaceRoot, "nx.json");
1845
1998
  const nxJson = readJson(nxJsonPath);
1846
1999
  const generators = {
1847
2000
  ...nxJson.generators,
@@ -1851,7 +2004,7 @@ function applyOverlay(workspaceRoot, options) {
1851
2004
  const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
1852
2005
  const patched = withEslintPlugin(withReleaseConfig(nxJson, options.ci));
1853
2006
  writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
1854
- const manifestPath = (0, import_node_path10.join)(workspaceRoot, "package.json");
2007
+ const manifestPath = (0, import_node_path11.join)(workspaceRoot, "package.json");
1855
2008
  const manifest = readJson(manifestPath);
1856
2009
  const scripts = {
1857
2010
  ...manifest.scripts,
@@ -1869,34 +2022,34 @@ function applyOverlay(workspaceRoot, options) {
1869
2022
  manifestPath,
1870
2023
  toJson({ ...manifest, name: `${options.scope}/source`, scripts, devDependencies: devDeps })
1871
2024
  );
1872
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
1873
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
1874
- const hookPath = (0, import_node_path10.join)(workspaceRoot, ".husky/commit-msg");
2025
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
2026
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
2027
+ const hookPath = (0, import_node_path11.join)(workspaceRoot, ".husky/commit-msg");
1875
2028
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1876
2029
  markExecutable(hookPath);
1877
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
1878
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".prettierrc.json"), PRETTIER_CONFIG);
1879
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".prettierignore"), PRETTIER_IGNORE);
2030
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2031
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.json"), PRETTIER_CONFIG);
2032
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierignore"), PRETTIER_IGNORE);
1880
2033
  removeNxScaffolding(workspaceRoot);
1881
- const codeWorkspacePath = (0, import_node_path10.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
2034
+ const codeWorkspacePath = (0, import_node_path11.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
1882
2035
  const existing = readCodeWorkspace(codeWorkspacePath);
1883
2036
  writeFileEnsured(codeWorkspacePath, vscodeWorkspace(options.workspaceName, existing?.tasks));
1884
2037
  if (options.workspaceName !== "undefined") {
1885
- (0, import_node_fs6.rmSync)((0, import_node_path10.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
2038
+ (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
1886
2039
  }
1887
2040
  const publishUrl = pythonPublishUrl(options.registry);
1888
2041
  if (options.ci === "azure" || options.ci === "both") {
1889
2042
  writeFileEnsured(
1890
- (0, import_node_path10.join)(workspaceRoot, "azure-pipelines.yml"),
2043
+ (0, import_node_path11.join)(workspaceRoot, "azure-pipelines.yml"),
1891
2044
  azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind)
1892
2045
  );
1893
2046
  }
1894
2047
  if (options.ci === "github" || options.ci === "both") {
1895
2048
  writeFileEnsured(
1896
- (0, import_node_path10.join)(workspaceRoot, ".github/workflows/ci.yml"),
2049
+ (0, import_node_path11.join)(workspaceRoot, ".github/workflows/ci.yml"),
1897
2050
  githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci)
1898
2051
  );
1899
- writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
2052
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
1900
2053
  }
1901
2054
  }
1902
2055
 
@@ -1965,7 +2118,7 @@ async function runNew(name, options) {
1965
2118
  ],
1966
2119
  process.cwd()
1967
2120
  );
1968
- const workspaceRoot = (0, import_node_path11.join)(process.cwd(), workspaceName);
2121
+ const workspaceRoot = (0, import_node_path12.join)(process.cwd(), workspaceName);
1969
2122
  logger.step(
1970
2123
  "Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
1971
2124
  );
@@ -1984,9 +2137,8 @@ async function runNew(name, options) {
1984
2137
  runPrettier(workspaceRoot);
1985
2138
  logger.success("Done. Next steps:");
1986
2139
  logger.info(` cd ${workspaceName}`);
1987
- logger.info(
1988
- " mnci add react-app web # or: node-app, node-function-app, npm-lib, internal-lib,"
1989
- );
2140
+ logger.info(" mnci add react-app web # or: react-lib, react-internal-lib, node-app,");
2141
+ logger.info(" # node-function-app, npm-lib, internal-lib,");
1990
2142
  logger.info(
1991
2143
  " # python-app, python-function-app, python-lib, python-internal-lib,"
1992
2144
  );
@@ -2001,7 +2153,7 @@ async function runNew(name, options) {
2001
2153
 
2002
2154
  // src/commands/upgrade.ts
2003
2155
  var import_node_fs7 = require("fs");
2004
- var import_node_path12 = require("path");
2156
+ var import_node_path13 = require("path");
2005
2157
  function resolveRegistry2(options, persisted) {
2006
2158
  if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
2007
2159
  const persistedAzure = persisted?.kind === "azure-artifacts" ? persisted : void 0;
@@ -2038,7 +2190,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
2038
2190
  }
2039
2191
  } catch {
2040
2192
  }
2041
- return (0, import_node_path12.basename)(workspaceRoot);
2193
+ return (0, import_node_path13.basename)(workspaceRoot);
2042
2194
  }
2043
2195
  function resolveOverlayOptions(workspaceRoot, options, persisted) {
2044
2196
  const scope = options.scope ?? persisted.scope;
@@ -2076,7 +2228,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
2076
2228
  };
2077
2229
  }
2078
2230
  function runUpgrade(workspaceRoot, options) {
2079
- if (!fileExists((0, import_node_path12.join)(workspaceRoot, "nx.json"))) {
2231
+ if (!fileExists((0, import_node_path13.join)(workspaceRoot, "nx.json"))) {
2080
2232
  throw new Error(
2081
2233
  `No nx.json found in ${workspaceRoot} \u2014 this does not look like an Nx workspace. Run 'mnci upgrade' from the workspace root.`
2082
2234
  );
@@ -2093,14 +2245,14 @@ function runUpgrade(workspaceRoot, options) {
2093
2245
 
2094
2246
  // src/commands/interactive.ts
2095
2247
  async function runInteractive() {
2096
- const inWorkspace = fileExists((0, import_node_path13.join)(process.cwd(), "nx.json"));
2248
+ const inWorkspace = fileExists((0, import_node_path14.join)(process.cwd(), "nx.json"));
2097
2249
  const newChoice = { name: "Create a new monorepo", value: "new" };
2098
2250
  const addChoice = { name: "Add a project to this workspace", value: "add" };
2099
2251
  const upgradeChoice = {
2100
2252
  name: "Upgrade this workspace (re-apply the latest overlay)",
2101
2253
  value: "upgrade"
2102
2254
  };
2103
- const action = await (0, import_prompts7.select)({
2255
+ const action = await (0, import_prompts8.select)({
2104
2256
  message: "What would you like to do?",
2105
2257
  choices: inWorkspace ? [addChoice, upgradeChoice, newChoice] : [newChoice, addChoice, upgradeChoice]
2106
2258
  });
@@ -2115,7 +2267,7 @@ async function runInteractive() {
2115
2267
 
2116
2268
  // src/util/versionChecker.ts
2117
2269
  var import_node_fs8 = require("fs");
2118
- var import_node_path14 = require("path");
2270
+ var import_node_path15 = require("path");
2119
2271
  var import_cross_spawn2 = __toESM(require("cross-spawn"));
2120
2272
  var PACKAGE_NAME = "@mnci/cli";
2121
2273
  var REGISTRY_TIMEOUT_MS = 2e3;
@@ -2171,7 +2323,7 @@ function releaseTriple(version) {
2171
2323
  }
2172
2324
  function readCliVersion(packageDirectory) {
2173
2325
  try {
2174
- const manifestPath = (0, import_node_path14.join)(packageDirectory, "..", "package.json");
2326
+ const manifestPath = (0, import_node_path15.join)(packageDirectory, "..", "package.json");
2175
2327
  const manifest = JSON.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf8"));
2176
2328
  return manifest.version ?? "0.0.0";
2177
2329
  } catch {