@mnci/cli 4.8.1 → 4.9.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
@@ -909,7 +909,9 @@ as `${{ secrets.PAT }}` — GitHub has no "variable group" concept, so unlike
909
909
  Azure this needs no CLI-collected name, just a secret you create once in the
910
910
  repo settings. Either way it's mapped as `env` on the npm steps and read by
911
911
  the root `.npmrc`'s `_password` block — the PAT value never lands in a file.
912
- No `npmAuthenticate@0` task (it would overwrite the hand-set password).
912
+ No `npmAuthenticate@0` task in this mode (it would overwrite the hand-set password).
913
+ For an Azure feed there is a second mode that needs no PAT at all — see
914
+ [the alternative on Azure](#the-alternative-on-azure-build-identity-auth-no-pat-at-all).
913
915
 
914
916
  #### Why `_password` and never `_authToken` — read this before "fixing" it
915
917
 
@@ -945,18 +947,23 @@ credentials by URL prefix and walks only *up* a path, so an entry on
945
947
 
946
948
  #### The alternative on Azure: build-identity auth, no PAT at all
947
949
 
948
- mnci does not generate this, but it is the better setup for an Azure Artifacts feed
949
- in the **same organisation** as the pipeline, and it is what a real workspace uses
950
- today. `npmAuthenticate@0` injects the build service identity's token — which *is*
950
+ `--npm-auth build-identity` (on `mnci new` and `mnci upgrade`) is the better setup for
951
+ an Azure Artifacts feed in the **same organisation** as the pipeline.
952
+ `npmAuthenticate@0` injects the build service identity's token — which *is*
951
953
  Entra-issued, so it satisfies the Bearer scheme the feed advertises — and there is no
952
954
  secret to store, encode, rotate or let expire.
953
955
 
954
- To switch a generated workspace over by hand:
956
+ ```bash
957
+ mnci upgrade --npm-auth build-identity # switch an existing workspace
958
+ mnci upgrade --npm-auth pat # and back
959
+ ```
960
+
961
+ It changes two files together, which is the point of doing it through mnci rather than
962
+ by hand:
955
963
 
956
- 1. Delete the credential lines from the root `.npmrc`, keeping only the
957
- `@scope:registry=` routing line. (`mnci upgrade` will rewrite them back — see the
958
- caveat below.)
959
- 2. Add this step **before** `npm ci` in `azure-pipelines.yml`:
964
+ 1. `.npmrc` keeps only the `@scope:registry=` routing line. No credentials.
965
+ 2. `azure-pipelines.yml` runs this step **before** `npm ci`, and `npm ci` no longer
966
+ gets a `PAT` mapping it would never read:
960
967
 
961
968
  ```yaml
962
969
  - task: npmAuthenticate@0
@@ -965,6 +972,17 @@ To switch a generated workspace over by hand:
965
972
  workingFile: .npmrc
966
973
  ```
967
974
 
975
+ The choice is persisted in `nx.json`'s `mnci` block, so a plain `mnci upgrade` keeps it.
976
+ A workspace that added the task by hand before this option existed needs no flag either:
977
+ `mnci upgrade` sees `npmAuthenticate@0` in its pipeline and keeps that mode, rather than
978
+ regenerating the pipeline without it and writing a PAT block the feed rejects. An explicit
979
+ `--npm-auth pat` always wins over that detection. `mnci doctor` fails a workspace whose
980
+ two halves disagree — a credential-free `.npmrc` with no task publishes nothing (installs
981
+ still pass, so CI stays green until the release step's 401), and a PAT block *plus* the
982
+ task appends a second credential to a file that already has one.
983
+
984
+ The remaining manual step is the grant:
985
+
968
986
  3. Grant the build identity **Feed Publisher (Contributor)** on the feed (Artifacts →
969
987
  Feed Settings → Permissions). Which identity depends on the job authorization
970
988
  scope: `<Project> Build Service (<org>)` when scoped to the project (the default),
@@ -972,11 +990,14 @@ To switch a generated workspace over by hand:
972
990
  401 is the tell that the wrong identity is in play.
973
991
 
974
992
  Caveats, stated rather than glossed. This is **Azure-only** — GitHub Actions has no
975
- equivalent task, so a `--ci=github` or `--ci=both` workspace still needs the PAT.
993
+ equivalent task, so `--ci github` and `--ci both` are **refused** with `build-identity`
994
+ (the credential-free `.npmrc` would authenticate nowhere on the GitHub side), and so is
995
+ public npm, which has no identity to borrow. Those workspaces keep the PAT.
976
996
  Local development then needs `npx vsts-npm-auth -config .npmrc` (Windows) or a
977
- hand-added credential, since developers no longer inherit one from the file.
978
- And `mnci upgrade` rewrites `.npmrc`, so it will restore the PAT block — `git diff`
979
- before committing an upgrade, which the upgrade docs already tell you to do.
997
+ hand-added credential, since developers no longer inherit one from the file — the
998
+ generated `.npmrc` says so in its own comments. Python and NuGet publishing are
999
+ unchanged: they read the raw PAT from the variable group, so that group is still needed
1000
+ for a workspace that publishes either.
980
1001
 
981
1002
  #### Diagnosing an auth failure
982
1003
 
package/dist/cli.js CHANGED
@@ -303,13 +303,34 @@ var TS_COMPILER_DEPENDENCIES = {
303
303
  "@typescript/native": "npm:typescript@^7.0.2",
304
304
  "typescript": "npm:@typescript/typescript6@^6.0.2"
305
305
  };
306
+ var NPM_AUTH_MODES = ["pat", "build-identity"];
307
+ function resolveNpmAuth(requested, registry, ci, fallback) {
308
+ if (requested !== void 0 && !NPM_AUTH_MODES.includes(requested)) {
309
+ throw new Error(`--npm-auth must be one of: ${NPM_AUTH_MODES.join(", ")} (got '${requested}').`);
310
+ }
311
+ const mode = requested ?? fallback;
312
+ if (mode !== "build-identity") {
313
+ return mode;
314
+ }
315
+ if (registry.kind !== "azure-artifacts") {
316
+ throw new Error(
317
+ "--npm-auth build-identity needs an Azure Artifacts feed: public npm has no build identity to borrow. Use --registry azure-artifacts, or drop the flag."
318
+ );
319
+ }
320
+ if (ci !== "azure") {
321
+ throw new Error(
322
+ `--npm-auth build-identity needs --ci azure (this workspace is '${ci}'): it is npmAuthenticate@0, an Azure Pipelines task, and GitHub Actions has no equivalent. Use --npm-auth pat, or drop the GitHub pipeline.`
323
+ );
324
+ }
325
+ return mode;
326
+ }
306
327
  function registryUrl(registry) {
307
328
  if (registry.kind === "azure-artifacts") {
308
329
  return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
309
330
  }
310
331
  return void 0;
311
332
  }
312
- function npmrcContent(registry, scope) {
333
+ function npmrcContent(registry, scope, npmAuth = "pat") {
313
334
  if (registry.kind === "npm") {
314
335
  return `; Publish authentication for the public npm registry.
315
336
  ;
@@ -343,6 +364,38 @@ function npmrcContent(registry, scope) {
343
364
  `;
344
365
  }
345
366
  const feedUrl = registryUrl(registry);
367
+ if (npmAuth === "build-identity") {
368
+ return `; Publish + resolution routing for this workspace's own scope.
369
+ ;
370
+ ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
371
+ ; feed: npm prefers a scope's registry over the global one when publishing a
372
+ ; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
373
+ ;
374
+ ; Only the scope is routed, on purpose. A global 'registry=' would send every
375
+ ; install through the feed as well, so 'npm ci' would need feed auth just to fetch
376
+ ; public packages.
377
+ ${scope}:registry=${feedUrl}
378
+
379
+ ; NO credentials are written here. The 'npmAuthenticate@0' task in
380
+ ; azure-pipelines.yml injects them into this file at build time, using the build
381
+ ; service identity - so there is no PAT to store, rotate or encode.
382
+ ;
383
+ ; That is forced by what the feed accepts. An unauthenticated PUT to the publish
384
+ ; endpoint answers with:
385
+ ; www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
386
+ ; Basic realm="...", TFS-Federated
387
+ ; so its Bearer scheme wants an ENTRA ID access token, which is what the task
388
+ ; supplies. A PAT is not one: npm sends _authToken verbatim as a Bearer header, so
389
+ ; a PAT there is rejected with "Unable to authenticate, your authentication token
390
+ ; seems to be invalid". A PAT can only authenticate through the Basic scheme
391
+ ; (username + base64 _password), which is what 'mnci upgrade --npm-auth pat' writes.
392
+ ;
393
+ ; The cost is local: nothing in this file authenticates a developer's machine, so
394
+ ; publishing or installing a private package by hand needs
395
+ ; 'npx vsts-npm-auth -config .npmrc' (Windows) or a credential you add yourself.
396
+ ; Building and testing never authenticate.
397
+ `;
398
+ }
346
399
  const feedKey = feedUrl.replace(/^https:/, "");
347
400
  const feedShortKey = feedKey.replace("npm/registry/", "npm/");
348
401
  return `; Publish + resolution routing for this workspace's own scope.
@@ -440,8 +493,26 @@ function withPythonGraphPlugin(nxJson, pluginInstalled) {
440
493
  }
441
494
  return { ...nxJson, plugins: [...plugins, PYTHON_GRAPH_PLUGIN] };
442
495
  }
496
+ function isMergeableObject(value) {
497
+ return typeof value === "object" && value !== null && !Array.isArray(value);
498
+ }
499
+ function preservingUnknownKeys(existing, owned) {
500
+ if (!isMergeableObject(existing)) {
501
+ return owned;
502
+ }
503
+ const merged = { ...existing };
504
+ for (const [key, value] of Object.entries(owned)) {
505
+ const previous = merged[key];
506
+ merged[key] = isMergeableObject(value) && isMergeableObject(previous) ? preservingUnknownKeys(previous, value) : value;
507
+ }
508
+ return merged;
509
+ }
443
510
  function withReleaseConfig(nxJson, ci) {
444
- return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
511
+ return {
512
+ ...nxJson,
513
+ defaultBase: "main",
514
+ release: preservingUnknownKeys(nxJson.release, releaseConfig(ci))
515
+ };
445
516
  }
446
517
  var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventional'] }
447
518
  `;
@@ -588,6 +659,8 @@ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block,
588
659
  // mnci/html @html-eslint/eslint-plugin
589
660
  // mnci/tests *.spec/*.test relaxations \u2014 eslint-plugin-jest
590
661
  // (Vitest's globals too; the two stacks share them)
662
+ // mnci/tests/mock-aware specs in a project: eslint-plugin-jest's unbound-method,
663
+ // which knows expect(mock.method) has no this to lose
591
664
  // mnci/nx-dependency-checks @nx/eslint-plugin, on publishable packages' manifests
592
665
  // mnci/standard JavaScript Standard Style as ESLint rules \u2014 the
593
666
  // whole formatting opinion, a faithful port of
@@ -834,7 +907,10 @@ function mnciConfig(options) {
834
907
  agent: options.agent,
835
908
  variableGroup: options.variableGroup,
836
909
  ci: options.ci,
837
- stack: { testRunner: options.stack.testRunner }
910
+ stack: { testRunner: options.stack.testRunner },
911
+ // Only when chosen: a workspace on the default keeps the block it always had,
912
+ // so an upgrade does not add a field nobody asked for.
913
+ ...options.npmAuth !== void 0 && { npmAuth: options.npmAuth }
838
914
  };
839
915
  }
840
916
  function readMnciConfig(workspaceRoot) {
@@ -970,9 +1046,24 @@ function npmAuthEnvVariable(registryKind, variableReference) {
970
1046
  function poolBlock(agent) {
971
1047
  return /^(?:ubuntu|windows|macos)-/i.test(agent) ? ` vmImage: ${agent}` : ` name: ${agent}`;
972
1048
  }
973
- function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts", nugetFeedUrl2) {
1049
+ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts", nugetFeedUrl2, npmAuth = "pat") {
974
1050
  const onMain = "and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(variables['Build.SourceBranchName'], 'main'))";
975
1051
  const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `$(${name})`);
1052
+ const buildIdentity = npmAuth === "build-identity" && registryKind === "azure-artifacts";
1053
+ const npmAuthenticateStep = buildIdentity ? ` # Injects feed credentials into .npmrc using the BUILD SERVICE IDENTITY, so no
1054
+ # PAT is stored, rotated or encoded anywhere. This is what the feed accepts: its
1055
+ # publish endpoint advertises a Bearer scheme bound to login.windows.net, which
1056
+ # wants an Entra ID access token, and a PAT is not one. Must run before 'npm ci'.
1057
+ # Needs the build identity to be a Feed Publisher (Contributor) on the feed.
1058
+ - task: npmAuthenticate@0
1059
+ displayName: Authenticate npm against the feed (build identity, no PAT)
1060
+ inputs:
1061
+ workingFile: .npmrc
1062
+
1063
+ ` : "";
1064
+ const npmCiEnv = buildIdentity ? "" : `
1065
+ env:
1066
+ ${npmAuthName}: ${npmAuthValue}`;
976
1067
  return `name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
977
1068
 
978
1069
  # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
@@ -1089,10 +1180,8 @@ steps:
1089
1180
  npm | "$(Agent.OS)"
1090
1181
  path: $(npm_config_cache)
1091
1182
 
1092
- - script: npm ci
1093
- displayName: Install dependencies
1094
- env:
1095
- ${npmAuthName}: ${npmAuthValue}
1183
+ ${npmAuthenticateStep} - script: npm ci
1184
+ displayName: Install dependencies${npmCiEnv}
1096
1185
 
1097
1186
  # Fails ONLY on an advisory that has a published fix, at moderate or above;
1098
1187
  # anything upstream has not fixed is printed and passes. See NPM_AUDIT_STEP's
@@ -1754,7 +1843,10 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
1754
1843
  onProgress(
1755
1844
  `.npmrc \u2014 ${options.registry.kind === "azure-artifacts" ? "Azure Artifacts feed routing and credentials" : "public npm registry auth"}`
1756
1845
  );
1757
- writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
1846
+ writeFileEnsured(
1847
+ (0, import_node_path2.join)(workspaceRoot, ".npmrc"),
1848
+ npmrcContent(options.registry, options.scope, options.npmAuth)
1849
+ );
1758
1850
  onProgress("commitlint.config.mjs and .husky/commit-msg \u2014 conventional commit enforcement");
1759
1851
  writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
1760
1852
  const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
@@ -1797,7 +1889,14 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
1797
1889
  onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
1798
1890
  writeFileEnsured(
1799
1891
  (0, import_node_path2.join)(workspaceRoot, "azure-pipelines.yml"),
1800
- azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind, nugetUrl)
1892
+ azurePipelinesYaml(
1893
+ options.agent,
1894
+ options.variableGroup,
1895
+ publishUrl,
1896
+ options.registry.kind,
1897
+ nugetUrl,
1898
+ options.npmAuth
1899
+ )
1801
1900
  );
1802
1901
  }
1803
1902
  if (options.ci === "github" || options.ci === "both") {
@@ -4750,6 +4849,37 @@ function checkNpmrcCredentialResolves(workspaceRoot, environment = process.env)
4750
4849
  remedy: "nothing, for installing or building \u2014 this only affects authenticated commands. To publish or deprecate by hand, run it from outside this workspace, or export a real token first. If it still fails there, `npm whoami` \u2014 an expired token gives the same 404"
4751
4850
  };
4752
4851
  }
4852
+ function checkNpmAuthMatchesPipeline(workspaceRoot, registry) {
4853
+ const npmrcPath = (0, import_node_path19.join)(workspaceRoot, ".npmrc");
4854
+ const pipelinePath = (0, import_node_path19.join)(workspaceRoot, "azure-pipelines.yml");
4855
+ if (registry?.kind !== "azure-artifacts" || !fileExists(npmrcPath) || !fileExists(pipelinePath)) {
4856
+ return void 0;
4857
+ }
4858
+ const holdsCredentials = (0, import_node_fs12.readFileSync)(npmrcPath, "utf8").split("\n").filter((line) => !line.trimStart().startsWith(";") && !line.trimStart().startsWith("#")).some((line) => /(?:_authToken|_password|_auth)\s*=/u.test(line));
4859
+ const runsTask = (0, import_node_fs12.readFileSync)(pipelinePath, "utf8").includes("npmAuthenticate@0");
4860
+ if (!holdsCredentials && !runsTask) {
4861
+ return {
4862
+ check: "npm auth: .npmrc and azure-pipelines.yml agree",
4863
+ ok: false,
4864
+ detail: ".npmrc carries no credentials and azure-pipelines.yml does not run npmAuthenticate@0, so nothing authenticates the feed \u2014 installs still pass, and the release step fails with a 401",
4865
+ remedy: "run `mnci upgrade --npm-auth build-identity` to add the task, or `mnci upgrade --npm-auth pat` to write the PAT block back"
4866
+ };
4867
+ }
4868
+ if (holdsCredentials && runsTask) {
4869
+ return {
4870
+ check: "npm auth: .npmrc and azure-pipelines.yml agree",
4871
+ ok: false,
4872
+ detail: ".npmrc holds a credential block AND azure-pipelines.yml runs npmAuthenticate@0, which appends a second credential to a file that already has one",
4873
+ remedy: "run `mnci upgrade --npm-auth build-identity` to drop the PAT block, or `mnci upgrade --npm-auth pat` to drop the task"
4874
+ };
4875
+ }
4876
+ return {
4877
+ check: "npm auth: .npmrc and azure-pipelines.yml agree",
4878
+ ok: true,
4879
+ detail: "",
4880
+ remedy: ""
4881
+ };
4882
+ }
4753
4883
  var NPM_TOKEN_PREFIX = "npm_";
4754
4884
  var LEGACY_NPM_TOKEN = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu;
4755
4885
  function npmjsCredentialShape(credentials, environment) {
@@ -4954,6 +5084,7 @@ function collectFindings(workspaceRoot) {
4954
5084
  checkResolvedEslint(workspaceRoot),
4955
5085
  checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
4956
5086
  checkNpmrcCredentialResolves(workspaceRoot),
5087
+ checkNpmAuthMatchesPipeline(workspaceRoot, nxJson.mnci?.registry),
4957
5088
  checkNoRootRuntimeDependencies(workspaceRoot),
4958
5089
  ...checkRollupSourceMaps(workspaceRoot),
4959
5090
  ...checkDeclarationSpecifiers(workspaceRoot),
@@ -5110,6 +5241,7 @@ async function runNew(name, options) {
5110
5241
  "ubuntu-latest"
5111
5242
  ));
5112
5243
  const variableGroup = ci === "github" ? options.variableGroup ?? "Build" : options.variableGroup ?? (options.yes ? "Build" : await promptText("Azure DevOps variable group holding the npm PAT", "Build"));
5244
+ const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci);
5113
5245
  const stack = await resolveStack(options);
5114
5246
  const nxCloud = options.nxCloud ?? (options.yes ? false : await promptNxCloud());
5115
5247
  const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs14.mkdtempSync)((0, import_node_path21.join)((0, import_node_os.tmpdir)(), "mnci-new-"));
@@ -5140,7 +5272,7 @@ async function runNew(name, options) {
5140
5272
  );
5141
5273
  applyOverlay(
5142
5274
  workspaceRoot,
5143
- { workspaceName, scope, registry, agent, variableGroup, ci, stack },
5275
+ { workspaceName, scope, registry, agent, variableGroup, ci, stack, ...npmAuth && { npmAuth } },
5144
5276
  logger.detail
5145
5277
  );
5146
5278
  (0, import_node_fs14.rmSync)((0, import_node_path21.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
@@ -5218,6 +5350,13 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
5218
5350
  }
5219
5351
  return (0, import_node_path22.basename)(workspaceRoot);
5220
5352
  }
5353
+ function detectNpmAuth(workspaceRoot) {
5354
+ try {
5355
+ return (0, import_node_fs15.readFileSync)((0, import_node_path22.join)(workspaceRoot, "azure-pipelines.yml"), "utf8").includes("npmAuthenticate@0") ? "build-identity" : void 0;
5356
+ } catch {
5357
+ return void 0;
5358
+ }
5359
+ }
5221
5360
  function resolveOverlayOptions(workspaceRoot, options, persisted) {
5222
5361
  const scope = options.scope ?? persisted.scope;
5223
5362
  if (!scope) {
@@ -5237,6 +5376,11 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
5237
5376
  );
5238
5377
  }
5239
5378
  const variableGroup = options.variableGroup ?? persisted.variableGroup ?? "Build";
5379
+ const detected = ci === "azure" && persisted.npmAuth === void 0 && options.npmAuth === void 0 ? detectNpmAuth(workspaceRoot) : void 0;
5380
+ if (detected) {
5381
+ logger.detail("keeping build-identity npm auth: azure-pipelines.yml already runs npmAuthenticate@0");
5382
+ }
5383
+ const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci, persisted.npmAuth ?? detected);
5240
5384
  const testRunner = options.testRunner ?? persisted.stack?.testRunner;
5241
5385
  if (!testRunner) {
5242
5386
  throw new Error(
@@ -5250,7 +5394,8 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
5250
5394
  agent,
5251
5395
  variableGroup,
5252
5396
  ci,
5253
- stack: { testRunner }
5397
+ stack: { testRunner },
5398
+ ...npmAuth && { npmAuth }
5254
5399
  };
5255
5400
  }
5256
5401
  function runUpgrade(workspaceRoot, options) {
@@ -5386,7 +5531,10 @@ function buildProgram(cliVersion) {
5386
5531
  program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option(
5387
5532
  "--agent <pool>",
5388
5533
  "CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name"
5389
- ).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--test-runner <runner>", "unit-test runner: jest | vitest").option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").option(
5534
+ ).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
5535
+ "--npm-auth <mode>",
5536
+ "how npm authenticates to an Azure Artifacts feed: pat (default) | build-identity (npmAuthenticate@0, no PAT; --ci azure only)"
5537
+ ).option("--ci <provider>", "CI provider: azure | github | both").option("--test-runner <runner>", "unit-test runner: jest | vitest").option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").option(
5390
5538
  "--into <dir>",
5391
5539
  "bootstrap into an existing directory (a fresh clone holding only .git); the directory name is the workspace name unless one is given"
5392
5540
  ).action(async (name, options) => {
@@ -5394,7 +5542,10 @@ function buildProgram(cliVersion) {
5394
5542
  });
5395
5543
  program.command("upgrade").description(
5396
5544
  "Re-apply the latest MoNecromanCI overlay to this workspace (release config, pipeline, npmrc, commitlint, husky hook, curated scripts)"
5397
- ).option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both (overrides the persisted value)").option(
5545
+ ).option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
5546
+ "--npm-auth <mode>",
5547
+ "how npm authenticates to an Azure Artifacts feed: pat (default) | build-identity (npmAuthenticate@0, no PAT; --ci azure only)"
5548
+ ).option("--ci <provider>", "CI provider: azure | github | both (overrides the persisted value)").option(
5398
5549
  "--test-runner <runner>",
5399
5550
  "unit-test runner: jest | vitest (overrides the persisted value)"
5400
5551
  ).action((options) => {