npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mnci/cli

v4.29.2

Published

MoNecromanCI CLI — a thin CLI over official and community Nx plugins: create an opinionated Nx monorepo (interactive wizard or flags; ESLint + Prettier, Jest/Vitest) and add React apps, Node apps, Azure Function apps (Node or Python), and npm/Python/Go/Fl

Readme

@mnci/cli

A thin CLI over what Nx already ships: an opinionated one-command Nx monorepo with automatic commit-message versioning, instead of hand-rolling templates, configs and CI engines.

The thesis

Most of what a monorepo tool needs to hand-roll — a template engine, a shared config package, a custom CI engine, a dependency-injection step for published packages, a doctor/drift-sync system to keep it all consistent — already has a first-party (or established community) Nx equivalent:

| Hand-rolled elsewhere | This CLI uses instead | | ------------------------------------------ | ----------------------------------------------------------------------------- | | Template engine + per-project config files | create-nx-workspace --preset=ts + nx g plugin generators | | A shared toolchain package for configs | The configs the Nx generators emit (one root ESLint/tsconfig) | | A custom multi-step CI engine | nx affected -t lint,typecheck,test,build + nx release (~60-line pipeline) | | A dist-package dependency injector | nx release updates dependent versions natively | | Hand-written Azure Function templates | @nx/node:application (plain Node app) + a thin Azure Functions v4 overlay | | doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |

How this package is organised

Source is arranged in vertical slices: one folder per outcome, each with an index.ts that is its whole public API. A sibling is reached only through that barrel, never by a path into its files, and a file's suffix says what role it plays (.use-case, .client, .repository, .algorithm, .validator, .handler). Tests sit beside what they test.

src/
  main.ts                     the CLI transport — decodes argv, calls one use case
  workspace-overlay/          the config files mnci owns and rewrites
  workspace-creation/         mnci new, and the interactive wizard
  workspace-upgrade/          mnci upgrade
  workspace-diagnostics/      mnci doctor
  project-scaffolding/        mnci add — one use case per kind, plus post-generation repairs
  rollup-library/             what a rollup-bundled library needs repaired to build, type and publish
  dependency-management/      mnci sync / mnci up, and the manifest + registry + semver machinery
  nx-workspace/               runs the Nx and npm CLIs, always via an argv array
  terminal/                   prompts in, coloured status out
  file-system/                JSON, JSONC workspace files, ensured writes
  project-name/               name validation
  cli-version/                the update check

The dependency graph is acyclic and flows one way: main → the command slices → the infrastructure slices → file-system as a leaf. That is checked, not assumed — and now enforced: the root eslint.config.mjs turns on @mnci/eslint-config's verticalSlices rules for packages/cli/src, so a deep sibling import, a file without a role suffix, a nested subfeature or a cycle between slices fails npm run lint rather than needing someone to re-run the checks by hand. It is scoped to this package deliberately; the config file says which packages were measured and why they are excluded.

Exceptions, and when they go away

Two places hold more than the one responsibility their name claims, and are named here because the next reader deserves to know before opening them:

| Path | Rule waived | Why, and removal condition | |---|---|---| | workspace-overlay/overlay.use-case.ts | one responsibility per file | ~5k lines covering CI YAML for two providers (including the native-app job), .npmrc, nuget.config, the VS Code workspace, release config and the CI guard scripts. Splitting it is a decomposition, not a move, so it was deliberately kept out of the change that created these slices. Temporary — removed when that decomposition lands. | | rollup-library/repair-rollup-config.use-case.spec.ts | a test takes its subject's basename | It also holds the withUpgradedDeclarationSpecifierPlugin describe, whose subject is rollup-config.algorithm.ts. That transform shares three fixtures with the repairs that apply it (OLD_DTS_PLUGIN_CONFIG, EXTENSION_ONLY_DTS_PLUGIN_CONFIG, loadWriteBundle), and duplicating them across two spec files is the worse trade. Permanent unless those fixtures stop being shared. |

rollup-library/ exists because of the own-the-concept rule. Its contents used to live in project-scaffolding, which meant workspace-diagnostics and workspace-upgrade depended on the scaffolding slice only to reach repair helpers — they have no interest in adding a project. The concept now sits in its own slice that depends on nothing above it (file-system alone), and all three consumers point at it.

Commands (deliberately few)

mnci new my-repo            # create a monorepo (prompts scope + registry)
mnci new my-repo --yes --registry npm --scope @my
mnci new my-repo --yes --registry npm --scope @my --nx-cloud  # opt in to Nx Cloud
mnci new --into .           # ...or bootstrap into a clone that already exists

cd my-repo
mnci add react-app web         # @nx/react (Vite + Jest)
mnci add node-app svc          # @nx/node (plain Node app, esbuild)
mnci add node-app api --framework express  # ...or fastify | koa | nest
mnci add npm-lib core --empty             # slice skeleton only, no sample (also internal-lib, react-lib, react-internal-lib)
mnci add node-function-app api # @nx/node + an Azure Functions v4 overlay
mnci add npm-lib sdk           # @nx/js publishable lib -> packages/
mnci add internal-lib utils    # @nx/js private lib -> libs/
mnci add react-lib ui          # @nx/react publishable component lib -> packages/
mnci add react-internal-lib design  # @nx/react private component lib -> libs/

# Python (@mnci/nx-python-pip — pip + Ruff + pytest + PyPA build/twine, no uv)
mnci add python-app svc            # app -> apps/ (wheel, zipped into the drop)
mnci add python-function-app fn    # Azure Functions (Python v2) -> apps/
mnci add python-lib shared         # publishable -> python-packages/ (twine upload)
mnci add python-internal-lib core  # private shared lib -> libs/
mnci add python-vendor shared --lib core  # wire core's module into shared's built wheel

# Go (@nx-go/nx-go — multi-module: one go.mod per project + a root go.work, golangci-lint + go test)
mnci add go-app api            # executable -> apps/ (binary, zipped into the drop)
mnci add go-app cli --release  # ...released: tag + per-platform zips on the GitHub Release
mnci add go-app tray --cgo     # needs a C toolchain: built on a runner of each OS
mnci add go-app site --web web # embeds and serves the React app apps/web in one binary
mnci add go-function-app fn    # serverless handler -> apps/
mnci add go-lib core           # publishable (by git tag) -> packages/
mnci add go-internal-lib util  # private shared package -> libs/

# Flutter (@mnci/nx-flutter — one root pubspec.yaml pub workspace, analyze + test)
mnci add flutter-app hello         # Flutter web app -> apps/ (bundle, zipped into the drop)
mnci add flutter-lib shared        # publishable (by git tag) -> packages/
mnci add flutter-internal-lib core # private shared package -> libs/

# VS Code extensions (@nx/node + vsce — bundled, packaged per platform, published by nx release)
mnci add vscode-extension editor                      # one universal .vsix -> dist/drop/
mnci add vscode-extension editor --sidecar api        # one .vsix per platform, go-app api's binary in bin/
mnci add vscode-extension editor --publisher acme     # Marketplace publisher (default: the scope without @)

mnci upgrade                  # re-apply the latest overlay (see below)
mnci upgrade --agent windows-latest   # ...with an explicit override

mnci doctor                   # check this workspace's invariants (read-only)

mnci ci verify                # the pipeline's verify phase, run here: see `mnci ci` below

mnci sync                     # converge dependency ranges + nx sync (TS project refs)
mnci sync --check             # ...report and exit non-zero, writing nothing

mnci up                       # what has a newer release, grouped; pick what to update
mnci up --check               # ...report only (the default when output is piped)

mnci i -w web left-pad        # add a dep to ONE project, using its toolchain (alias: mnci install)
mnci i -w api github.com/x/y  # ...go get in the Go module; dotnet/flutter/pip all dispatched the same way
mnci i -w ui -w web react     # ...-w repeats, to add to several projects at once
mnci i -w svc jest -D         # ...--save-dev where the ecosystem has a dev-dependency notion
mnci i                        # no -w: install/restore the whole workspace (the one-shot bootstrap)

Where a dependency belongs: root vs project

One rule, and it is the same in every language mnci supports:

Shared development and tool packages live at the root. Runtime dependencies belong to the package that imports them.

| | Runtime deps declared in | What the root file holds | | -------- | -------------------------------------------------------------------- | --------------------------------------------------------------------- | | npm | each project's package.json | package.json — scripts, devDependencies, overrides (root-only by npm's rules) | | pip | each project's pyproject.toml (a function app: its requirements.txt) | requirements-dev.txt — the shared toolchain, nothing else | | pub | each member's pubspec.yaml | pubspec.yaml — the member list and an SDK floor, no dependency blocks | | go | each project's own go.mod (multi-module) | go.work — the use list only; mnci owns it, no requirements |

Go follows the same rule as everything else (MoNecromanCI/MoNecromanCi#289). Each project has its own go.mod with a fetchable module path (<host>/<org>/<repo>/<dir>, derived from the git origin), tied together by a root go.work that mnci owns. That is what lets mnci i -w <go-project> <pkg> go get into exactly that module. The one hazard it introduces — a stale use entry whose directory is gone makes go list -m fail and breaks the whole Nx project graph — is caught by mnci doctor, which fails on it and names the line to remove.

Why hoisting a runtime dependency to the root is a bug, not a tidy-up

It looks like centralisation and it is not. Two independent reasons:

  1. The root manifest is private and never published. A runtime dependency declared there reaches no consumer of any package; an installed @scope/lib simply fails to resolve it.
  2. @nx/rollup externalises exactly what a project's OWN manifest declares. Pull a dependency out of packages/thing/package.json and rollup stops treating it as external — it inlines a private copy into the bundle. Measured on a real generated workspace: moving axios out of one package's manifest took its published bundle from 14.5 KB to 832 KB, silently.

Two things catch this, from opposite directions. @nx/dependency-checks (in the root ESLint config, so it runs as part of lint) fails the project whose import is now undeclared, and mnci doctor fails the root that took it.

Debugging: breakpoints in the TypeScript, not the built JavaScript

A publishable library is bundled by @nx/rollup, so what runs is dist/*.js. A breakpoint in the .ts binds only if the build emitted a source map that points back at real files. Three separate things had to be fixed for that to be true, and a generated workspace now gets all three:

| | The default | What mnci writes | | --- | --- | --- | | sourceMap | unset, so no .js.map at all | true, in withNx's first argument | | compiler | 'swc', hardcoded by @nx/js:lib | 'babel' — see below | | sources paths | OS-native, one parent segment too many | repaired by sourcemapPathTransform |

Each one alone leaves breakpoints grey, and none of them reports an error.

sourceMap has to go in the first argument. The obvious spot is output: { sourcemap: true } in the second — the generator's own placeholder comment even suggests it — and it silently does nothing: withNx spreads your output and then assigns sourcemap: options.sourceMap, so its own undefined value always wins.

The compiler swap is not a preference. @nx/rollup's swc plugin calls swc's transform() without sourceMaps, so swc returns no map, the rollup chain breaks, and the map comes out valid-looking and empty — sources: []. Measured on a real package: swc gave 0 sources, babel gave 9. Revert the swap once Nx passes sourceMaps through; issue #308 has the one-line upstream fix.

Re-measured against the exact toolchain mnci new pins today (Nx 23.2.0, @swc/core 1.15.8): @nx/rollup's swc plugin still does not pass sourceMaps to transform() — read straight from the installed package, not assumed — yet a real build with compiler: 'swc' left unmodified now emits a map with real sources/sourcesContent, and tracing a generated position through it with @jridgewell/trace-mapping resolves to the correct original line and column. So the empty-map failure this section exists to route around did not reproduce on the current pinned versions, even though the documented root cause (the plugin's own missing sourceMaps option) is still there verbatim. That is not the revert condition stated above — nothing upstream changed the call this section is about — so the swap stays in place as a safety net rather than being removed on an unexplained, unpinned-by-upstream behavior change that a future @swc/core patch could revert without notice. A real, separate bug in the swap itself was found and fixed, independent of the above: it matched compiler: 'swc' with a plain string, one space after the colon. @stylistic/key-spacing (aligned on value) — which eslint --fix applies to every mnci-generated file — pads that column out to whatever the object's longest key is, so on any config reached after even one lint pass (most concretely: mnci upgrade repairing a project whose add partially failed) the literal silently stopped matching and the swap silently no-op'd, while sourceMap: true and sourcemapPathTransform — added by separate, whitespace-tolerant repairs — went in regardless. mnci doctor's check did not catch it either, since it only verifies the flag, not the compiler. Confirmed end to end and fixed with the same whitespace-tolerant approach sourceMap: true's own guard already used.

The paths are wrong twice over. rollup hands sourcemapPathTransform a path like ..\..\src\index.ts for a map in dist/ — one parent segment too many, so it resolves above the project to a file that does not exist, and back-slashed, which is invalid in a sourcemap sources entry on every platform (a sources entry is URL-style — the same bug class as the declaration stub). Both are repaired, by collapsing the parent-segment run rather than stripping a fixed prefix, so it cannot go stale at another nesting depth.

Maps are built always and published never. !**/*.js.map joins files, so npm run <lib>:build is debuggable while the tarball stays lean — the same trade already made for .d.ts.map. There is deliberately no dev-build flag: a build you have to remember to run differently is one you will not have run at the moment you need it.

mnci doctor reports any rollup config missing this, and mnci upgrade sweeps packages/* and libs/* to add it — a rollup config is written once at add time, so a workspace generated earlier would never fix itself otherwise. The sweep is idempotent.

mnci sync: making every project agree

nx sync runs the workspace's sync generators, and the only one a generated workspace registers is @nx/js:typescript-sync — it reconciles TypeScript project references and has no opinion whatsoever about dependency versions. And npm has no catalog:, pnpm's one-version-per-workspace mechanism, so keeping two projects on the same range is a convention nothing enforces.

mnci sync is both halves:

  1. Every external package declared at more than one version converges on one spec.
  2. nx sync then reconciles the TypeScript project references.

The winning spec is the one matching what is actually resolved — node_modules for npm, pubspec.lock for pub, the interpreter for pip. That is the same source @nx/dependency-checks pins a drifted range to when it auto-fixes, so the command and the lint rule converge on one answer instead of overwriting each other. With nothing installed, the highest declared range wins instead, and the report says which rule was applied.

It keeps the range operator the majority of sites already use, so a workspace that pins exactly stays pinned.

Three things it deliberately never touches:

  • Peer ranges. >=21.0.0 on @nx/devkit is a compatibility declaration, not a version choice — narrowing it to the 23.x you happen to resolve drops two majors of consumers. The first run of this command against mnci's own repo reported six findings, five of which were exactly that mistake.
  • The workspace's own projects. An internal @scope/lib is symlinked and versioned by nx release; its loose range is what lets both the link and the tag satisfy it.
  • A spec whose shape it cannot safely edit — a git:/path:/URL target, a workspace: protocol, an npm:pkg@range alias, a pub git: map. Those are reported as a warning and left alone.

Go reports "nothing to sync" rather than a silent pass, because one root go.mod means one version of every module — there is nothing that could disagree.

--check reports and exits non-zero without writing anything, so it works as a CI step. --ecosystem npm|pip|pub|go narrows the run.

mnci up: what has a newer release, and who is using it

Modelled on npm-check -u — the same four sections in the same order, the same interactive multiselect — with one addition that npm-check cannot give you in a monorepo: every project declaring the package.

Minor Update  New backwards-compatible features.
  @nx/devkit                 devDep/peerDep   23.1.1  ›  23.2.0  (root), packages/nx-python-pip, packages/nx-flutter
  @typescript-eslint/parser  dep/devDep        8.68.0  ›  8.69.0  packages/eslint-config, packages/az-durable

That column is the point. It is what tells you an upgrade touches three projects before you pick it, and picking one rewrites every declaration of it — which is what stops mnci up from creating the drift mnci sync then has to repair.

Latest versions come from each ecosystem's own tooling, never a hand-rolled HTTP call: npm view (so a scoped Azure Artifacts feed and its .npmrc credentials just work), pip index versions, one go list -m -u -json all, one flutter pub outdated --json. An ecosystem whose toolchain is absent is reported as a loud SKIPPED, never quietly dropped.

The same three exclusions as mnci sync apply, plus two more that only matter here: an indirect Go module (go mod tidy owns those) and an aliased install, where the manifest key names a different package than the one on disk. The alias case is not hypothetical — mnci's own root manifest pins the dual TypeScript compiler as typescript: npm:@typescript/typescript6@^6.0.2, and the first run of this command offered "typescript 6.0.2 › 7.0.2", which is real TypeScript's version, about a package the workspace does not have.

A selected Go module is upgraded with go get <module>@<version>, never by editing go.mod — that file is the toolchain's to write.

Flags: --check (report only; also the automatic behaviour when stdout is not a TTY, so a piped or CI run reports instead of hanging on a prompt), -y/--yes (take everything), --ecosystem, and --no-install (edit the manifests but skip the reinstall).

mnci ci: the pipeline's phases, as a command

mnci ci verify runs the pipeline's verify phase on your machine: nx sync:check, then every project (or, when a pull request's target branch is set, only the projects affected since the merge-base with it) through lint, typecheck, test and build. It exits with the failing command's own status, so it can stand in for the pipeline's step.

It is not a shortcut for those Nx targets, and the CLI still has no wrapper for nx test. It is the pipeline's own logic, which until now lived as a node -e one-liner inside the generated YAML, ported to tested code, so a laptop and a pipeline run the same thing. The design (one npx mnci ci call in the pipeline, with room for your own steps around it) is tracked in #269, and this is its first phase: the generated pipelines do not call it yet and are unchanged, so nothing about an existing workspace changes. Phases follow as their guards are ported.

  • Same scoping as the guard. No pull-request target means every project, so a push to main verifies in full. A pull request verifies what is affected since the merge-base with origin/<target>, fetching the target once if that ref is missing, and falls back to every project when no merge-base can be found, since a run that verifies too little still reports green. The target comes from GITHUB_BASE_REF or SYSTEM_PULLREQUEST_TARGETBRANCH, so to reproduce a pull request run locally, set one: GITHUB_BASE_REF=main mnci ci verify.
  • Native (cgo) apps are left out, as in the pipeline, since one agent cannot build them.
  • Log groups. Under GitHub Actions and Azure Pipelines each part is a collapsible group (::group::, ##[group]); locally it is a plain heading.
  • Checked against the guard it replaces. An integration spec runs the inline guard and this command against the same real git repository, with a recording stand-in for npx, across a push, a pull request, Azure's refs/heads/ form, a missing origin/<target> and an unresolvable one, and requires the same Nx commands in each.

mnci doctor: checking the invariants actually hold

Read-only — it never edits the workspace. Every failing finding names the command that fixes it (usually mnci upgrade), and it exits non-zero when anything failed, so it works as a CI step as well as a local command.

Every check corresponds to an invariant that has actually been violated, in this repo or in a workspace it generated. None are hypothetical; a check nobody has ever needed is noise that trains people to ignore the output.

| Check | The failure it catches | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | 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 | | @nx/eslint/plugin registered in nx.json | Without it npm run lint exits 0 while linting nothing | | The resolved eslint major | A declared range and an installed version are different things — manifests once said ^10 while the pin said 9 | | .npmrc matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed | | versionActions on publishable Dart/Python packages | Its absence aborts nx release for the whole workspace, not just that project | | nx sync:check | A stale TypeScript project reference that was never committed | | No runtime dependency in the root manifest | A dependency hoisted to the root, which reaches no consumer (the root is private) and makes @nx/rollup inline a private copy into the package that imports it — 14.5 KB to 832 KB on a real measurement. See "Where a dependency belongs" above | | Source maps enabled in every rollup config | A build that emits no .js.map, so every breakpoint in a .ts file stays grey and unbound with nothing reporting why. A rollup config is written once at add time, so an older workspace never fixes itself — mnci upgrade sweeps them | | No retired formatter is still configured | A leftover .prettierrc*, .oxfmtrc.json or oxlint.config.ts, or a prettier/oxlint/oxfmt devDependency, runs from no command line — which is what makes it dangerous, since an editor extension still resolves it and reformats on save, undoing Standard after every gate has passed |

Everything else is plain Nx, surfaced as a small curated set of root scripts — each a single cross-platform command:

| Script | Runs | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | npm run build | nx run-many -t build | | npm run lint | nx run-many -t lint | | npm run test | nx run-many -t test | | npm run typecheck | nx run-many -t typecheck — its own script because a bundler-built project's build strips types without reading them | | npm run affected | nx affected -t lint,typecheck,test,build (vs main) | | npm run graph | nx graph | | npm run release:preview | nx release --dry-run | | npm run python:install | fixed Python toolchain (ruff/pytest/build/twine) + editable-install every Python project — the same two guards CI runs, for local dev | | prepare | husky (commit-msg lint hook) |

Every add also wires local-dev commands

Every mnci add (and the inline internal-lib case) finishes by calling registerProjectCommands (project-scaffolding/post-generation.use-case.ts), which writes up to three root package.json scripts for the project just added:

| Script | Runs | When it's added | | -------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | <name>:build | nx run <name>:build | the kind has a build target (not every kind does — a private lib with nothing to publish, or a Python function app deployed as source, has none) | | <name>:qa | nx run <name>:lint && nx run <name>:test | always — every kind has both | | <name>:start | the kind's real local-dev command | only kinds with a genuine dev-server story — never a library |

The same three (when present) are appended as VS Code Tasks into the workspace's <workspace-name>.code-workspace file, so they also show up under Terminal → Run Task / the Command Palette — build/qa grouped accordingly, start marked isBackground since it runs a process that doesn't exit on its own. Re-running add for the same project name overwrites its own scripts/tasks rather than duplicating them.

Run and Debug: the launch section

Tasks are reachable only through Terminal → Run Task. The Run and Debug panel reads a separate launch section, so a workspace with tasks alone offers nothing in the dropdown people actually open. Every generated workspace therefore also gets four launch configurations, one per verify target:

| configuration | runs | | --- | --- | | mnci: build | npm run build | | mnci: test | npm run test | | mnci: lint | npm run lint | | mnci: typecheck | npm run typecheck |

Three details are load-bearing rather than incidental:

  • type: node-terminal, not node. nx run-many executes every target in a child process. A plain node launch attaches to the Nx parent alone, so a breakpoint inside a spec never binds; node-terminal runs the command in VS Code's JS Debug Terminal, which instruments children as they spawn. It also avoids a second trap — a node launch defaults to internalConsole, which renders none of Nx's progress output, so a build there looks like it has hung.
  • They drive npm run <script>, never a path into node_modules. The obvious program: node_modules/nx/bin/nx.js is wrong: Nx ships its bin at dist/bin/nx.js, and that path moves between versions. Driving the root script tracks whatever it does, so a change to the scripts reaches these for free.
  • cwd is ${workspaceFolder:<name>}, scoped by folder name. A bare ${workspaceFolder} is ambiguous the moment a second folder joins the workspace, and VS Code then refuses to resolve it — breaking all four at once.

Your own configurations are safe: mnci upgrade replaces only the entries named mnci: * and carries every other one through untouched.

:start resolves differently per kind — an existing generator target where one already exists, a small nx:run-commands target mnci writes where none did:

| Kind(s) | :start runs | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | react-app, node-app | nx run <name>:serve — the generator's own inferred dev-server target | | node-function-app, python-function-app | nx run <name>:start → func start (Azure Functions Core Tools, install separately — never a prerequisite for add itself) | | python-app | nx run <name>:start → python3 main.py — mnci writes a runnable main.py, since the plugin's own sample module has no entry point | | go-app | nx run <name>:start → go run . | | flutter-app | nx run <name>:start → flutter run -d chrome (web is the only platform this plugin builds for) | | every library, go-function-app | no :start at all — see below |

go-function-app is a known gap, not an oversight: unlike the Node and Python function-app kinds, it writes no host.json/custom-handler config, so there is nothing for func start to attach to. Shipping a :start script that would just fail felt worse than being upfront that it doesn't exist yet.

What new actually does

  1. npx create-nx-workspace@latest <name> --preset=ts — npm workspaces + TypeScript project references. Libraries get no project.json; targets are inferred from each project's tsconfig/package.json.
  2. Patches nx.json with the release opinion (the only config Nx has no default for): independent versioning from conventional commits, {projectName}@{version} tags, tag-only git (commit: false) — nothing is ever pushed to main; future runs resolve versions from tag names. Also fills in namedInputs.sharedGlobals with the root config files (eslint.config.mjs, eslint.config.mnci.mjs, tsconfig.base.json, package.json), without which nx affected on a pull request is blind to them: they live in no project, so changing one marked only the root pseudo-project — which has no lint/typecheck/test/build target — and the affected-scoped verify step ran nothing at all while reporting green.
  3. Writes two ESLint files, and only one of them is mnci's: eslint.config.mnci.mjs holds the whole linting opinion (one import from @mnci/eslint-config, plus a commented inventory naming every config block) and is rewritten on every upgrade; eslint.config.mjs is the file ESLint actually loads, imports that one, holds your blocks, and is written once and then never touched again. See Your half of the ESLint config below. There is no formatter config, because ESLint is the formatter. Also writes .npmrc (publish auth — see Publish auth below), commitlint.config.mjs, a husky commit-msg hook, the chosen CI provider's pipeline file(s) (azure-pipelines.yml and/or .github/workflows/ci.yml, --ci, default azure; github/both also gets .github/dependabot.yml — weekly dependency-update PRs), a <workspace-name>.code-workspace file (VS Code workspace configuration with folder structure, ESLint settings, recommended extensions, and an empty tasks array that mnci add fills in per project — see below — open it in VS Code via File > Open Workspace from File), and the curated root scripts.
  4. Installs the chosen stack (see below), husky + @commitlint/* for real, so versions resolve at generation time.

mnci upgrade: re-applying the overlay to an existing workspace

Every fix to workspace-overlay/overlay.use-case.ts — a release-config correction, a CI guard rewritten, a new Windows code path — only ever reached future mnci new calls until this existed; nothing let an already-generated workspace pick one up. mnci upgrade, run from the workspace root, closes that gap: it resolves the same options new would have and calls the exact same applyOverlay new itself calls — the one function that does every bit of mnci-owned file writing (nx.json's release/sync/generators/namedInputs.sharedGlobals/ mnci blocks, .npmrc, eslint.config.mnci.mjs (not eslint.config.mjs — see below), commitlint.config.mjs, .husky/commit-msg, the CI pipeline file(s), .devcontainer/devcontainer.json, the <workspace-name>.code-workspace file, and the curated root package.json scripts). Nothing else in the workspace — app/lib source, project.json targets from mnci add — is ever touched, and it finishes by running eslint --fix over the result, the same way new and every add do.

The .code-workspace file is the one partial case, and deliberately so: its folders, settings and extensions are regenerated, but the tasks array is read back and carried through unchanged. Those tasks are per-project state written by mnci add, not overlay-owned, so regenerating them wholesale would wipe every project's build/qa/start entry on upgrade.

The launch array is handled differently again — merged, not carried through. mnci owns the four mnci: * configurations and replaces them, while any configuration you added yourself survives. The asymmetry is deliberate: tasks are written by mnci add and the overlay has no idea which projects exist, whereas the launch entries are entirely overlay-authored and should track an upgrade.

upgrade also deletes things, which is stronger than the overwriting it has always done — one more reason to run git diff first, as the command's own output tells you to:

  • create-nx-workspace's .prettierrc and .vscode/, plus every formatter config a past mnci version wrote — .prettierrc.json, .prettierrc.mjs, .prettierignore, .oxfmtrc.json, oxlint.config.ts. None of them runs from a command line any more, and that is exactly what makes them dangerous: a globally installed esbenp.prettier-vscode or oxc.oxc-vscode still resolves one and still reformats on save, undoing Standard while lint stays green because the damage lands after the check. .vscode/ is superseded by the .code-workspace file.
  • every per-project eslint.config.* under apps/, libs/ and packages/. This is the migration path for a workspace generated before mnci owned linting: without it an upgrade would install the root config while each project kept linting against its own stale copy. The root config is never touched, and neither is a config anywhere outside those three directories.
mnci upgrade                          # re-apply from persisted config alone
mnci upgrade --agent windows-latest   # override one field; the override is
                                       # persisted too, so the next upgrade
                                       # remembers it

Where the options come from: mnci new now persists the full set it resolved (scope, registry, agent, variableGroup, ci, the stack) into nx.json's mnci block — previously only the stack was kept. upgrade reads that block back; an explicit flag on the upgrade command line always wins over the persisted value. A workspace generated before this was persisted (or hand-edited to remove a field) gets a clear, specific error naming the one flag needed (No npm scope found in nx.json's persisted config. Pass --scope explicitly.) rather than a prompt or a guess.

There is deliberately no diff preview or confirmation prompt built in: applyOverlay is a plain, deterministic file-writer (same content in, same content out, every time), and virtually every generated workspace is already a git repo — review the result with git diff before committing, the same way you'd review any other regenerated file. This does mean upgrade will overwrite hand customizations to any of the files it owns (e.g. an extra CI job appended by hand to the pipeline file) — git diff is exactly how you'd notice and re-apply those on top.

Stack: one choice asked up front

mnci new (run bare, or with flags) asks one question — the test runner. It is stored where every later mnci add honours it, so the whole workspace stays one stack:

| Question | Options | Default | Stored as / honoured via | | --------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------- | | --test-runner | jest | vitest | jest | nx.json generator unitTestRunner default; the hand-built function app follows it too |

Your half of the ESLint config

There are two root ESLint files, and the split exists because the old single-file layout lost work. eslint.config.mjs used to be mnci-owned and rewritten wholesale on every mnci upgrade — so a block appended to it, in the way a comment mnci itself wrote three lines above described, was deleted without a word. Worse, upgrade then tells you to run npm run format, so the first thing that happens after your overrides vanish is every file in the repository being rewritten against the rules you thought you had changed.

So:

| File | Owner | On mnci upgrade | | --- | --- | --- | | eslint.config.mnci.mjs | mnci | rewritten every time | | eslint.config.mjs | you | written once, then never touched |

eslint.config.mjs is what ESLint loads, because eslint.config.mjs is ESLint's own default filename — the file the tool looks for has to be the one you own, or the tool's default is the one mnci overwrites.

import mnci from './eslint.config.mnci.mjs'

export default [
  ...mnci(),
  { name: 'local/legacy-app-allows-any', files: ['apps/legacy/**/*.ts'], rules: { … } },
]

The owned file exports a function, not a resolved array, so options still reach @mnci/eslint-config from the file you own: ...mnci({ verticalSlices: ['packages/*/src/**/*.ts'] }). An array would have had nowhere to receive them — mnci's own repository passes verticalSlices, which is how that was caught.

Upgrading an existing workspace. If your eslint.config.mjs is still the old single-file one and you never edited it, mnci upgrade moves it onto the split for you: it replaces the file only when it provably holds nothing but mnci's own output. If you did edit it, it is left exactly as it is and mnci doctor reports that it no longer imports the rules, with the line to add. mnci does not rewrite that file any more — which is the point, and also why it cannot do this part for you.

Linting and formatting are unified across the workspace, from exactly one pair of config files. The rules are three lines importing @mnci/eslint-config; every @nx/* generator drops a config into the project it creates, and mnci add deletes it. Projects still get their lint target: @nx/eslint/plugin infers it by mapping config directories onto the project roots beneath them, so the root config covers them all. (Verified, not assumed — and the e2e enforces both "every project has a lint target" and "the root config genuinely reports violations in a project with no config of its own", because a future Nx change there would silently switch linting off workspace-wide.)

mnci runs eslint --fix itself at the end of new and every add, so a generated workspace passes its own lint immediately — Nx's generators emit semicolons and double quotes, and without that pass the first commit buries every real change under generator noise.

One linter, which is also the formatter. There is no --linter flag and no choice to make: @mnci/eslint-config carries code quality, type-aware rules and JavaScript Standard Style formatting in a single ESLint config at the root. npm run format is eslint . --fix --cache; there is no format:check, because lint already reports formatting as ordinary errors with a rule name, a line and a column.

That is the point of the arrangement rather than a side effect. A formatter and a linter that both hold style opinions have to be kept in agreement, and the older setup dodged that only by having ESLint hold no style opinion at all — which is exactly why a formatting mistake produced no squiggle and no message in the editor. It also makes space-before-function-paren enforceable for the first time: every Prettier-compatible formatter, oxfmt included, rewrites function f (a) back to function f(a).

npm run lint checks one thing npm run format does not. lint is nx run-many -t lint; format is a bare eslint . --fix --cache. The @nx/dependency-checks rule needs the Nx project graph, and outside a target it prints No cached ProjectGraph is available. The rule will be skipped. So a dependency problem shows up in lint and never in format, your editor, or a pre-commit hook.

That asymmetry is now a safety property rather than a hazard. The rule is fixable, and format passes --fix; when the graph was warm — which it is after any nx command in the same workspace — a format run could and did rewrite package.json. @mnci/eslint-config turns off the two checks whose fixers do that (checkObsoleteDependencies, checkVersionMismatches), so neither path is destructive, and the skip means format cannot reach a manifest at all. Warm the graph deliberately (npx nx show projects) if you want the rule evaluated in format too; mnci does not, because it would make every format run pay for a graph computation to enforce what lint already gates.

Upgrading an older workspace. mnci upgrade deletes every config a previous version could have written for a second tool — .prettierrc, .prettierrc.json, .prettierrc.mjs, .prettierignore, .oxfmtrc.json, oxlint.config.ts — and drops prettier, oxlint, oxfmt and @mnci/oxlint-config from devDependencies. Both halves matter: the files are inert from the command line, but an editor extension still resolves them, and the VS Code extension resolves a formatter from the project's dependencies, so a stale declaration is enough to reformat on save against an opinion nothing checks. mnci doctor reports a workspace that has not been upgraded yet.

--into: bootstrapping into a repository that already exists

create-nx-workspace <name> creates the directory itself and exits with DIRECTORY_EXISTS when one is already there. That rules out the most common way a repository actually starts: the host creates it, you clone it, and the clone holds a .git directory, a README and a licence. Doing it by hand means generating into a temp parent, copying everything except .git and node_modules across, and reinstalling — four steps, each of which can quietly lose a file.

mnci new --into <dir> is that, done once and tested:

git clone [email protected]:me/my-repo.git
cd my-repo
mnci new --into .

The workspace name defaults to the directory's name, because the directory is already named and retyping it is a way to get the two out of step; pass a name argument to override it.

What it does with the files that are already there is the whole of the risk, so every case is decided in advance:

| Already in the directory | What happens | | --- | --- | | .git | Never touched. Keeping it is the point. | | README.md, LICENSE* | Kept. The generator's README is boilerplate; yours is usually the only hand-written file in the repository. | | .gitignore | Merged. Your lines stay, and the generated ones (.nx/cache, dist, out-tsc, …) are appended under a labelled heading. Lines already present are not repeated, so it is idempotent. | | Anything else the new workspace also writes | Refused, naming every collision, before a single file is written. | | Anything the new workspace does not write | Left alone. |

Two checks, not one. A directory holding package.json, nx.json, tsconfig.base.json, node_modules, apps/, libs/ or packages/ is rejected before anything is generated — it is a project already, and the answer there is mnci upgrade, not mnci new. The full collision check needs the generated tree, so it runs afterwards, but still before the first write: a refusal leaves the target byte-identical to how it was found, and the staging copy is discarded.

Adopting a flat Go module

A repository with its own go.mod, a root main.go and internal/ packages is the other common starting point, and --into takes it as it is. Measured on a clone of a real one (twelve internal/ packages, its own release.yml):

  • go.mod and go.sum are never touched. add go-* skips the module bootstrap when a go.mod exists, so the module path, the require lines and the Go version survive byte for byte, and no go.work is created.
  • The Go plugin is registered anyway. The bootstrap that registers it in nx.json is the one that is skipped, which used to leave the plugin installed and unlisted: every target worked, and Nx had no Go project graph, so nx affected skipped an app that imports a changed library, silently. add go-* now registers it, mnci upgrade repairs a workspace that adopted before, and mnci doctor fails while Go projects exist and it is missing.
  • Your own workflow coexists. A release workflow that fires on v* tags and the generated ci.yml, which fires on pushes and pull requests to main, do not overlap. mnci tags a released app <name>@<version>, so once --release and its GitHub Release assets do what yours did, delete the old workflow.
  • The format step can fail on a file you already had, and says so without failing the run: a UTF-16 .json at the root made eslint . report a parse error. The root lint covers root-level files, so a repository's existing JSON, Markdown and YAML are now inside it; fix or delete what it names.

Landing the code is mechanical, so it is a recipe, not a command (git mv and one import rewrite are the whole job, and a command would have to guess your layout). With youtube-downloader as the module path, mvd-cli as the app and mvd-core as the library:

mnci add go-app mvd-cli
mnci add go-internal-lib mvd-core

# The generated starters are placeholders: drop them.
rm apps/mvd-cli/main.go apps/mvd-cli/main_test.go
rm -r libs/mvd-core/mvdcore

# git mv, not mv, so `git log --follow` reaches the history before the move.
git mv main.go apps/mvd-cli/main.go
for slice in internal/*/; do git mv "$slice" "libs/mvd-core/$(basename "$slice")"; done

# Rewrite the import paths (macOS: sed -i '').
grep -rl 'youtube-downloader/internal/' --include=*.go apps libs \
  | xargs sed -i 's#youtube-downloader/internal/#youtube-downloader/libs/mvd-core/#g'

npx nx run-many -t test,build --projects=mvd-cli,mvd-core

Use nx and not a bare go test ./... from the root: ./... also walks node_modules, where npm packages that contain Go code sit (one did here). On the repository measured, this left go build clean, every test passing, 62 renames detected by git, and the app depending on the library in the project graph. The e2e (go adoption) runs this recipe on a fixture and asserts each of those.

Not measured here: whether golangci-lint, which is what each Go project's lint runs, passes on a codebase that has never been linted. Run nx run <lib>:lint before relying on it, and expect to fix or silence what it reports.

Layout convention = release scoping

| Directory | Contents | Released? | | ------------------ | -------------------------------------------------------------- | -------------------------------------- | | apps/ | React / Node / Python / Go / Flutter apps (plain or Functions) | Never (packed into the drop) | | apps/ (tagged) | VS Code extensions (type:vscode-extension) | Yes — nx release, vsce publish | | apps/ (tagged) | Go apps added with --release (release:go) | Yes — nx release tag, zips on the GitHub Release | | packages/ | Publishable npm libraries, plus Go and Dart packages | Yes — nx release, per-package tags | | python-packages/ | Publishable Python packages (hatchling wheels) | Yes — twine upload (Azure Artifacts) | | libs/ | Internal libraries (TS, Python, Go or Dart), never published | Never |

The directory is very nearly the whole model — one exception, and it is a bug fix rather than a nicety. go-lib also lives in packages/, but a Go package has no per-project manifest (mnci puts every Go project in one root go.mod), so Nx's default versionActions looks for a package.json that is not there and aborts while building the release graph — killing nx release for the whole workspace, not just the Go project. It is therefore excluded with !tag:type:go-lib, which is also the semantically correct call: one module means its packages have no independent versions to bump. A publishable Dart package in packages/ needs no such exclusion — pubspec.yaml has a real version: field, and @mnci/nx-flutter stamps a versionActions override that reads it. Publishable Python packages get their own python-packages/ dir so the npm nx release (packages/*) is never entangled with Python publishing.

The second exception goes the other way: a VS Code extension lives in apps/ (it is an application, and the slice lint treats it as one) but is versioned and published like a package. release.projects matches it by its tag, tag:type:vscode-extension, rather than by path: the array is mnci's and is rewritten by every mnci upgrade, so a hand-added apps/my-extension would be lost, while a tag matcher is the same on every upgrade and covers every extension there will ever be.

The third is an opt-in: a Go app added with mnci add go-app <name> --release is tagged release:go and joins the release the same way. Without the flag an app stays unreleased, because most Go apps are internal tools. See Releasing a Go app in the Go section.

Every kind builds to its own Nx-default output location (apps/<name>/dist, packages/<name>/dist, ...) — no post-generation build-output rewiring for any kind. mnci add is pure delegation to the official generators; each one's own default is left exactly as-is.

Published packages CAN depend on internal libraries

Import an internal lib from an npm-lib directly — and do not add it to the npm-lib's dependencies (npm workspaces links every workspace member into the root node_modules regardless):

// packages/sdk/src/lib/sdk.ts
import { utils } from '@demo/utils' // libs/utils — private, never published

It works because npm-libs are rollup bundles: @nx/rollup's withNx externalizes exactly what the manifest declares (dependencies + peerDependencies), so real npm deps stay external and declared, while the undeclared internal lib is compiled from source INTO the bundle — the private name never reaches the published package.json. Trade-off: the published output is a single bundle (no per-file deep imports).

React apps go the other way (Vite bundles everything by default), and the e2e proves both directions for real: unlike the published npm-lib, which must keep real npm dependencies external (declared, not bundled) for the published tarball to install correctly downstream, a react-app build has no install step at deploy/runtime, so it inlines everything — the private internal lib AND real npm dependencies alike.

Node apps (node-app/node-function-app) are a third case: @nx/node:application's esbuild build is non-bundled — it transpiles each file individually and mirrors the workspace tree into dist, so nothing is ever textually inlined. A private internal lib is compiled by its own tsc build and copied into dist at its own path (resolved by a real require at run time, the same way npm workspaces resolve it during development); a real npm dependency stays a real require too, resolved from node_modules — present locally, or installed at deploy time (see "How Node apps work" below).

Cross-project imports (@scope/lib) resolve through TypeScript project references under --preset=ts, and those references are maintained by nx sync, not by the generators. mnci add runs nx sync for you right after generation — but references also go stale any time you hand-edit a file to add a new cross-project import later (nothing about that is an mnci add, so that step can't catch it). For that case every generated workspace sets sync.applyChanges: true in nx.json: --preset=ts already registers the @nx/js:typescript-sync generator on the build/typecheck targets, so instead of just prompting ("Would you like to sync the identified changes?") on your next nx build/typecheck/affected, Nx fixes the references automatically — no prompt, no manual npx nx sync. A brand-new package may still need one VSCode window reload to be picked up by the TypeScript server.

applyChanges only affects interactive runs, by design: CI always runs sync generators in dry-run mode and fails instead of silently patching an ephemeral checkout that never gets committed. That's what the pipeline's nx sync:check step (below) surfaces early — if it fails, run npx nx sync locally and commit the result.

CI (Azure Pipelines and/or GitHub Actions, any agent OS)

mnci new asks which CI provider(s) to write a pipeline file for (--ci, default azure): azure writes azure-pipelines.yml, github writes .github/workflows/ci.yml, both writes both — pick github for a GitHub-hosted repo, or both while migrating between the two. Whichever provider(s), the pipeline does the exact same thing: both files are built from the same shared guard scripts (workspace-overlay/overlay.use-case.ts's PYTHON_INSTALL_GUARD, PACK_APPS_GUARD, releaseGuard, AFFECTED_OR_ALL_GUARD), so they can never drift on what CI actually runs — only the provider's own syntax differs. That matters most for the last of those: the two providers detect a pull request through different environment variables, so the guard reads both, and a provider-specific copy would change what CI verifies rather than merely how it is spelled.

The pipeline contains no bash and no PowerShell: every step is a built-in task/action or a single-line git/npm/npx/node command that cmd.exe and sh execute identically, so it runs unchanged on Linux, macOS and Windows agents. The build agent/runner is your choice at mnci new (--agent, default ubuntu-latest): on Azure a Microsoft-hosted image (ubuntu-/windows-/macos-…) becomes pool.vmImage, anything else a self-hosted pool.name; on GitHub the same value is passed straight through as runs-on: (GitHub's own hosted runner labels already match the common Azure vmImage names, and a self-hosted label is just as valid there).

Every run (PR and main) installs dependencies, then runs npm audit (non-blocking) and, once the Python toolchain is installed, pip-audit (also non-blocking) — visibility, not enforcement: verified empirically that a real npm audit on this monorepo's own tree flagged nothing but already-latest upstream packages (nx, verdaccio) bundling their own not- yet-patched transitive dependencies, nothing an edit to this workspace's manifest could fix. A hard-failing audit step would turn CI red for a problem with no user-actionable fix, for as long as upstream took to patch it — so both steps always exit 0 regardless of findings, surfacing results as a clearly labelled section in every CI log instead. The actionable response to a real finding (a targeted overrides entry on just the vulnerable transitive package) is exactly what this monorepo's own fix(deps) commit did — a manual, reviewed response, not something CI attempts automatically.

Then nx sync:check (fails fast and clearly if the workspace wasn't synced+committed locally — see above), then one verify step running lint,typecheck,test,build. typecheck is in that list because a bundler-built project strips types without reading them, so build passing proves nothing about type correctness.

That step verifies the affected projects on a pull request and every project on anything else — including a push to main, so a release is always verified in full. There is deliberately no separate npm run lint step: that is nx run-many -t lint, a strict subset of the list above, and on an affected-scoped PR it would re-lint every project and throw the benefit away.

There is no format:check step either, and its removal is not a saving but a consequence: lint reports formatting itself now, so a second step would run the same binary twice over the same tree.

Every fallback in that step verifies everything: no PR target branch, an unresolvable merge-base (shallow clone, absent remote branch), any non-PR run. That direction is deliberate — resolving the base too wide costs a few minutes, while resolving it too narrow means CI runs almost nothing, reports green and has verified nothing. The base is a git merge-base, not either provider's "base SHA" field and not the GitHub-only nrwl/nx-set-shas, so one mechanism serves both providers and is correct in each by construction.

Pushes to main then:

  • Pack all apps — each app's package target zips its build output into dist/drop/<type>-<name>.zip (e.g. node-function-app-api.zip, react-app-web.zip); the whole dist/drop is published as the drop artifact.
  • Tag the run per app (Azure only) — one build tag per zip, exactly <type>-<name> (derived from the zip filenames, so the tag can never drift from the artifact). A classic Azure release/CD pipeline keys its trigger off these; GitHub Actions has no equivalent mechanism, so the drop artifact (one zip per app inside it) is the portable substitute there.
  • Release — version, tag and publish — one npx nx release --yes covering npm (packages/*), Python (python-packages/*) and C# (a csharp-lib's NuGet publish): version bump from conventional commits → {projectName}@{version} git tag pushed to main (tag-only, never a commit) → publish to the feed (npm via .npmrc, Python via twine when an Azure feed is configured — installed from the generated requirements-dev.txt, no uv, no Poetry — and a csharp-lib's own nx-release-publish target running dotnet pack/dotnet nuget push when NUGET_PAT is set, self-gating to a no-op otherwise). Reuses the base64 PAT, decoded to the raw token twine needs for the Python publish and to the raw token NuGet's %NUGET_PAT% substitution needs. Skipped cleanly when there is nothing to release. A guarded step installs the fixed Python toolchain (ruff/pytest/build/twine/pip-audit) before any Python target runs, skipped cleanly on a workspace with no Python projects. Go and Flutter libraries publish by git tag only — see their own sections below — so neither needs a step here. On a --ci=github workspace this same step also creates a GitHub Release per project, with a changelog Nx generates from conventional commits — nx release pushes the tag itself here (needs GITHUB_TOKEN, which GitHub Actions provides for free under the workflow's own contents: write permission), so there's no separate explicit git push origin --tags step on this provider. --ci=azure and --ci=both keep today's behaviour (no GitHub Release, explicit tag push) — GitHub Release creation only turns on when GitHub Actions is the only configured provider, since that's the one case a GITHUB_TOKEN is guaranteed to exist.

Publish auth

The generated .npmrc differs by registry kind, because the honest answer does.

--registry azure-artifacts routes the workspace's own scope to the feed and supplies the feed's credentials:

@my:registry=https://pkgs.dev.azure.com/<org>/<proj>/_packaging/<feed>/npm/registry/
//pkgs.dev.azure.com/.../npm/registry/:username=AzureArtifacts
//pkgs.dev.azure.com/.../npm/registry/:_password=${PAT}

Scope routing is real protection here, not decoration: npm prefers a scope's registry over the global one when publishing a scoped package, so @my/* cannot reach npmjs.org by accident. Verified against a real registry — with only the scope line set, npm reports Publishing to <feed> — and again from a generated workspace, whose npm publish --dry-run targets the feed.

Only the scope is routed, deliberately. A global registry= would push every install through the feed as well, so npm ci would need feed auth just to fetch public packages; as generated, public dependencies still come from npmjs.org and a developer with no PAT set can install normally.

--registry npm gets the auth line and nothing else:

//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

There is no @scope:registry line, and the generated file explains why: npmjs.org is already the default, so routing the scope there changes nothing — and calling it protection against an accidental public publish would be false, because the public registry is the intended target. Worth stating plainly because this file previously claimed exactly that protection while emitting no routi