[
  {
    "path": ".github/workflows/publish-commit.yml",
    "content": "name: Publish Any Commit\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n    tags:\n      - '!**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4.0.0\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      - run: pnpm dlx pkg-pr-new publish --pnpm\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    uses: sxzz/workflows/.github/workflows/release.yml@v1\n    with:\n      publish: true\n    permissions:\n      contents: write\n      id-token: write\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, lts/*]\n\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install\n        run: pnpm install\n      - name: Lint\n        run: pnpm run lint\n      - name: Typecheck\n        run: pnpm run typecheck\n      - name: Test\n        run: pnpm run test\n"
  },
  {
    "path": ".gitignore",
    "content": "_storage.json\n.cache\n.DS_Store\n.env\n.eslintcache\n.ghfs\n.grunt\n.idea\n.lock-wscript\n.next\n.node_repl_history\n.npm\n.nuxt\n.nyc_output\n.serverless\n.vuepress/dist\n.yarn-integrity\n*.log\n*.pid\n*.pid.lock\n*.seed\nbower_components\nbuild/Release\ncoverage\ndist\njspm_packages\nlib-cov\nlogs\nnode_modules\nnpm-debug.log*\npids\ntypings\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  // Enable the ESlint flat config support\n  \"eslint.experimental.useFlatConfig\": true,\n\n  // Disable the default formatter, use eslint instead\n  \"prettier.enable\": false,\n  \"editor.formatOnSave\": false,\n\n  // Auto fix\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll\": \"explicit\",\n    \"source.organizeImports\": \"never\"\n  },\n\n  // Silent the stylistic rules in you IDE, but still auto fix them\n  \"eslint.rules.customizations\": [\n    { \"rule\": \"style/*\", \"severity\": \"off\" },\n    { \"rule\": \"*-indent\", \"severity\": \"off\" },\n    { \"rule\": \"*-spacing\", \"severity\": \"off\" },\n    { \"rule\": \"*-spaces\", \"severity\": \"off\" },\n    { \"rule\": \"*-order\", \"severity\": \"off\" },\n    { \"rule\": \"*-dangle\", \"severity\": \"off\" },\n    { \"rule\": \"*-newline\", \"severity\": \"off\" },\n    { \"rule\": \"*quotes\", \"severity\": \"off\" },\n    { \"rule\": \"*semi\", \"severity\": \"off\" }\n  ],\n\n  // Enable eslint for all supported languages\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\",\n    \"vue\",\n    \"html\",\n    \"markdown\",\n    \"json\",\n    \"jsonc\",\n    \"yaml\"\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Anthony Fu <https://github.com/antfu>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ni\n\n~~*`npm i` in a yarn project, again? F\\*\\*k!*~~\n\n**ni** - use the right package manager\n\n<br>\n\n<pre>\n<code>\nnpm i -g <b>@antfu/ni</b>\n</code>\n</pre>\n\n<a href='https://docs.npmjs.com/cli/v6/commands/npm'>npm</a> · <a href='https://yarnpkg.com'>yarn</a> · <a href='https://pnpm.io/'>pnpm</a> · <a href='https://bun.sh/'>bun</a> · <a href='https://deno.land/'>deno</a>\n\n<br>\n\n### `ni` - install\n\n```bash\nni\n\n# npm install\n# yarn install\n# pnpm install\n# bun install\n# deno install\n```\n\n```bash\nni vite\n\n# npm i vite\n# yarn add vite\n# pnpm add vite\n# bun add vite\n# deno add vite\n```\n\n```bash\nni @types/node -D\n\n# npm i @types/node -D\n# yarn add @types/node -D\n# pnpm add -D @types/node\n# bun add -d @types/node\n# deno add -D @types/node\n```\n\n```bash\nni -P\n\n# npm i --omit=dev\n# yarn install --production\n# pnpm i --production\n# bun install --production\n# (deno not supported)\n```\n\n```bash\nni --frozen\n\n# npm ci\n# yarn install --frozen-lockfile (Yarn 1)\n# yarn install --immutable (Yarn Berry)\n# pnpm install --frozen-lockfile\n# bun install --frozen-lockfile\n# deno install --frozen\n```\n\n```bash\nni -g eslint\n\n# npm i -g eslint\n# yarn global add eslint (Yarn 1)\n# pnpm add -g eslint\n# bun add -g eslint\n# deno install eslint\n\n# this uses default agent, regardless your current working directory\n```\n\n```bash\nni -i\n\n# interactively select the dependency to install\n# search for packages by name\n```\n\n<details>\n<summary>catalogs support</summary>\n\n> Since v29.0.0\n\nWhen working in a pnpm workspace with [catalogs](https://pnpm.io/catalogs) configured in `pnpm-workspace.yaml`, `ni` automatically enters **catalog mode**. Instead of adding packages with pinned versions, it writes `catalog:` references into `package.json` and updates the workspace catalog.\n\n```bash\n# Given pnpm-workspace.yaml with:\n#   catalogs:\n#     prod:\n#       react: ^18.3.0\n\nni react\n# → detects react in \"prod\" catalog\n# → writes \"react\": \"catalog:prod\" to package.json\n# → runs pnpm install\n\nni lodash\n# → lodash not in any catalog\n# → prompts to select a catalog (or skip)\n# → fetches latest version, updates pnpm-workspace.yaml\n# → writes \"lodash\": \"catalog:prod\" to package.json\n# → runs pnpm install\n```\n\nWhen only a default catalog (`catalog:` top-level) is used, new packages are added directly without prompting. When only named catalogs exist, the default catalog is never offered.\n\nFlags like `-D` are respected — the catalog ref is written to the correct `package.json` section:\n\n```bash\nni typescript -D\n# → writes \"typescript\": \"catalog:dev\" to devDependencies\n```\n\nUse `-w` / `--workspace` to target the workspace root `package.json`:\n\n```bash\nni react -w\n# → writes catalog ref to workspace root package.json\n```\n\nTo disable catalog mode, set `catalog=false` in `~/.nirc` or `NI_CATALOG=false` environment variable.\n\n</details>\n\n<br>\n\n### `nr` - run\n\n```bash\nnr dev --port=3000\n\n# npm run dev -- --port=3000\n# yarn run dev --port=3000\n# pnpm run dev --port=3000\n# bun run dev --port=3000\n# deno task dev --port=3000\n```\n\n```bash\nnr\n\n# interactively select the script to run\n# supports https://www.npmjs.com/package/npm-scripts-info convention\n```\n\n```bash\nnr -\n\n# rerun the last command\n```\n\n```bash\nnr -p\nnr -p dev\n\n# interactively select the package and script to run\n```\n\n<details>\n<summary>shell completion scripts</summary>\n\n```bash\n# Add completion script for bash\nnr --completion-bash >> ~/.bashrc\n\n# Add completion script for zsh\n# For zim:fw\nmkdir -p ~/.zim/custom/ni-completions\nnr --completion-zsh > ~/.zim/custom/ni-completions/_ni\necho \"zmodule $HOME/.zim/custom/ni-completions --fpath .\" >> ~/.zimrc\nzimfw install\n\n# Add completion script for fish\nmkdir -p ~/.config/fish/completions\nnr --completion-fish > ~/.config/fish/completions/nr.fish\n```\n\n</details>\n\n<br>\n\n### `nlx` - download & execute\n\n```bash\nnlx vitest\n\n# npx vitest\n# yarn dlx vitest\n# pnpm dlx vitest\n# bunx vitest\n# deno run npm:vitest\n```\n\n<br>\n\n### `nup` - upgrade\n\n```bash\nnup\n\n# npm upgrade\n# yarn upgrade (Yarn 1)\n# yarn up (Yarn Berry)\n# pnpm update\n# bun update\n# deno upgrade\n```\n\n```bash\nnup -i\n\n# (not available for npm)\n# yarn upgrade-interactive (Yarn 1)\n# yarn up -i (Yarn Berry)\n# pnpm update -i\n# bun update -i\n# deno outdated -u -i\n```\n\n<br>\n\n### `nun` - uninstall\n\n```bash\nnun webpack\n\n# npm uninstall webpack\n# yarn remove webpack\n# pnpm remove webpack\n# bun remove webpack\n# deno remove webpack\n```\n\n```bash\nnun\n\n# interactively multi-select\n# the dependencies to remove\n```\n\n```bash\nnun -g silent\n\n# npm uninstall -g silent\n# yarn global remove silent\n# pnpm remove -g silent\n# bun remove -g silent\n# deno uninstall -g silent\n```\n\n<br>\n\n### `nci` - clean install\n\n```bash\nnci\n\n# npm ci\n# yarn install --frozen-lockfile\n# pnpm install --frozen-lockfile\n# bun install --frozen-lockfile\n# deno cache --reload\n```\n\n<br>\n\n### `nd` - dedupe dependencies\n\n```bash\nnd\n\n# npm dedupe\n# yarn dedupe\n# pnpm dedupe\n```\n\n<br>\n\n### `na` - agent alias\n\n```bash\nna\n\n# npm\n# yarn\n# pnpm\n# bun\n# deno\n```\n\n```bash\nna run foo\n\n# npm run foo\n# yarn run foo\n# pnpm run foo\n# bun run foo\n# deno task foo\n```\n\n<br>\n\n### Global Flags\n\n```bash\n# ?               | Print the command execution depends on the agent\nni vite ?\n\n# -C              | Change directory before running the command\nni -C packages/foo vite\nnr -C playground dev\n\n# -v, --version   | Show version number\nni -v\n\n# -h, --help      | Show help\nni -h\n```\n\n<br>\n\n### Config\n\n```ini\n; ~/.nirc\n\n; fallback when no lock found\ndefaultAgent=npm # default \"prompt\"\n\n; for global installs\nglobalAgent=npm\n\n; use node --run instead of package manager run command (requires Node.js 22+)\nrunAgent=node\n\n; prefix commands with sfw\nuseSfw=true\n\n; use catalog mode when catalogs are detected (default true)\ncatalog=true\n```\n\n```bash\n# ~/.bashrc\n\n# custom configuration file path\nexport NI_CONFIG_FILE=\"$HOME/.config/ni/nirc\"\n\n# environment variables have higher priority than config file if presented\nexport NI_DEFAULT_AGENT=\"npm\" # default \"prompt\"\nexport NI_GLOBAL_AGENT=\"npm\"\nexport NI_USE_SFW=\"true\"\nexport NI_CATALOG=\"false\" # disable catalog mode\n```\n\n```ps\n# for Windows\n\n# custom configuration file path in PowerShell accessible within the `$profile` path\n$Env:NI_CONFIG_FILE = 'C:\\to\\your\\config\\location'\n```\n\n<br>\n\n### Automatic installation\n\nYou can set `NI_AUTO_INSTALL=true` to enable automatic installation.\n\nIf the corresponding package manager (**npm**, **yarn**, **pnpm**, **bun**, or **deno**) is not installed, it will install it globally before running the command.\n\n### Integrations\n\n#### Homebrew\n\nYou can install ni with [Homebrew](https://brew.sh/):\n\n```bash\nbrew install ni\n```\n\n#### asdf\n\nYou can also install ni via the [3rd-party asdf-plugin](https://github.com/CanRau/asdf-ni.git) maintained by [CanRau](https://github.com/CanRau)\n\n```bash\n# first add the plugin\nasdf plugin add ni https://github.com/CanRau/asdf-ni.git\n\n# then install the latest version\nasdf install ni latest\n\n# and make it globally available\nasdf global ni latest\n```\n\n### How?\n\n**ni** assumes that you work with lock-files (and you should).\n\nBefore `ni` runs the command, it detects your `yarn.lock` / `pnpm-lock.yaml` / `package-lock.json` / `bun.lock` / `bun.lockb` / `deno.json` / `deno.jsonc` to know the current package manager (or `packageManager` field in your packages.json if specified) using the [package-manager-detector](https://github.com/antfu-collective/package-manager-detector) package and then runs the corresponding [package-manager-detector command](https://github.com/antfu-collective/package-manager-detector/blob/main/src/commands.ts).\n\n### Trouble shooting\n\n#### Conflicts with PowerShell\n\nPowerShell comes with a built-in alias `ni` for the `New-Item` cmdlet. To remove the alias in your current PowerShell session in favor of this package, use the following command:\n\n```PowerShell\n'Remove-Item Alias:ni -Force -ErrorAction Ignore'\n```\n\nIf you want to persist the changes, you can add them to your PowerShell profile. The profile path is accessible within the `$profile` variable. The ps1 profile file can normally be found at\n\n- PowerShell 5 (Windows PowerShell): `C:\\Users\\USERNAME\\Documents\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1`\n- PowerShell 7: `C:\\Users\\USERNAME\\Documents\\PowerShell\\Microsoft.PowerShell_profile.ps1`\n- VSCode: `C:\\Users\\USERNAME\\Documents\\PowerShell\\Microsoft.VSCode_profile.ps1`\n\nYou can use the following script to remove the alias at shell start by adding the above command to your profile:\n\n```PowerShell\nif (-not (Test-Path $profile)) {\n  New-Item -ItemType File -Path (Split-Path $profile) -Force -Name (Split-Path $profile -Leaf)\n}\n\n$profileEntry = 'Remove-Item Alias:ni -Force -ErrorAction Ignore'\n$profileContent = Get-Content $profile\nif ($profileContent -notcontains $profileEntry) {\n  (\"`n\" + $profileEntry) | Out-File $profile -Append -Force -Encoding UTF8\n}\n```\n\n#### `nx`, `nix` and `nu` are no longer available\n\nWe renamed `nx`/`nix` and `nu` to `nlx` and `nup` to avoid conflicts with the other existing tools - [nx](https://nx.dev/), [nix](https://nixos.org/) and [nushell](https://www.nushell.sh/). You can always alias them back on your shell configuration file (`.zshrc`, `.bashrc`, etc).\n\n```bash\nalias nx=\"nlx\"\n# or\nalias nix=\"nlx\"\n# or\nalias nu=\"nup\"\n```\n"
  },
  {
    "path": "bin/na.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/na.mjs'\n"
  },
  {
    "path": "bin/nci.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nci.mjs'\n"
  },
  {
    "path": "bin/nd.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nd.mjs'\n"
  },
  {
    "path": "bin/ni.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/ni.mjs'\n"
  },
  {
    "path": "bin/nlx.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nlx.mjs'\n"
  },
  {
    "path": "bin/nr.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nr.mjs'\n"
  },
  {
    "path": "bin/nun.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nun.mjs'\n"
  },
  {
    "path": "bin/nup.mjs",
    "content": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nup.mjs'\n"
  },
  {
    "path": "eslint.config.js",
    "content": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  pnpm: true,\n})\n  .removeRules(\n    'markdown/heading-increment',\n    'e18e/prefer-static-regex',\n  )\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@antfu/ni\",\n  \"type\": \"module\",\n  \"version\": \"29.0.0\",\n  \"packageManager\": \"pnpm@10.32.1\",\n  \"description\": \"Use the right package manager\",\n  \"author\": \"Anthony Fu <anthonyfu117@hotmail.com>\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/antfu-collective/ni#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/antfu-collective/ni.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/antfu-collective/ni/issues\"\n  },\n  \"exports\": {\n    \".\": \"./dist/index.mjs\",\n    \"./na\": \"./dist/na.mjs\",\n    \"./nci\": \"./dist/nci.mjs\",\n    \"./nd\": \"./dist/nd.mjs\",\n    \"./ni\": \"./dist/ni.mjs\",\n    \"./nlx\": \"./dist/nlx.mjs\",\n    \"./nr\": \"./dist/nr.mjs\",\n    \"./nun\": \"./dist/nun.mjs\",\n    \"./nup\": \"./dist/nup.mjs\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"types\": \"./dist/index.d.mts\",\n  \"bin\": {\n    \"ni\": \"bin/ni.mjs\",\n    \"nci\": \"bin/nci.mjs\",\n    \"nr\": \"bin/nr.mjs\",\n    \"nup\": \"bin/nup.mjs\",\n    \"nd\": \"bin/nd.mjs\",\n    \"nlx\": \"bin/nlx.mjs\",\n    \"na\": \"bin/na.mjs\",\n    \"nun\": \"bin/nun.mjs\"\n  },\n  \"files\": [\n    \"bin\",\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=20.19.0\"\n  },\n  \"scripts\": {\n    \"prepublishOnly\": \"npm run build\",\n    \"dev\": \"tsx src/commands/ni.ts\",\n    \"nr\": \"tsx src/commands/nr.ts\",\n    \"build\": \"tsdown\",\n    \"release\": \"bumpp\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"prepare\": \"simple-git-hooks\",\n    \"lint\": \"eslint\",\n    \"test\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"fzf\": \"catalog:prod\",\n    \"package-manager-detector\": \"catalog:prod\",\n    \"tinyexec\": \"catalog:prod\",\n    \"tinyglobby\": \"catalog:prod\"\n  },\n  \"inlinedDependencies\": {\n    \"fast-npm-meta\": \"1.4.2\",\n    \"yaml\": \"2.8.2\",\n    \"pnpm-workspace-yaml\": \"1.6.0\",\n    \"ini\": \"6.0.0\",\n    \"kleur\": \"4.1.5\",\n    \"@posva/prompts\": \"2.4.4\",\n    \"sisteransi\": \"1.0.5\",\n    \"isexe\": \"4.0.0\",\n    \"which\": \"6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"catalog:dev\",\n    \"@posva/prompts\": \"catalog:prod-inlined\",\n    \"@types/ini\": \"catalog:dev\",\n    \"@types/node\": \"catalog:dev\",\n    \"@types/which\": \"catalog:dev\",\n    \"bumpp\": \"catalog:dev\",\n    \"eslint\": \"catalog:dev\",\n    \"fast-npm-meta\": \"catalog:prod-inlined\",\n    \"ini\": \"catalog:prod-inlined\",\n    \"lint-staged\": \"catalog:dev\",\n    \"pnpm-workspace-yaml\": \"catalog:prod-inlined\",\n    \"simple-git-hooks\": \"catalog:dev\",\n    \"taze\": \"catalog:dev\",\n    \"tsdown\": \"catalog:dev\",\n    \"tsx\": \"catalog:dev\",\n    \"typescript\": \"catalog:dev\",\n    \"vitest\": \"catalog:dev\",\n    \"which\": \"catalog:prod-inlined\"\n  },\n  \"simple-git-hooks\": {\n    \"pre-commit\": \"pnpm lint-staged\"\n  },\n  \"lint-staged\": {\n    \"*\": \"eslint --fix\"\n  }\n\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "shellEmulator: true\n\ntrustPolicy: no-downgrade\ntrustPolicyIgnoreAfter: 604800 # 7 days\n\npackages: []\ncatalogs:\n  dev:\n    '@antfu/eslint-config': ^7.7.2\n    '@types/ini': ^4.1.1\n    '@types/node': ^25.5.0\n    '@types/which': ^3.0.4\n    bumpp: ^10.4.1\n    eslint: ^10.0.3\n    lint-staged: ^16.4.0\n    simple-git-hooks: ^2.13.1\n    taze: ^19.10.0\n    tsdown: ^0.21.2\n    tsx: ^4.21.0\n    typescript: ^5.9.3\n    vitest: ^4.1.0\n  prod:\n    fzf: ^0.5.2\n    package-manager-detector: ^1.6.0\n    tinyexec: ^1.0.4\n    tinyglobby: ^0.2.15\n  prod-inlined:\n    '@posva/prompts': ^2.4.4\n    fast-npm-meta: ^1.4.2\n    ini: ^6.0.0\n    pnpm-workspace-yaml: ^1.6.0\n    which: ^6.0.1\nonlyBuiltDependencies:\n  - esbuild\n  - simple-git-hooks\n  - unrs-resolver\n"
  },
  {
    "path": "src/catalog/detect.ts",
    "content": "import type { Agent } from 'package-manager-detector'\nimport type { CatalogProvider } from './types'\nimport { pnpmCatalogProvider } from './pnpm'\n\nexport function getCatalogProvider(agent: Agent): CatalogProvider | null {\n  if (agent === 'pnpm')\n    return pnpmCatalogProvider\n  return null\n}\n"
  },
  {
    "path": "src/catalog/handler.ts",
    "content": "import type { Agent } from 'package-manager-detector'\nimport type { ExtendedResolvedCommand, RunnerContext } from '../runner'\nimport type { DepType } from './package-json'\nimport type { CatalogConfig, CatalogProvider } from './types'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport { getLatestVersion } from 'fast-npm-meta'\nimport { getCatalog } from '../config'\nimport { getCommand } from '../parse'\nimport { getCatalogProvider } from './detect'\nimport { findClosestPackageJson, updatePackageJsonCatalogRefs } from './package-json'\nimport { promptSelectCatalog } from './prompt'\nimport { getCatalogRef } from './types'\n\nfunction splitPackagesAndFlags(args: string[]): { packages: string[], flags: string[] } {\n  const packages: string[] = []\n  const flags: string[] = []\n  for (const arg of args) {\n    if (arg.startsWith('-'))\n      flags.push(arg)\n    else\n      packages.push(arg)\n  }\n  return { packages, flags }\n}\n\nfunction getDepType(flags: string[]): DepType {\n  if (flags.includes('-D') || flags.includes('-d'))\n    return 'devDependencies'\n  if (flags.includes('--save-peer'))\n    return 'peerDependencies'\n  return 'dependencies'\n}\n\nasync function resolveVersion(pkgName: string): Promise<string> {\n  const meta = await getLatestVersion(pkgName)\n  return `^${meta.version}`\n}\n\nasync function resolveCatalogForPackage(\n  provider: CatalogProvider,\n  config: CatalogConfig,\n  pkgName: string,\n  programmatic?: boolean,\n): Promise<{ catalogName: string | undefined, version?: string }> {\n  // Check if already in a catalog\n  const existing = provider.findPackage(config, pkgName)\n  if (existing) {\n    return { catalogName: existing.name }\n  }\n\n  // Prompt user to select catalog\n  const { catalogName } = await promptSelectCatalog(config, pkgName, programmatic)\n  if (!catalogName)\n    return { catalogName: undefined }\n\n  // Fetch latest version for new catalog entry\n  const version = await resolveVersion(pkgName)\n  return { catalogName, version }\n}\n\nexport async function handleCatalogInstall(\n  agent: Agent,\n  args: string[],\n  ctx?: RunnerContext,\n): Promise<ExtendedResolvedCommand | undefined> {\n  const catalogEnabled = await getCatalog()\n  if (!catalogEnabled)\n    return undefined\n\n  const provider = getCatalogProvider(agent)\n  if (!provider)\n    return undefined\n\n  // Check for workspace flag\n  const hasWorkspaceFlag = args.includes('-w') || args.includes('--workspace')\n  const cleanArgs = args.filter(a => a !== '-w' && a !== '--workspace')\n\n  const { packages, flags } = splitPackagesAndFlags(cleanArgs)\n\n  // No packages to add (bare install, frozen, etc.)\n  if (packages.length === 0)\n    return undefined\n\n  const cwd = ctx?.cwd ?? process.cwd()\n  const config = await provider.detect(cwd)\n  if (!config)\n    return undefined\n\n  const depType = getDepType(flags)\n  const catalogEntries: { name: string, catalogRef: string }[] = []\n  const skippedPackages: string[] = []\n\n  for (const pkg of packages) {\n    const result = await resolveCatalogForPackage(provider, config, pkg, ctx?.programmatic)\n\n    if (result.catalogName) {\n      // Add to catalog file if it's a new entry\n      if (result.version) {\n        await provider.addPackage(config, result.catalogName, pkg, result.version)\n        if (!ctx?.programmatic) {\n          // eslint-disable-next-line no-console\n          console.log(`${styleText('green', '+')} ${styleText('cyan', pkg)} ${styleText('dim', `→ ${result.catalogName} catalog (${result.version})`)}`)\n        }\n      }\n      else if (!ctx?.programmatic) {\n        const existingCatalog = provider.findPackage(config, pkg)\n        // eslint-disable-next-line no-console\n        console.log(`${styleText('green', '✓')} ${styleText('cyan', pkg)} ${styleText('dim', `→ found in ${existingCatalog!.name} catalog`)}`)\n      }\n      catalogEntries.push({ name: pkg, catalogRef: getCatalogRef(result.catalogName) })\n    }\n    else {\n      skippedPackages.push(pkg)\n    }\n  }\n\n  if (catalogEntries.length === 0)\n    return undefined\n\n  // Determine target package.json\n  let pkgJsonPath: string | null\n  if (hasWorkspaceFlag) {\n    pkgJsonPath = path.join(path.dirname(config.filePath), 'package.json')\n  }\n  else {\n    pkgJsonPath = findClosestPackageJson(cwd)\n  }\n\n  if (!pkgJsonPath) {\n    if (!ctx?.programmatic) {\n      console.error(styleText('red', '✗ No package.json found'))\n      process.exit(1)\n    }\n    throw new Error('No package.json found')\n  }\n\n  // Update package.json with catalog refs\n  updatePackageJsonCatalogRefs(pkgJsonPath, catalogEntries, depType)\n\n  // If some packages were skipped, add them normally alongside install\n  if (skippedPackages.length > 0) {\n    return getCommand(agent, 'add', [...skippedPackages, ...flags])\n  }\n\n  // All packages handled via catalogs, just run install\n  return getCommand(agent, 'install', [])\n}\n"
  },
  {
    "path": "src/catalog/package-json.ts",
    "content": "import fs from 'node:fs'\nimport path from 'node:path'\n\nexport function findClosestPackageJson(cwd: string): string | null {\n  let dir = path.resolve(cwd)\n  while (true) {\n    const filePath = path.join(dir, 'package.json')\n    if (fs.existsSync(filePath))\n      return filePath\n    const parent = path.dirname(dir)\n    if (parent === dir)\n      return null\n    dir = parent\n  }\n}\n\nfunction detectIndent(content: string): string {\n  const match = content.match(/^(\\s+)\"/m)\n  return match?.[1] ?? '  '\n}\n\nexport type DepType = 'dependencies' | 'devDependencies' | 'peerDependencies'\n\nexport function updatePackageJsonCatalogRefs(\n  pkgJsonPath: string,\n  entries: { name: string, catalogRef: string }[],\n  depType: DepType,\n): void {\n  const content = fs.readFileSync(pkgJsonPath, 'utf-8')\n  const indent = detectIndent(content)\n  const data = JSON.parse(content)\n\n  if (!data[depType])\n    data[depType] = {}\n\n  for (const entry of entries) {\n    data[depType][entry.name] = entry.catalogRef\n  }\n\n  fs.writeFileSync(pkgJsonPath, `${JSON.stringify(data, null, indent)}\\n`)\n}\n"
  },
  {
    "path": "src/catalog/pnpm.ts",
    "content": "import type { CatalogConfig, CatalogInfo, CatalogProvider } from './types'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml'\n\nfunction findPnpmWorkspaceYaml(cwd: string): string | null {\n  let dir = path.resolve(cwd)\n  while (true) {\n    const filePath = path.join(dir, 'pnpm-workspace.yaml')\n    if (fs.existsSync(filePath))\n      return filePath\n    const parent = path.dirname(dir)\n    if (parent === dir)\n      return null\n    dir = parent\n  }\n}\n\nexport const pnpmCatalogProvider: CatalogProvider = {\n  async detect(cwd: string): Promise<CatalogConfig | null> {\n    const filePath = findPnpmWorkspaceYaml(cwd)\n    if (!filePath)\n      return null\n\n    const content = fs.readFileSync(filePath, 'utf-8')\n    const workspace = parsePnpmWorkspaceYaml(content)\n    const json = workspace.toJSON()\n\n    const catalogs: CatalogInfo[] = []\n    const hasDefaultCatalog = json.catalog != null && Object.keys(json.catalog).length > 0\n    const hasNamedCatalogs = json.catalogs != null && Object.keys(json.catalogs).length > 0\n\n    if (!hasDefaultCatalog && !hasNamedCatalogs)\n      return null\n\n    if (hasDefaultCatalog) {\n      catalogs.push({\n        name: 'default',\n        packages: json.catalog!,\n      })\n    }\n\n    if (hasNamedCatalogs) {\n      for (const [name, packages] of Object.entries(json.catalogs!)) {\n        catalogs.push({ name, packages })\n      }\n    }\n\n    return {\n      filePath,\n      catalogs,\n      hasDefaultCatalog,\n      hasNamedCatalogs,\n    }\n  },\n\n  findPackage(config: CatalogConfig, pkgName: string): CatalogInfo | undefined {\n    return config.catalogs.find(c => pkgName in c.packages)\n  },\n\n  async addPackage(config: CatalogConfig, catalogName: string, pkgName: string, version: string): Promise<void> {\n    const content = fs.readFileSync(config.filePath, 'utf-8')\n    const workspace = parsePnpmWorkspaceYaml(content)\n    workspace.setPackage(catalogName, pkgName, version)\n    fs.writeFileSync(config.filePath, workspace.toString())\n\n    // Update the in-memory config\n    const existing = config.catalogs.find(c => c.name === catalogName)\n    if (existing) {\n      existing.packages[pkgName] = version\n    }\n    else {\n      config.catalogs.push({ name: catalogName, packages: { [pkgName]: version } })\n    }\n  },\n}\n"
  },
  {
    "path": "src/catalog/prompt.ts",
    "content": "import type { CatalogConfig } from './types'\nimport { styleText } from 'node:util'\nimport prompts from '@posva/prompts'\n\nconst SKIP = '__skip__'\nconst CREATE_NEW = '__create_new__'\n\nexport interface CatalogSelection {\n  catalogName: string | undefined\n}\n\nexport async function promptSelectCatalog(\n  config: CatalogConfig,\n  pkgName: string,\n  programmatic?: boolean,\n): Promise<CatalogSelection> {\n  // Only default catalog: no prompt needed\n  if (config.hasDefaultCatalog && !config.hasNamedCatalogs) {\n    return { catalogName: 'default' }\n  }\n\n  if (programmatic) {\n    return { catalogName: undefined }\n  }\n\n  const catalogChoices = config.catalogs.map(c => ({\n    title: c.name,\n    value: c.name,\n  }))\n\n  const { catalog } = await prompts({\n    type: 'select',\n    name: 'catalog',\n    message: `select catalog for ${styleText('yellow', pkgName)}`,\n    choices: [\n      ...catalogChoices,\n      { title: styleText('dim', 'create new catalog'), value: CREATE_NEW },\n      { title: styleText('dim', 'skip (install without catalog)'), value: SKIP },\n    ],\n  })\n\n  if (catalog === undefined || catalog === SKIP) {\n    return { catalogName: undefined }\n  }\n\n  if (catalog === CREATE_NEW) {\n    const newName = await promptNewCatalogName()\n    return { catalogName: newName }\n  }\n\n  return { catalogName: catalog }\n}\n\nasync function promptNewCatalogName(): Promise<string | undefined> {\n  const { name } = await prompts({\n    type: 'text',\n    name: 'name',\n    message: 'new catalog name',\n  })\n  return name || undefined\n}\n"
  },
  {
    "path": "src/catalog/types.ts",
    "content": "export interface CatalogInfo {\n  name: string\n  packages: Record<string, string>\n}\n\nexport interface CatalogConfig {\n  filePath: string\n  catalogs: CatalogInfo[]\n  hasDefaultCatalog: boolean\n  hasNamedCatalogs: boolean\n}\n\nexport interface CatalogProvider {\n  detect: (cwd: string) => Promise<CatalogConfig | null>\n  findPackage: (config: CatalogConfig, pkgName: string) => CatalogInfo | undefined\n  addPackage: (config: CatalogConfig, catalogName: string, pkgName: string, version: string) => Promise<void>\n}\n\nexport function getCatalogRef(catalogName: string): string {\n  return catalogName === 'default' ? 'catalog:' : `catalog:${catalogName}`\n}\n"
  },
  {
    "path": "src/commands/index.ts",
    "content": "export * from '../index'\n"
  },
  {
    "path": "src/commands/na.ts",
    "content": "import { parseNa } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNa)\n"
  },
  {
    "path": "src/commands/nci.ts",
    "content": "import { parseNi } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(\n  (agent, args, hasLock) => parseNi(agent, [...args, '--frozen-if-present'], hasLock),\n  { autoInstall: true },\n)\n"
  },
  {
    "path": "src/commands/nd.ts",
    "content": "import { parseNd } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNd)\n"
  },
  {
    "path": "src/commands/ni.ts",
    "content": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport prompts from '@posva/prompts'\nimport { Fzf } from 'fzf'\nimport { handleCatalogInstall } from '../catalog/handler'\nimport { fetchNpmPackages } from '../fetch'\nimport { parseNi } from '../parse'\nimport { runCli } from '../runner'\nimport { exclude } from '../utils'\n\nrunCli(async (agent, args, ctx) => {\n  const isInteractive = args[0] === '-i'\n\n  if (isInteractive) {\n    let fetchPattern: string\n\n    if (args[1] && !args[1].startsWith('-')) {\n      fetchPattern = args[1]\n    }\n    else {\n      const { pattern } = await prompts({\n        type: 'text',\n        name: 'pattern',\n        message: 'search for package',\n      })\n\n      fetchPattern = pattern\n    }\n\n    if (!fetchPattern) {\n      process.exitCode = 1\n      return\n    }\n\n    const packages = await fetchNpmPackages(fetchPattern)\n\n    if (!packages.length) {\n      console.error('No results found')\n      process.exitCode = 1\n      return\n    }\n\n    const fzf = new Fzf(packages, {\n      selector: (item: Choice) => item.title,\n      casing: 'case-insensitive',\n    })\n\n    const { dependency } = await prompts({\n      type: 'autocomplete',\n      name: 'dependency',\n      choices: packages,\n      instructions: false,\n      message: 'choose a package to install',\n      limit: 15,\n      async suggest(input: string, choices: Choice[]) {\n        const results = fzf.find(input)\n        return results.map(r => choices.find((c: any) => c.value === r.item.value))\n      },\n    })\n\n    if (!dependency) {\n      process.exitCode = 1\n      return\n    }\n\n    args = exclude(args, '-d', '-p', '-i')\n\n    /**\n     * yarn and bun do not support\n     * the installation of peers programmatically\n     */\n    const canInstallPeers = ['npm', 'pnpm'].includes(agent)\n\n    const { mode } = await prompts({\n      type: 'select',\n      name: 'mode',\n      message: `install ${styleText('yellow', dependency.name)} as`,\n      choices: [\n        {\n          title: 'prod',\n          value: '',\n          selected: true,\n        },\n        {\n          title: 'dev',\n          value: '-D',\n        },\n        {\n          title: `peer`,\n          value: '--save-peer',\n          disabled: !canInstallPeers,\n        },\n      ],\n    })\n\n    if (mode === undefined) {\n      process.exitCode = 1\n      return\n    }\n\n    args.push(dependency.name, mode)\n  }\n\n  // Catalog mode: intercept before normal add\n  if (!args.includes('-g')) {\n    const catalogCmd = await handleCatalogInstall(agent, args, ctx)\n    if (catalogCmd !== undefined)\n      return catalogCmd\n  }\n\n  return parseNi(agent, args, ctx)\n})\n"
  },
  {
    "path": "src/commands/nlx.ts",
    "content": "import { parseNlx } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNlx)\n"
  },
  {
    "path": "src/commands/nr.ts",
    "content": "import type { Choice } from '@posva/prompts'\nimport type { PackageScript } from '../package'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimport { byLengthAsc, Fzf } from 'fzf'\nimport { getCompletionSuggestions, rawBashCompletionScript, rawFishCompletionScript, rawZshCompletionScript } from '../completion'\nimport { readPackageScripts, readWorkspaceScripts } from '../package'\nimport { parseNr } from '../parse'\nimport { runCli } from '../runner'\nimport { dump, load } from '../storage'\nimport { limitText } from '../utils'\n\nrunCli(async (agent, args, ctx) => {\n  const storage = await load()\n\n  const promptSelectScript = async (raw: PackageScript[]) => {\n    const terminalColumns = process.stdout?.columns || 80\n\n    const last = storage.lastRunCommand\n    const choices = raw.reduce<Choice[]>((acc, { key, description }) => {\n      const item = {\n        title: key,\n        value: key,\n        description: limitText(description, terminalColumns - 15),\n      }\n      if (last && key === last) {\n        return [item, ...acc]\n      }\n      return [...acc, item]\n    }, [])\n\n    const fzf = new Fzf(raw, {\n      selector: item => `${item.key} ${item.description}`,\n      casing: 'case-insensitive',\n      tiebreakers: [byLengthAsc],\n    })\n\n    // Workaround for @posva/prompts autocomplete bug where ESC key submits instead of canceling\n    // https://github.com/terkelg/prompts/issues/362\n    let isExited = false\n\n    try {\n      const { fn } = await prompts({\n        name: 'fn',\n        message: 'script to run',\n        type: 'autocomplete',\n        choices,\n        async suggest(input: string, choices: Choice[]) {\n          if (!input)\n            return choices\n          const results = fzf.find(input)\n          return results.map(r => choices.find(c => c.value === r.item.key))\n        },\n        onState(state) {\n          if (state.exited)\n            isExited = true\n        },\n      })\n      if (!fn || isExited)\n        process.exit(1)\n      args.push(fn)\n    }\n    catch {\n      process.exit(1)\n    }\n  }\n\n  // Use --completion to generate completion script and do completion logic\n  // (No package manager would have an argument named --completion)\n  if (args[0] === '--completion') {\n    const compLine = process.env.COMP_LINE\n    const rawCompCword = process.env.COMP_CWORD\n    // In bash\n    if (compLine !== undefined && rawCompCword !== undefined) {\n      const compCword = Number.parseInt(rawCompCword, 10)\n      const compWords = args.slice(1)\n      // Only complete the second word (nr __here__ ...)\n      if (compCword === 1) {\n        const suggestions = getCompletionSuggestions(compWords, ctx)\n\n        // eslint-disable-next-line no-console\n        console.log(suggestions.join('\\n'))\n      }\n    }\n    // In other shells, return suggestions directly\n    else {\n      const suggestions = getCompletionSuggestions(args, ctx)\n\n      // eslint-disable-next-line no-console\n      console.log(suggestions.join('\\n'))\n    }\n    return\n  }\n\n  // -p is a flag attempt to read scripts from monorepo\n  if (args[0] === '-p') {\n    const raw = await readWorkspaceScripts(ctx, args)\n    // Show prompt if there are multiple scripts\n    if (raw.length > 1) {\n      await promptSelectScript(raw)\n    }\n  }\n\n  if (args[0] === '-') {\n    if (!storage.lastRunCommand) {\n      if (!ctx?.programmatic) {\n        console.error('No last command found')\n        process.exit(1)\n      }\n\n      throw new Error('No last command found')\n    }\n    args[0] = storage.lastRunCommand\n  }\n\n  if (args.length === 0 && !ctx?.programmatic) {\n    const raw = readPackageScripts(ctx)\n    await promptSelectScript(raw)\n  }\n\n  if (storage.lastRunCommand !== args[0]) {\n    storage.lastRunCommand = args[0]\n    dump()\n  }\n\n  return parseNr(agent, args, ctx)\n}, {\n  onBeforeCommand: (args, ctx) => {\n    // Print ZSH completion script.\n    if (args[0] === '--completion-zsh') {\n    // eslint-disable-next-line no-console\n      console.log(rawZshCompletionScript)\n      return ctx.exit()\n    }\n\n    // Print Bash completion script\n    if (args[0] === '--completion-bash') {\n    // eslint-disable-next-line no-console\n      console.log(rawBashCompletionScript)\n      return ctx.exit()\n    }\n\n    // Print Fish completion script\n    if (args[0] === '--completion-fish') {\n    // eslint-disable-next-line no-console\n      console.log(rawFishCompletionScript)\n      return ctx.exit()\n    }\n  },\n})\n"
  },
  {
    "path": "src/commands/nun.ts",
    "content": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimport { Fzf } from 'fzf'\nimport { getPackageJSON } from '../fs'\nimport { parseNun } from '../parse'\nimport { runCli } from '../runner'\nimport { exclude } from '../utils'\n\nrunCli(async (agent, args, ctx) => {\n  const isMultiple = args[0] === '-m' // Compatible with issue/311\n  const isGlobal = args.includes('-g')\n\n  const isInteractive = !args.length && !ctx?.programmatic\n\n  if ((isInteractive || isMultiple) && !isGlobal) {\n    const pkg = getPackageJSON(ctx)\n\n    const allDependencies = { ...pkg.dependencies, ...pkg.devDependencies }\n\n    const raw = Object.entries(allDependencies) as [string, string][]\n\n    if (!raw.length) {\n      console.error('No dependencies found')\n      return\n    }\n\n    const fzf = new Fzf(raw, {\n      selector: ([dep, version]) => `${dep} ${version}`,\n      casing: 'case-insensitive',\n    })\n\n    const choices: Choice[] = raw.map(([dependency, version]) => ({\n      title: dependency,\n      value: dependency,\n      description: version,\n    }))\n\n    if (isMultiple)\n      args = exclude(args, '-m')\n\n    try {\n      const { depsToRemove } = await prompts({\n        type: 'autocompleteMultiselect',\n        name: 'depsToRemove',\n        choices,\n        min: 1,\n        instructions: false,\n        message: 'remove dependencies',\n        async suggest(input: string, choices: Choice[]) {\n          const results = fzf.find(input)\n          return results.map(r => choices.find(c => c.value === r.item[0]))\n        },\n      })\n\n      if (Array.isArray(depsToRemove))\n        args.push(...depsToRemove)\n    }\n    catch {\n      process.exit(1)\n    }\n  }\n\n  return parseNun(agent, args, ctx)\n})\n"
  },
  {
    "path": "src/commands/nup.ts",
    "content": "import { parseNup } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNup)\n"
  },
  {
    "path": "src/completion.ts",
    "content": "import type { RunnerContext } from '.'\nimport { byLengthAsc, Fzf } from 'fzf'\nimport { readPackageScripts } from './package'\n\n// Print completion script\nexport const rawBashCompletionScript = `\n###-begin-nr-completion-###\n\nif type complete &>/dev/null; then\n  _nr_completion() {\n    local words\n    local cur\n    local cword\n    _get_comp_words_by_ref -n =: cur words cword\n    IFS=$'\\\\n'\n    COMPREPLY=($(COMP_CWORD=$cword COMP_LINE=$cur nr --completion \\${words[@]}))\n  }\n  complete -F _nr_completion nr\nfi\n\n###-end-nr-completion-###\n`.trim()\n\nexport const rawZshCompletionScript = `\n#compdef nr\n\n_nr_completion() {\n  local -a completions\n  completions=(\"\\${(f)$(nr --completion $words[2,-1])}\")\n  \n  compadd -a completions\n}\n\n_nr_completion\n`.trim()\n\nexport const rawFishCompletionScript = `\nfunction _nr_completion\n  set -l tokens (commandline -xpc)\n  if test (count $tokens) -ge 1\n    set tokens $tokens[2..-1]\n  end\n  nr --completion $tokens 2>/dev/null\nend\n\ncomplete -c nr -f -a '(_nr_completion)' -d 'package.json scripts'\n`.trim()\n\nexport function getCompletionSuggestions(args: string[], ctx: RunnerContext | undefined) {\n  const raw = readPackageScripts(ctx)\n  const fzf = new Fzf(raw, {\n    selector: item => item.key,\n    casing: 'case-insensitive',\n    tiebreakers: [byLengthAsc],\n  })\n\n  const results = fzf.find(args[1] || '')\n\n  return results.map(r => r.item.key)\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "import type { Agent } from 'package-manager-detector'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport ini from 'ini'\nimport { detect } from './detect'\n\nconst customRcPath = process.env.NI_CONFIG_FILE\n\nconst home = process.platform === 'win32'\n  ? process.env.USERPROFILE\n  : process.env.HOME\n\nconst defaultRcPath = path.join(home || '~/', '.nirc')\n\nconst rcPath = customRcPath || defaultRcPath\n\ninterface Config {\n  defaultAgent: Agent | 'prompt'\n  globalAgent: Agent\n  runAgent: 'node' | undefined\n  useSfw: boolean\n  catalog: boolean\n}\n\nconst defaultConfig: Config = {\n  defaultAgent: 'prompt',\n  globalAgent: 'npm',\n  runAgent: undefined,\n  useSfw: false,\n  catalog: true,\n}\n\nlet config: Config | undefined\n\nexport async function getConfig(): Promise<Config> {\n  if (!config) {\n    config = { ...defaultConfig, ...fs.existsSync(rcPath)\n      ? ini.parse(fs.readFileSync(rcPath, 'utf-8'))\n      : null }\n\n    if (process.env.NI_DEFAULT_AGENT)\n      config.defaultAgent = process.env.NI_DEFAULT_AGENT as Agent\n\n    if (process.env.NI_GLOBAL_AGENT)\n      config.globalAgent = process.env.NI_GLOBAL_AGENT as Agent\n\n    if (process.env.NI_RUN_AGENT === 'node')\n      config.runAgent = process.env.NI_RUN_AGENT\n\n    if (process.env.NI_USE_SFW !== undefined)\n      config.useSfw = process.env.NI_USE_SFW === 'true'\n\n    if (process.env.NI_CATALOG !== undefined)\n      config.catalog = process.env.NI_CATALOG !== 'false'\n\n    const agent = await detect({ programmatic: true })\n    if (agent)\n      config.defaultAgent = agent\n  }\n\n  return config\n}\n\nexport async function getDefaultAgent(programmatic?: boolean) {\n  const { defaultAgent } = await getConfig()\n  if (defaultAgent === 'prompt' && (programmatic || process.env.CI))\n    return 'npm'\n  return defaultAgent\n}\n\nexport async function getGlobalAgent() {\n  const { globalAgent } = await getConfig()\n  return globalAgent\n}\n\nexport async function getRunAgent() {\n  const { runAgent } = await getConfig()\n  return runAgent\n}\n\nexport async function getUseSfw() {\n  const { useSfw } = await getConfig()\n  return useSfw\n}\n\nexport async function getCatalog() {\n  const { catalog } = await getConfig()\n  return catalog\n}\n"
  },
  {
    "path": "src/detect.ts",
    "content": "import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimport { detect as detectPM } from 'package-manager-detector'\nimport { INSTALL_PAGE } from 'package-manager-detector/constants'\nimport { x } from 'tinyexec'\nimport { cmdExists, terminalLink } from './utils'\n\nexport interface DetectOptions {\n  autoInstall?: boolean\n  programmatic?: boolean\n  cwd?: string\n  /**\n   * Should use Volta when present\n   *\n   * @see https://volta.sh/\n   * @default true\n   */\n  detectVolta?: boolean\n}\n\nexport async function detect({ autoInstall, programmatic, cwd }: DetectOptions = {}) {\n  const targetDir = cwd ?? process.cwd()\n\n  // Check for deno.json or deno.jsonc before using package-manager-detector\n  if (existsSync(path.join(targetDir, 'deno.json')) || existsSync(path.join(targetDir, 'deno.jsonc'))) {\n    // Return early with deno agent if deno.json/deno.jsonc is present\n    return 'deno'\n  }\n\n  const {\n    name,\n    agent,\n    version,\n  } = await detectPM({\n    cwd,\n    onUnknown: (packageManager) => {\n      if (!programmatic) {\n        console.warn('[ni] Unknown packageManager:', packageManager)\n      }\n      return undefined\n    },\n  }) || {}\n\n  // auto install\n  if (name && !cmdExists(name) && !programmatic) {\n    if (!autoInstall) {\n      console.warn(`[ni] Detected ${name} but it doesn't seem to be installed.\\n`)\n\n      if (process.env.CI)\n        process.exit(1)\n\n      const link = terminalLink(name, INSTALL_PAGE[name])\n      const { tryInstall } = await prompts({\n        name: 'tryInstall',\n        type: 'confirm',\n        message: `Would you like to globally install ${link}?`,\n      })\n      if (!tryInstall)\n        process.exit(1)\n    }\n\n    await x(\n      'npm',\n      ['i', '-g', `${name}${version ? `@${version}` : ''}`],\n      {\n        nodeOptions: {\n          stdio: 'inherit',\n          cwd,\n        },\n        throwOnError: true,\n      },\n    )\n  }\n\n  return agent\n}\n"
  },
  {
    "path": "src/environment.ts",
    "content": "import process from 'node:process'\n\nexport interface EnvironmentOptions {\n  autoInstall: boolean\n}\n\nconst DEFAULT_ENVIRONMENT_OPTIONS: EnvironmentOptions = {\n  autoInstall: false,\n}\n\nexport function getEnvironmentOptions(): EnvironmentOptions {\n  return {\n    ...DEFAULT_ENVIRONMENT_OPTIONS,\n    autoInstall: process.env.NI_AUTO_INSTALL === 'true',\n  }\n}\n"
  },
  {
    "path": "src/fetch.ts",
    "content": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport { formatPackageWithUrl } from './utils'\n\nexport interface NpmPackage {\n  name: string\n  description: string\n  version: string\n  keywords: string[]\n  date: string\n  links: {\n    npm: string\n    homepage: string\n    repository: string\n  }\n}\n\ninterface NpmRegistryResponse {\n  objects: { package: NpmPackage }[]\n}\n\nexport async function fetchNpmPackages(pattern: string): Promise<Choice[]> {\n  const registryLink = (pattern: string) =>\n    `https://registry.npmjs.com/-/v1/search?text=${pattern}&size=35`\n\n  const terminalColumns = process.stdout?.columns || 80\n\n  try {\n    const result = await fetch(registryLink(pattern))\n      .then(res => res.json()) as NpmRegistryResponse\n\n    return result.objects.map(({ package: pkg }) => ({\n      title: formatPackageWithUrl(\n        `${pkg.name.padEnd(30, ' ')} ${styleText('blue', `v${pkg.version}`)}`,\n        pkg.links.repository ?? pkg.links.npm,\n        terminalColumns,\n      ),\n      value: pkg,\n    }))\n  }\n  catch {\n    console.error('Error when fetching npm registry')\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "src/fs.ts",
    "content": "import type { RunnerContext } from './runner'\nimport fs from 'node:fs'\nimport { resolve } from 'node:path'\nimport process from 'node:process'\n\nexport function getPackageJSON(ctx?: RunnerContext): any {\n  const cwd = ctx?.cwd ?? process.cwd()\n  const path = resolve(cwd, 'package.json')\n\n  if (fs.existsSync(path)) {\n    try {\n      const raw = fs.readFileSync(path, 'utf-8')\n      const data = JSON.parse(raw)\n      return data\n    }\n    catch (e) {\n      if (!ctx?.programmatic) {\n        console.warn('Failed to parse package.json')\n        process.exit(1)\n      }\n\n      throw e\n    }\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * from './config'\nexport * from './detect'\n\nexport * from './parse'\nexport * from './runner'\nexport * from './utils'\nexport * from 'package-manager-detector/commands'\nexport * from 'package-manager-detector/constants'\n"
  },
  {
    "path": "src/monorepo.ts",
    "content": "import type { Choice } from '@posva/prompts'\nimport type { RunnerContext } from './runner'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimport { byLengthAsc, Fzf } from 'fzf'\nimport { globSync } from 'tinyglobby'\nimport { getPackageJSON } from './fs'\n\nexport const IGNORE_PATHS = [\n  '**/node_modules/**',\n  '**/dist/**',\n  '**/public/**',\n  '**/fixture/**',\n  '**/fixtures/**',\n]\n\nexport function findPackages(ctx?: RunnerContext) {\n  const { cwd = process.cwd() } = ctx ?? {}\n  const packagePath = resolve(cwd, 'package.json')\n  if (!existsSync(packagePath))\n    return []\n\n  const pkgs = globSync('**/package.json', {\n    ignore: IGNORE_PATHS,\n    cwd,\n    onlyFiles: true,\n    dot: false,\n    expandDirectories: false,\n  })\n\n  if (pkgs.length <= 1)\n    return [packagePath]\n  return pkgs\n}\n\nexport async function promptSelectPackage(ctx?: RunnerContext, command?: string): Promise<RunnerContext | undefined> {\n  const cwd = ctx?.cwd ?? process.cwd()\n  const packagePaths = findPackages(ctx)\n  if (packagePaths.length <= 1) {\n    return ctx\n  }\n\n  const blank = ' '.repeat(process.stdout?.columns || 80)\n  // Prompt the user to select a package\n  let choices: (Choice & { scripts: Record<string, string> })[] = packagePaths.map((item) => {\n    const filePath = resolve(cwd, item)\n    const dir = dirname(filePath)\n    const pkg = getPackageJSON({ ...ctx, cwd: dir, programmatic: true })\n\n    return {\n      title: pkg.name ?? item,\n      value: dir,\n      description: `${pkg.description ?? filePath}${blank}`,\n      scripts: pkg.scripts,\n    }\n  })\n\n  // Filter packages that have the command\n  if (command) {\n    choices = choices.filter(c => c.scripts?.[command])\n  }\n  if (!choices.length) {\n    return ctx\n  }\n  if (choices.length === 1) {\n    return { ...ctx, cwd: choices[0].value }\n  }\n\n  const fzf = new Fzf(choices, {\n    selector: item => `${item.title} ${item.description}`,\n    casing: 'case-insensitive',\n    tiebreakers: [byLengthAsc],\n  })\n\n  let res: string\n  try {\n    const { pkg } = await prompts({\n      name: 'pkg',\n      message: 'select a package',\n      type: 'autocomplete',\n      choices,\n      async suggest(input: string, choices: Choice[]) {\n        if (!input)\n          return choices\n        const results = fzf.find(input)\n        return results.map(r => choices.find(c => c.value === r.item.value))\n      },\n    })\n    if (!pkg)\n      throw new Error('No package selected')\n    res = pkg\n  }\n  catch (error) {\n    if (!ctx?.programmatic)\n      process.exit(1)\n    throw error\n  }\n\n  return { ...ctx, cwd: res }\n}\n"
  },
  {
    "path": "src/package.ts",
    "content": "import type { RunnerContext } from '.'\nimport { getPackageJSON } from './fs'\nimport { promptSelectPackage } from './monorepo'\n\nexport interface PackageScript {\n  key: string\n  cmd: string\n  description: string\n}\n\nexport async function readWorkspaceScripts(ctx: RunnerContext | undefined, args: string[]): Promise<PackageScript[]> {\n  const index = args.findIndex(i => i === '-p')\n  let command: string = ''\n  if (index !== -1) {\n    command = args[index + 1]\n  }\n\n  const context = await promptSelectPackage(ctx, command)\n  // Change cwd to the selected package\n  if (ctx && context?.cwd) {\n    ctx.cwd = context.cwd\n  }\n  const scripts = readPackageScripts(context)\n  const cmdIndex = scripts.findIndex(i => i.key === command)\n  if (command && cmdIndex !== -1) {\n    return [scripts[cmdIndex]]\n  }\n  return scripts\n}\n\nexport function readPackageScripts(ctx: RunnerContext | undefined): PackageScript[] {\n  // support https://www.npmjs.com/package/npm-scripts-info conventions\n  const pkg = getPackageJSON(ctx)\n  const rawScripts = pkg.scripts || {}\n  const scriptsInfo = pkg['scripts-info'] || {}\n\n  const scripts = Object.entries(rawScripts)\n    .filter(i => !i[0].startsWith('?'))\n    .map(([key, cmd]) => ({\n      key,\n      cmd,\n      description: scriptsInfo[key] || rawScripts[`?${key}`] || cmd,\n    }))\n\n  if (scripts.length === 0 && !ctx?.programmatic) {\n    console.warn('No scripts found in package.json')\n  }\n\n  return scripts as PackageScript[]\n}\n"
  },
  {
    "path": "src/parse.ts",
    "content": "import type { Agent, Command, ResolvedCommand } from 'package-manager-detector'\nimport type { ExtendedResolvedCommand, Runner } from './runner'\nimport process from 'node:process'\nimport { COMMANDS, constructCommand, getRunAgent } from '.'\nimport { exclude } from './utils'\n\nexport class UnsupportedCommand extends Error {\n  constructor({ agent, command }: { agent: Agent, command: Command }) {\n    super(`Command \"${command}\" is not support by agent \"${agent}\"`)\n  }\n}\n\nexport function getCommand(\n  agent: Agent,\n  command: Command,\n  args: string[] = [],\n): ExtendedResolvedCommand {\n  if (!COMMANDS[agent])\n    throw new Error(`Unsupported agent \"${agent}\"`)\n  if (!COMMANDS[agent][command])\n    throw new UnsupportedCommand({ agent, command })\n\n  return constructCommand(COMMANDS[agent][command], args)!\n}\n\nexport const parseNi = <Runner>((agent, args, ctx) => {\n  // bun use `-d` instead of `-D`, #90\n  if (agent === 'bun')\n    args = args.map(i => i === '-D' ? '-d' : i)\n\n  // npm use `--omit=dev` instead of `--production`\n  if (agent === 'npm')\n    args = args.map(i => i === '-P' ? '--omit=dev' : i)\n\n  if (args.includes('-P'))\n    args = args.map(i => i === '-P' ? '--production' : i)\n\n  if (args.includes('-g'))\n    return getCommand(agent, 'global', exclude(args, '-g'))\n\n  if (args.includes('--frozen-if-present')) {\n    args = exclude(args, '--frozen-if-present')\n    return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)\n  }\n\n  if (args.includes('--frozen'))\n    return getCommand(agent, 'frozen', exclude(args, '--frozen'))\n\n  if (args.length === 0 || args.every(i => i.startsWith('-')))\n    return getCommand(agent, 'install', args)\n\n  return getCommand(agent, 'add', args)\n})\n\nexport const parseNr = <Runner>(async (agent, args, ctx) => {\n  if (args.length === 0)\n    args.push('start')\n\n  const runAgent = await getRunAgent()\n\n  let runWithNode = false\n  if (runAgent === 'node') {\n    const [majorNodeVersion] = process.versions.node.split('.').map(Number)\n    if (majorNodeVersion < 22) {\n      throw new Error('The runAgent \"node\" requires Node.js 22.0.0 or higher')\n    }\n    runWithNode = true\n    args = ['--run', ...args]\n  }\n\n  let hasIfPresent = false\n  if (args.includes('--if-present')) {\n    args = exclude(args, '--if-present')\n    hasIfPresent = true\n  }\n\n  if (args.includes('-p'))\n    args = exclude(args, '-p')\n\n  const cmd = runWithNode ? { command: 'node', args } : getCommand(agent, 'run', args)\n\n  if (ctx?.cwd)\n    cmd.cwd = ctx.cwd\n\n  if (!cmd)\n    return cmd\n\n  if (hasIfPresent && !runWithNode)\n    cmd.args.splice(1, 0, '--if-present')\n\n  return cmd\n})\n\nexport const parseNup = <Runner>((agent, args) => {\n  if (args.includes('-i'))\n    return getCommand(agent, 'upgrade-interactive', exclude(args, '-i'))\n\n  return getCommand(agent, 'upgrade', args)\n})\n\nexport const parseNd = <Runner>((agent, args) => {\n  // https://yarnpkg.com/cli/dedupe#options\n  // https://pnpm.io/cli/dedupe#--check\n  if (agent === 'pnpm')\n    args = args.map(i => i === '-c' ? '--check' : i)\n\n  // https://docs.npmjs.com/cli/v11/commands/npm-dedupe#dry-run\n  if (agent === 'npm')\n    args = args.map(i => i === '-c' ? '--dry-run' : i)\n\n  return getCommand(agent, 'dedupe', args)\n})\n\nexport const parseNun = <Runner>((agent, args) => {\n  if (args.includes('-g'))\n    return getCommand(agent, 'global_uninstall', exclude(args, '-g'))\n  return getCommand(agent, 'uninstall', args)\n})\n\nexport const parseNlx = <Runner>((agent, args) => {\n  return getCommand(agent, 'execute', args)\n})\n\nexport const parseNa = <Runner>((agent, args) => {\n  return getCommand(agent, 'agent', args)\n})\n\nexport function serializeCommand(command?: ResolvedCommand) {\n  if (!command)\n    return undefined\n  if (command.args.length === 0)\n    return command.command\n  return `${command.command} ${command.args.map(i => i.includes(' ') ? `\"${i}\"` : i).join(' ')}`\n}\n"
  },
  {
    "path": "src/runner.ts",
    "content": "import type { Agent, ResolvedCommand } from 'package-manager-detector'\nimport type { Options as TinyExecOptions } from 'tinyexec'\nimport type { DetectOptions } from './detect'\n/* eslint-disable no-console */\nimport { resolve } from 'node:path'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport prompts from '@posva/prompts'\nimport { AGENTS } from 'package-manager-detector'\nimport { x } from 'tinyexec'\nimport { version } from '../package.json'\nimport { getDefaultAgent, getGlobalAgent, getUseSfw } from './config'\nimport { detect } from './detect'\nimport { getEnvironmentOptions } from './environment'\nimport { getCommand, UnsupportedCommand } from './parse'\nimport { cmdExists, remove } from './utils'\n\nconst DEBUG_SIGN = '?'\nconst PROGRAMMATIC_SIGN = '--programmatic'\n\nexport interface RunnerContext {\n  programmatic?: boolean\n  hasLock?: boolean\n  cwd?: string\n}\n\nexport interface ExtendedResolvedCommand extends ResolvedCommand {\n  cwd?: string\n}\n\ninterface RunOptions {\n  /**\n   * Called before agent detection and command execution.\n   *\n   * Useful for performing concrete, agent-agnostic operations.\n   */\n  onBeforeCommand?: (args: string[], ctx: Pick<RunnerContext, 'cwd' | 'programmatic'> & {\n    /**\n     * Skips subsequent command execution.\n     *\n     * This is useful for operations such as generating shell-completion scripts.\n     */\n    exit: () => void\n  }) => void | Promise<void>\n}\n\nexport type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<ExtendedResolvedCommand | undefined> | ExtendedResolvedCommand | undefined\n\nexport async function runCli(fn: Runner, options: DetectOptions & RunOptions & { args?: string[] } = {}) {\n  options = {\n    ...getEnvironmentOptions(),\n    ...options,\n  }\n  const {\n    args = process.argv.slice(2).filter(Boolean),\n  } = options\n  try {\n    await run(fn, args, options)\n  }\n  catch (error) {\n    if (error instanceof UnsupportedCommand && !options.programmatic)\n      console.log(styleText('red', `\\u2717 ${error.message}`))\n\n    if (!options.programmatic)\n      process.exit(1)\n\n    throw error\n  }\n}\n\nexport async function getCliCommand(\n  fn: Runner,\n  args: string[],\n  options: DetectOptions = {},\n  cwd: string = options.cwd ?? process.cwd(),\n) {\n  const isGlobal = args.includes('-g')\n  if (isGlobal)\n    return await fn(await getGlobalAgent(), args)\n\n  let agent = (await detect({ ...options, cwd })) || (await getDefaultAgent(options.programmatic))\n  if (agent === 'prompt') {\n    agent = (\n      await prompts({\n        name: 'agent',\n        type: 'select',\n        message: 'Choose the agent',\n        choices: AGENTS.filter(i => !i.includes('@')).map(value => ({ title: value, value })),\n      })\n    ).agent\n    if (!agent)\n      return\n  }\n\n  return await fn(agent as Agent, args, {\n    programmatic: options.programmatic,\n    hasLock: Boolean(agent),\n    cwd,\n  })\n}\n\nexport async function run(fn: Runner, args: string[], options: DetectOptions & RunOptions = {}) {\n  const { detectVolta = true } = options\n\n  const debug = args.includes(DEBUG_SIGN)\n  if (debug)\n    remove(args, DEBUG_SIGN)\n\n  const programmaticFromArgs = args.includes(PROGRAMMATIC_SIGN)\n  if (programmaticFromArgs)\n    remove(args, PROGRAMMATIC_SIGN)\n\n  const programmatic = options.programmatic || programmaticFromArgs\n\n  let cwd = options.cwd ?? process.cwd()\n  if (args[0] === '-C') {\n    cwd = resolve(cwd, args[1])\n    args.splice(0, 2)\n  }\n\n  if (args.length === 1 && (args[0]?.toLowerCase() === '-v' || args[0] === '--version')) {\n    const getCmd = (a: Agent) => AGENTS.includes(a)\n      ? getCommand(a, 'agent', ['-v'])\n      : { command: a, args: ['-v'] }\n    const xVersionOptions = {\n      nodeOptions: {\n        cwd,\n      },\n      throwOnError: true,\n    } satisfies Partial<TinyExecOptions>\n    const getV = (a: string) => {\n      const { command, args } = getCmd(a as Agent)\n      return x(command, args, xVersionOptions)\n        .then(e => e.stdout)\n        .then(e => e.startsWith('v') ? e : `v${e}`)\n    }\n    const globalAgentPromise = getGlobalAgent()\n    const globalAgentVersionPromise = globalAgentPromise.then(getV)\n    const agentPromise = detect({ ...options, cwd }).then(a => a || '')\n    const agentVersionPromise = agentPromise.then(a => a && getV(a))\n    const nodeVersionPromise = getV('node')\n\n    console.log(`@antfu/ni  ${styleText('cyan', `v${version}`)}`)\n    console.log(`node       ${styleText('green', await nodeVersionPromise)}`)\n    const [agent, agentVersion] = await Promise.all([agentPromise, agentVersionPromise])\n    if (agent)\n      console.log(`${agent.padEnd(10)} ${styleText('blue', agentVersion)}`)\n    else\n      console.log('agent      no lock file')\n    const [globalAgent, globalAgentVersion] = await Promise.all([globalAgentPromise, globalAgentVersionPromise])\n    console.log(`${(`${globalAgent} -g`).padEnd(10)} ${styleText('blue', globalAgentVersion)}`)\n    return\n  }\n\n  if (args.length === 1 && ['-h', '--help'].includes(args[0])) {\n    const dash = styleText('dim', '-')\n    console.log(`${styleText(['green', 'bold'], '@antfu/ni')} ${styleText('dim', `use the right package manager v${version}`)}\\n`)\n    console.log(`ni    ${dash}  install`)\n    console.log(`nr    ${dash}  run`)\n    console.log(`nlx   ${dash}  execute`)\n    console.log(`nup   ${dash}  upgrade`)\n    console.log(`nun   ${dash}  uninstall`)\n    console.log(`nci   ${dash}  clean install`)\n    console.log(`na    ${dash}  agent alias`)\n    console.log(`nd    ${dash}  dedupe dependencies`)\n    console.log(`ni -v ${dash}  show used agent`)\n    console.log(`ni -i ${dash}  interactive package management`)\n    console.log(styleText('yellow', '\\ncheck https://github.com/antfu/ni for more documentation.'))\n    return\n  }\n\n  if (options.onBeforeCommand) {\n    let shouldExit = false\n    await options.onBeforeCommand(args, {\n      cwd,\n      programmatic,\n      exit: () => { shouldExit = true },\n    })\n    if (shouldExit)\n      return\n  }\n\n  const command = await getCliCommand(fn, args, { ...options, programmatic }, cwd)\n\n  if (!command)\n    return\n\n  const useSfw = await getUseSfw()\n  if (useSfw && cmdExists('sfw')) {\n    command.args = [command.command, ...command.args]\n    command.command = 'sfw'\n  }\n  else if (useSfw) {\n    if (programmatic)\n      throw new Error('sfw is enabled but not installed')\n\n    console.error('[ni] sfw is enabled but not installed.')\n    console.error('[ni] Install it with: npm install -g sfw')\n    process.exit(1)\n  }\n\n  if (detectVolta && cmdExists('volta')) {\n    command.args = ['run', command.command, ...command.args]\n    command.command = 'volta'\n  }\n\n  if (debug) {\n    const commandStr = [command.command, ...command.args].join(' ')\n    console.log(commandStr)\n    return\n  }\n\n  const proc = x(\n    command.command,\n    command.args,\n    {\n      nodeOptions: {\n        stdio: 'inherit',\n        cwd: command.cwd ?? cwd,\n      },\n      throwOnError: true,\n    },\n  )\n\n  process.once('SIGINT', async () => {\n    // Ensure the proc finishes cleanup before exiting\n    await proc\n    process.exit(proc.exitCode)\n  })\n\n  await proc\n}\n"
  },
  {
    "path": "src/storage.ts",
    "content": "import { existsSync, promises as fs } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { CLI_TEMP_DIR, writeFileSafe } from './utils'\n\nexport interface Storage {\n  lastRunCommand?: string\n}\n\nlet storage: Storage | undefined\n\nconst storagePath = resolve(CLI_TEMP_DIR, '_storage.json')\n\nexport async function load(fn?: (storage: Storage) => Promise<boolean> | boolean) {\n  if (!storage) {\n    storage = existsSync(storagePath)\n      ? (JSON.parse(await fs.readFile(storagePath, 'utf-8') || '{}') || {})\n      : {}\n  }\n\n  if (fn) {\n    if (await fn(storage!))\n      await dump()\n  }\n\n  return storage!\n}\n\nexport async function dump() {\n  if (storage)\n    await writeFileSafe(storagePath, JSON.stringify(storage))\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import type { Buffer } from 'node:buffer'\nimport { existsSync, promises as fs } from 'node:fs'\nimport os from 'node:os'\nimport { dirname, join } from 'node:path'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport which from 'which'\n\nexport const CLI_TEMP_DIR = join(os.tmpdir(), 'antfu-ni')\n\nexport function remove<T>(arr: T[], v: T) {\n  const index = arr.indexOf(v)\n  if (index >= 0)\n    arr.splice(index, 1)\n\n  return arr\n}\n\nexport function exclude<T>(arr: T[], ...v: T[]) {\n  return arr.slice().filter(item => !v.includes(item))\n}\n\nexport function cmdExists(cmd: string) {\n  return which.sync(cmd, { nothrow: true }) !== null\n}\n\ninterface TempFile {\n  path: string\n  fd: fs.FileHandle\n  cleanup: () => void\n}\n\nlet counter = 0\n\nasync function openTemp(): Promise<TempFile | undefined> {\n  if (!existsSync(CLI_TEMP_DIR))\n    await fs.mkdir(CLI_TEMP_DIR, { recursive: true })\n\n  const competitivePath = join(CLI_TEMP_DIR, `.${process.pid}.${counter}`)\n  counter += 1\n\n  return fs.open(competitivePath, 'wx')\n    .then(fd => ({\n      fd,\n      path: competitivePath,\n      cleanup() {\n        fd.close().then(() => {\n          if (existsSync(competitivePath))\n            fs.unlink(competitivePath)\n        })\n      },\n    }))\n    .catch((error: any) => {\n      if (error && error.code === 'EEXIST')\n        return openTemp()\n\n      else\n        return undefined\n    })\n}\n\n/**\n * Write file safely avoiding conflicts\n */\nexport async function writeFileSafe(\n  path: string,\n  data: string | Buffer = '',\n): Promise<boolean> {\n  const temp = await openTemp()\n\n  if (temp) {\n    fs.writeFile(temp.path, data)\n      .then(() => {\n        const directory = dirname(path)\n        if (!existsSync(directory))\n          fs.mkdir(directory, { recursive: true })\n\n        return fs.rename(temp.path, path)\n          .then(() => true)\n          .catch(() => false)\n      })\n      .catch(() => false)\n      .finally(temp.cleanup)\n  }\n\n  return false\n}\n\nexport function limitText(text: string, maxWidth: number) {\n  if (text.length <= maxWidth)\n    return text\n  return `${text.slice(0, maxWidth)}${styleText('dim', '…')}`\n}\n\nexport function terminalLink(text: string, url: string, options?: { fallback?: (text: string, url: string) => string }): string {\n  // Use OSC 8 hyperlink escape sequence\n  // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda\n  if (\n    process.env.FORCE_HYPERLINK\n    || (process.stdout.isTTY && !process.env.NO_HYPERLINK)\n  ) {\n    return `\\u001B]8;;${url}\\u0007${text}\\u001B]8;;\\u0007`\n  }\n  if (options?.fallback)\n    return options.fallback(text, url)\n  return `${text} (${url})`\n}\n\nexport function formatPackageWithUrl(pkg: string, url?: string, limits = 80) {\n  return url\n    ? terminalLink(\n        pkg,\n        url,\n        {\n          fallback: (_, url) => (pkg.length + url.length > limits)\n            ? pkg\n            : `${pkg} ${styleText('dim', `- ${url}`)}`,\n        },\n      )\n    : pkg\n}\n"
  },
  {
    "path": "taze.config.ts",
    "content": "import { defineConfig } from 'taze'\n\nexport default defineConfig({\n  ignorePaths: [\n    'test/fixtures',\n  ],\n})\n"
  },
  {
    "path": "test/config/.nirc",
    "content": "defaultAgent=npm\nglobalAgent=pnpm\nuseSfw=true\n\n"
  },
  {
    "path": "test/config/config.test.ts",
    "content": "import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { beforeEach, expect, it, vi } from 'vitest'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nbeforeEach(() => {\n  vi.unstubAllEnvs()\n  vi.resetModules()\n})\n\nvi.mock('../../src/detect', () => ({\n  detect: vi.fn(),\n}))\n\nit('has correct defaults', async () => {\n  const { getConfig } = await import('../../src/config')\n  const config = await getConfig()\n\n  expect(config).toEqual({\n    defaultAgent: 'prompt',\n    globalAgent: 'npm',\n    runAgent: undefined,\n    useSfw: false,\n    catalog: true,\n  })\n})\n\nit('loads .nirc', async () => {\n  vi.stubEnv('NI_CONFIG_FILE', join(__dirname, './.nirc'))\n\n  const { getConfig } = await import('../../src/config')\n  const config = await getConfig()\n\n  expect(config).toEqual({\n    defaultAgent: 'npm',\n    globalAgent: 'pnpm',\n    runAgent: undefined,\n    useSfw: true,\n    catalog: true,\n  })\n})\n\nit('reads environment variable config', async () => {\n  vi.stubEnv('NI_DEFAULT_AGENT', 'npm')\n  vi.stubEnv('NI_GLOBAL_AGENT', 'pnpm')\n  vi.stubEnv('NI_USE_SFW', 'true')\n\n  const { getConfig } = await import('../../src/config')\n  const config = await getConfig()\n\n  expect(config).toEqual({\n    defaultAgent: 'npm',\n    globalAgent: 'pnpm',\n    runAgent: undefined,\n    useSfw: true,\n    catalog: true,\n  })\n})\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm/package.json",
    "content": "{\n  \"name\": \"test-workspace\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm/packages/app/package.json",
    "content": "{\n  \"name\": \"test-app\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm/pnpm-workspace.yaml",
    "content": "packages:\n  - packages/*\n\n# Named catalogs\ncatalogs:\n  prod:\n    react: ^18.3.0\n    express: ^4.21.0\n  dev:\n    typescript: ^5.6.0\n    vitest: ^2.1.0\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm-default-only/package.json",
    "content": "{\n  \"name\": \"test-workspace-default\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm-default-only/pnpm-workspace.yaml",
    "content": "packages:\n  - packages/*\n\ncatalog:\n  react: ^18.3.0\n  express: ^4.21.0\n"
  },
  {
    "path": "test/fixtures/lockfile/bun/bun.lockb",
    "content": ""
  },
  {
    "path": "test/fixtures/lockfile/unknown/future-package-manager.json",
    "content": "{}\n"
  },
  {
    "path": "test/fixtures/packager/bun/package.json",
    "content": "{\n  \"packageManager\": \"bun@0\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/deno/deno.json",
    "content": "{}\n"
  },
  {
    "path": "test/fixtures/packager/npm/package.json",
    "content": "{\n  \"packageManager\": \"npm@7\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm/package.json",
    "content": "{\n  \"packageManager\": \"pnpm@8\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm-version-range/package.json",
    "content": "{\n  \"packageManager\": \"^pnpm@8.0.0\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm@6/package.json",
    "content": "{\n  \"packageManager\": \"pnpm@6\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/unknown/package.json",
    "content": "{\n  \"packageManager\": \"future-package-manager\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/yarn/package.json",
    "content": "{\n  \"packageManager\": \"yarn@1\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/yarn@berry/package.json",
    "content": "{\n  \"packageManager\": \"yarn@3\"\n}\n"
  },
  {
    "path": "test/na/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'bun'))\nit('foo', _('foo', 'bun foo'))\nit('run test', _('run test', 'bun run test'))\n"
  },
  {
    "path": "test/na/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'deno'))\nit('foo', _('foo', 'deno foo'))\nit('run test', _('run test', 'deno run test'))\nit('task dev', _('task dev', 'deno task dev'))\nit('install', _('install', 'deno install'))\nit('add package', _('add package', 'deno add package'))\n"
  },
  {
    "path": "test/na/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'npm'))\nit('foo', _('foo', 'npm foo'))\nit('run test', _('run test', 'npm run test'))\n"
  },
  {
    "path": "test/na/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'pnpm'))\nit('foo', _('foo', 'pnpm foo'))\nit('run test', _('run test', 'pnpm run test'))\n"
  },
  {
    "path": "test/na/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn'))\nit('foo', _('foo', 'yarn foo'))\nit('run test', _('run test', 'yarn run test'))\n"
  },
  {
    "path": "test/na/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn'))\nit('foo', _('foo', 'yarn foo'))\nit('run test', _('run test', 'yarn run test'))\n"
  },
  {
    "path": "test/ng.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { getCommand } from '../src/commands'\n\nit('wrong agent', () => {\n  expect(() => {\n    getCommand('idk' as any, 'install', [])\n  }).toThrow('Unsupported agent \"idk\"')\n})\n"
  },
  {
    "path": "test/ni/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'bun install'))\n\nit('single add', _('axios', 'bun add axios'))\n\nit('add dev', _('vite -D', 'bun add vite -d'))\n\nit('multiple', _('eslint @types/node', 'bun add eslint @types/node'))\n\nit('global', _('eslint -g', 'bun add -g eslint'))\n\nit('frozen', _('--frozen', 'bun install --frozen-lockfile'))\n\nit('production', _('-P', 'bun install --production'))\n\nit('frozen production', _('--frozen -P', 'bun install --frozen-lockfile --production'))\n"
  },
  {
    "path": "test/ni/catalog.spec.ts",
    "content": "import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { describe, expect, it } from 'vitest'\nimport { pnpmCatalogProvider } from '../../src/catalog/pnpm'\nimport { getCatalogRef } from '../../src/catalog/types'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst fixtureDir = join(__dirname, '..', 'fixtures', 'catalog', 'pnpm')\nconst defaultOnlyFixtureDir = join(__dirname, '..', 'fixtures', 'catalog', 'pnpm-default-only')\n\ndescribe('getCatalogRef', () => {\n  it('returns \"catalog:\" for default', () => {\n    expect(getCatalogRef('default')).toBe('catalog:')\n  })\n\n  it('returns \"catalog:name\" for named', () => {\n    expect(getCatalogRef('dev')).toBe('catalog:dev')\n    expect(getCatalogRef('prod')).toBe('catalog:prod')\n  })\n})\n\ndescribe('pnpmCatalogProvider.detect', () => {\n  it('detects named catalogs', async () => {\n    const config = await pnpmCatalogProvider.detect(fixtureDir)\n    expect(config).not.toBeNull()\n    expect(config!.hasDefaultCatalog).toBe(false)\n    expect(config!.hasNamedCatalogs).toBe(true)\n    expect(config!.catalogs).toHaveLength(2)\n    expect(config!.catalogs.map(c => c.name)).toEqual(['prod', 'dev'])\n  })\n\n  it('detects default catalog only', async () => {\n    const config = await pnpmCatalogProvider.detect(defaultOnlyFixtureDir)\n    expect(config).not.toBeNull()\n    expect(config!.hasDefaultCatalog).toBe(true)\n    expect(config!.hasNamedCatalogs).toBe(false)\n    expect(config!.catalogs).toHaveLength(1)\n    expect(config!.catalogs[0].name).toBe('default')\n  })\n\n  it('detects from subdirectory', async () => {\n    const subDir = join(fixtureDir, 'packages', 'app')\n    const config = await pnpmCatalogProvider.detect(subDir)\n    expect(config).not.toBeNull()\n    expect(config!.catalogs).toHaveLength(2)\n  })\n\n  it('returns null when no workspace file', async () => {\n    const config = await pnpmCatalogProvider.detect('/tmp')\n    expect(config).toBeNull()\n  })\n})\n\ndescribe('pnpmCatalogProvider.findPackage', () => {\n  it('finds package in named catalog', async () => {\n    const config = (await pnpmCatalogProvider.detect(fixtureDir))!\n    const result = pnpmCatalogProvider.findPackage(config, 'react')\n    expect(result).not.toBeUndefined()\n    expect(result!.name).toBe('prod')\n  })\n\n  it('finds package in dev catalog', async () => {\n    const config = (await pnpmCatalogProvider.detect(fixtureDir))!\n    const result = pnpmCatalogProvider.findPackage(config, 'typescript')\n    expect(result).not.toBeUndefined()\n    expect(result!.name).toBe('dev')\n  })\n\n  it('returns undefined for unknown package', async () => {\n    const config = (await pnpmCatalogProvider.detect(fixtureDir))!\n    const result = pnpmCatalogProvider.findPackage(config, 'unknown-pkg')\n    expect(result).toBeUndefined()\n  })\n\n  it('finds package in default catalog', async () => {\n    const config = (await pnpmCatalogProvider.detect(defaultOnlyFixtureDir))!\n    const result = pnpmCatalogProvider.findPackage(config, 'react')\n    expect(result).not.toBeUndefined()\n    expect(result!.name).toBe('default')\n  })\n})\n"
  },
  {
    "path": "test/ni/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'deno install'))\n\nit('single add', _('axios', 'deno add axios'))\n\nit('multiple', _('eslint @types/node', 'deno add eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'deno add eslint @types/node -D'))\n\nit('global', _('eslint -g', 'deno install -g eslint'))\n\nit('frozen', _('--frozen', 'deno install --frozen'))\n\nit('production', _('-P', 'deno install --production'))\n\nit('frozen production', _('--frozen -P', 'deno install --frozen --production'))\n"
  },
  {
    "path": "test/ni/interactive.spec.ts",
    "content": "import type { Agent } from 'package-manager-detector'\nimport type { RunnerContext } from '../../src'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { fetchNpmPackages } from '../../src/fetch'\nimport { parseNi } from '../../src/parse'\nimport { exclude } from '../../src/utils'\n\nvi.mock('@posva/prompts')\nvi.mock('../../src/fetch')\n\ndescribe('interactive mode - Ctrl+C cancellation', () => {\n  let originalExitCode: typeof process.exitCode\n\n  beforeEach(() => {\n    originalExitCode = process.exitCode\n    process.exitCode = 0\n    vi.clearAllMocks()\n  })\n\n  afterEach(() => {\n    process.exitCode = originalExitCode\n  })\n\n  async function niRunner(agent: Agent, args: string[], ctx?: RunnerContext) {\n    const isInteractive = args[0] === '-i'\n\n    if (isInteractive) {\n      let fetchPattern: string\n\n      if (args[1] && !args[1].startsWith('-')) {\n        fetchPattern = args[1]\n      }\n      else {\n        const { pattern } = await prompts({\n          type: 'text',\n          name: 'pattern',\n          message: 'search for package',\n        })\n\n        fetchPattern = pattern\n      }\n\n      if (!fetchPattern) {\n        process.exitCode = 1\n        return\n      }\n\n      const packages = await fetchNpmPackages(fetchPattern)\n\n      if (!packages.length) {\n        console.error('No results found')\n        process.exitCode = 1\n        return\n      }\n\n      const { dependency } = await prompts({\n        type: 'autocomplete',\n        name: 'dependency',\n        choices: packages,\n        instructions: false,\n        message: 'choose a package to install',\n        limit: 15,\n      })\n\n      if (!dependency) {\n        process.exitCode = 1\n        return\n      }\n\n      args = exclude(args, '-d', '-p', '-i')\n\n      const canInstallPeers = ['npm', 'pnpm'].includes(agent)\n\n      const { mode } = await prompts({\n        type: 'select',\n        name: 'mode',\n        message: `install ${dependency.name} as`,\n        choices: [\n          {\n            title: 'prod',\n            value: '',\n            selected: true,\n          },\n          {\n            title: 'dev',\n            value: '-D',\n          },\n          {\n            title: `peer`,\n            value: '--save-peer',\n            disabled: !canInstallPeers,\n          },\n        ],\n      })\n\n      if (mode === undefined) {\n        process.exitCode = 1\n        return\n      }\n\n      args.push(dependency.name, mode)\n    }\n\n    return parseNi(agent, args, ctx)\n  }\n\n  it('should exit gracefully when user cancels package selection with Ctrl+C', async () => {\n    vi.mocked(prompts)\n      .mockResolvedValueOnce({ pattern: 'react' }) // First prompt: search pattern\n      .mockResolvedValueOnce({ dependency: undefined }) // Second prompt: cancelled with Ctrl+C\n\n    vi.mocked(fetchNpmPackages).mockResolvedValue([\n      { title: 'react', value: 'react' },\n      { title: 'react-dom', value: 'react-dom' },\n    ])\n\n    const result = await niRunner('npm', ['-i'])\n\n    expect(process.exitCode).toBe(1)\n    expect(result).toBeUndefined()\n  })\n\n  it('should exit gracefully when user cancels installation mode selection with Ctrl+C', async () => {\n    vi.mocked(prompts)\n      .mockResolvedValueOnce({ pattern: 'react' }) // First prompt: search pattern\n      .mockResolvedValueOnce({ dependency: { name: 'react', value: 'react' } }) // Second prompt: select package\n      .mockResolvedValueOnce({ mode: undefined }) // Third prompt: cancelled with Ctrl+C\n\n    vi.mocked(fetchNpmPackages).mockResolvedValue([\n      { title: 'react', value: 'react' },\n    ])\n\n    const result = await niRunner('npm', ['-i'])\n\n    expect(process.exitCode).toBe(1)\n    expect(result).toBeUndefined()\n  })\n\n  it('should exit gracefully when user cancels initial search pattern with Ctrl+C', async () => {\n    vi.mocked(prompts)\n      .mockResolvedValueOnce({ pattern: undefined }) // First prompt: cancelled with Ctrl+C\n\n    const result = await niRunner('npm', ['-i'])\n\n    expect(process.exitCode).toBe(1)\n    expect(result).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "test/ni/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'npm i'))\n\nit('single add', _('axios', 'npm i axios'))\n\nit('multiple', _('eslint @types/node', 'npm i eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'npm i eslint @types/node -D'))\n\nit('global', _('eslint -g', 'npm i -g eslint'))\n\nit('frozen', _('--frozen', 'npm ci'))\n\nit('production', _('-P', 'npm i --omit=dev'))\n\nit('frozen production', _('--frozen -P', 'npm ci --omit=dev'))\n"
  },
  {
    "path": "test/ni/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'pnpm i'))\n\nit('single add', _('axios', 'pnpm add axios'))\n\nit('multiple', _('eslint @types/node', 'pnpm add eslint @types/node'))\n\nit('-D', _('-D eslint @types/node', 'pnpm add -D eslint @types/node'))\n\nit('global', _('eslint -g', 'pnpm add -g eslint'))\n\nit('frozen', _('--frozen', 'pnpm i --frozen-lockfile'))\n\nit('forward1', _('--anything', 'pnpm i --anything'))\nit('forward2', _('-a', 'pnpm i -a'))\n\nit('production', _('-P', 'pnpm i --production'))\n\nit('frozen production', _('--frozen -P', 'pnpm i --frozen-lockfile --production'))\n"
  },
  {
    "path": "test/ni/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn install'))\n\nit('single add', _('axios', 'yarn add axios'))\n\nit('multiple', _('eslint @types/node', 'yarn add eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D'))\n\nit('global', _('eslint ni -g', 'yarn global add eslint ni'))\n\nit('frozen', _('--frozen', 'yarn install --frozen-lockfile'))\n\nit('production', _('-P', 'yarn install --production'))\n\nit('frozen production', _('--frozen -P', 'yarn install --frozen-lockfile --production'))\n"
  },
  {
    "path": "test/ni/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn install'))\n\nit('single add', _('axios', 'yarn add axios'))\n\nit('multiple', _('eslint @types/node', 'yarn add eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D'))\n\nit('global', _('eslint ni -g', 'npm i -g eslint ni'))\n\nit('frozen', _('--frozen', 'yarn install --immutable'))\n\nit('production', _('-P', 'yarn install --production'))\n\nit('frozen production', _('--frozen -P', 'yarn install --immutable --production'))\n"
  },
  {
    "path": "test/nlx/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'bun x esbuild'))\nit('multiple', _('esbuild --version', 'bun x esbuild --version'))\n"
  },
  {
    "path": "test/nlx/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'deno run npm:esbuild'))\nit('multiple', _('esbuild --version', 'deno run npm:esbuild --version'))\nit('vitest', _('vitest', 'deno run npm:vitest'))\nit('with args', _('typescript --version', 'deno run npm:typescript --version'))\n"
  },
  {
    "path": "test/nlx/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'npx esbuild'))\nit('multiple', _('esbuild --version', 'npx esbuild --version'))\n"
  },
  {
    "path": "test/nlx/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'pnpm dlx esbuild'))\nit('multiple', _('esbuild --version', 'pnpm dlx esbuild --version'))\n"
  },
  {
    "path": "test/nlx/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'npx esbuild'))\nit('multiple', _('esbuild --version', 'npx esbuild --version'))\n"
  },
  {
    "path": "test/nlx/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('esbuild', 'yarn dlx esbuild'))\nit('multiple', _('esbuild --version', 'yarn dlx esbuild --version'))\n"
  },
  {
    "path": "test/nr/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'bun run start'))\n\nit('script', _('dev', 'bun run dev'))\n\nit('script with arguments', _('build --watch -o', 'bun run build --watch -o'))\n\nit('colon', _('build:dev', 'bun run build:dev'))\n"
  },
  {
    "path": "test/nr/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'deno task start'))\n\nit('if-present', _('test --if-present', 'deno task --if-present test'))\n\nit('script', _('dev', 'deno task dev'))\n\nit('script with arguments', _('build --watch -o', 'deno task build --watch -o'))\n\nit('colon', _('build:dev', 'deno task build:dev'))\n"
  },
  {
    "path": "test/nr/nodeRunAgent.spec.ts",
    "content": "import type { ResolvedCommand } from 'package-manager-detector'\nimport { beforeEach, expect, it, vi } from 'vitest'\nimport { parseNr } from '../../src/commands'\n\nconst agent = 'npm'\nconst [majorNodeVersion] = process.versions.node.split('.').map(Number)\nconst supportsNodeRun = majorNodeVersion >= 22\n\nfunction _(arg: string, expected: ResolvedCommand) {\n  return async () => {\n    expect(\n      await parseNr(agent, arg.split(' ').filter(Boolean)),\n    ).toEqual(\n      expected,\n    )\n  }\n}\n\nfunction expectError(arg: string) {\n  return async () => {\n    await expect(\n      parseNr(agent, arg.split(' ').filter(Boolean)),\n    ).rejects.toThrow('requires Node.js 22.0.0 or higher')\n  }\n}\n\nbeforeEach(() => {\n  vi.stubEnv('NI_RUN_AGENT', 'node')\n})\n\nit('empty', supportsNodeRun ? _('', { command: 'node', args: ['--run', 'start'] }) : expectError(''))\n\nit('if-present', supportsNodeRun ? _('test --if-present', { command: 'node', args: ['--run', 'test'] }) : expectError('test --if-present'))\n\nit('script', supportsNodeRun ? _('dev', { command: 'node', args: ['--run', 'dev'] }) : expectError('dev'))\n\nit('script with arguments', supportsNodeRun ? _('build --watch -o', { command: 'node', args: ['--run', 'build', '--watch', '-o'] }) : expectError('build --watch -o'))\n\nit('colon', supportsNodeRun ? _('build:dev', { command: 'node', args: ['--run', 'build:dev'] }) : expectError('build:dev'))\n"
  },
  {
    "path": "test/nr/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'npm run start'))\n\nit('if-present', _('test --if-present', 'npm run --if-present test'))\n\nit('script', _('dev', 'npm run dev'))\n\nit('script with arguments', _('build --watch -o', 'npm run build -- --watch -o'))\n\nit('colon', _('build:dev', 'npm run build:dev'))\n"
  },
  {
    "path": "test/nr/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'pnpm run start'))\n\nit('if-present', _('test --if-present', 'pnpm run --if-present test'))\n\nit('script', _('dev', 'pnpm run dev'))\n\nit('script with arguments', _('build --watch -o', 'pnpm run build --watch -o'))\n\nit('colon', _('build:dev', 'pnpm run build:dev'))\n"
  },
  {
    "path": "test/nr/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn run start'))\n\nit('if-present', _('test --if-present', 'yarn run --if-present test'))\n\nit('script', _('dev', 'yarn run dev'))\n\nit('script with arguments', _('build --watch -o', 'yarn run build --watch -o'))\n\nit('colon', _('build:dev', 'yarn run build:dev'))\n"
  },
  {
    "path": "test/nr/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNr(agent, arg.split(/\\s/g).filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn run start'))\n\nit('if-present', _('test --if-present', 'yarn run --if-present test'))\n\nit('script', _('dev', 'yarn run dev'))\n\nit('script with arguments', _('build --watch -o', 'yarn run build --watch -o'))\n\nit('colon', _('build:dev', 'yarn run build:dev'))\n"
  },
  {
    "path": "test/nun/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('axios', 'bun remove axios'))\n\nit('multiple', _('eslint @types/node', 'bun remove eslint @types/node'))\n\nit('global', _('eslint ni -g', 'bun remove -g eslint ni'))\n"
  },
  {
    "path": "test/nun/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('webpack', 'deno remove webpack'))\nit('multiple', _('webpack eslint', 'deno remove webpack eslint'))\nit('global', _('webpack -g', 'deno uninstall -g webpack'))\nit('forward', _('webpack --save-dev', 'deno remove webpack --save-dev'))\n"
  },
  {
    "path": "test/nun/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('axios', 'npm uninstall axios'))\n\nit('multiple', _('eslint @types/node', 'npm uninstall eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'npm uninstall eslint @types/node -D'))\n\nit('global', _('eslint -g', 'npm uninstall -g eslint'))\n"
  },
  {
    "path": "test/nun/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single add', _('axios', 'pnpm remove axios'))\n\nit('multiple', _('eslint @types/node', 'pnpm remove eslint @types/node'))\n\nit('-D', _('-D eslint @types/node', 'pnpm remove -D eslint @types/node'))\n\nit('global', _('eslint -g', 'pnpm remove --global eslint'))\n"
  },
  {
    "path": "test/nun/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single uninstall', _('axios', 'yarn remove axios'))\n\nit('multiple', _('eslint @types/node', 'yarn remove eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D'))\n\nit('global', _('eslint ni -g', 'yarn global remove eslint ni'))\n"
  },
  {
    "path": "test/nun/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('single add', _('axios', 'yarn remove axios'))\n\nit('multiple', _('eslint @types/node', 'yarn remove eslint @types/node'))\n\nit('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D'))\n\nit('global', _('eslint ni -g', 'npm uninstall -g eslint ni'))\n"
  },
  {
    "path": "test/nup/bun.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\nfunction _(arg: string, expected: string | null) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'bun update'))\n\nit('interactive', _('-i', 'bun update -i'))\n\nit('interactive latest', _('-i --latest', 'bun update -i --latest'))\n"
  },
  {
    "path": "test/nup/deno.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'deno outdated --update'))\n\nit('interactive', _('-i', 'deno outdated --update'))\n\nit('interactive latest', _('-i --latest', 'deno outdated --update --latest'))\n"
  },
  {
    "path": "test/nup/npm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'npm update'))\n"
  },
  {
    "path": "test/nup/pnpm.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'pnpm update'))\n\nit('interactive', _('-i', 'pnpm update -i'))\n\nit('interactive latest', _('-i --latest', 'pnpm update -i --latest'))\n"
  },
  {
    "path": "test/nup/yarn.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn upgrade'))\n\nit('interactive', _('-i', 'yarn upgrade-interactive'))\n\nit('interactive latest', _('-i --latest', 'yarn upgrade-interactive --latest'))\n"
  },
  {
    "path": "test/nup/yarn@berry.spec.ts",
    "content": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@berry'\nfunction _(arg: string, expected: string) {\n  return async () => {\n    expect(\n      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),\n    ).toBe(\n      expected,\n    )\n  }\n}\n\nit('empty', _('', 'yarn up'))\n\nit('interactive', _('-i', 'yarn up -i'))\n"
  },
  {
    "path": "test/programmatic/__snapshots__/detect.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`lockfile > bun 1`] = `\"bun\"`;\n\nexports[`lockfile > deno 1`] = `\"deno\"`;\n\nexports[`lockfile > npm 1`] = `\"npm\"`;\n\nexports[`lockfile > pnpm 1`] = `\"pnpm\"`;\n\nexports[`lockfile > pnpm@6 1`] = `\"pnpm\"`;\n\nexports[`lockfile > unknown 1`] = `undefined`;\n\nexports[`lockfile > yarn 1`] = `\"yarn\"`;\n\nexports[`lockfile > yarn@berry 1`] = `\"yarn\"`;\n\nexports[`packager > bun 1`] = `\"bun\"`;\n\nexports[`packager > deno 1`] = `\"deno\"`;\n\nexports[`packager > npm 1`] = `\"npm\"`;\n\nexports[`packager > pnpm 1`] = `\"pnpm\"`;\n\nexports[`packager > pnpm@6 1`] = `\"pnpm@6\"`;\n\nexports[`packager > unknown 1`] = `undefined`;\n\nexports[`packager > yarn 1`] = `\"yarn\"`;\n\nexports[`packager > yarn@berry 1`] = `\"yarn@berry\"`;\n"
  },
  {
    "path": "test/programmatic/__snapshots__/runCli.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`debug mode > should return command results in plain text format 1`] = `\"npm i @antfu/ni\"`;\n\nexports[`lockfile > bun > na 1`] = `\"bun\"`;\n\nexports[`lockfile > bun > na run foo 1`] = `\"bun run foo\"`;\n\nexports[`lockfile > bun > ni --frozen 1`] = `\"bun install --frozen-lockfile\"`;\n\nexports[`lockfile > bun > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > bun > ni 1`] = `\"bun install\"`;\n\nexports[`lockfile > bun > ni foo -D 1`] = `\"bun add foo -d\"`;\n\nexports[`lockfile > bun > ni foo 1`] = `\"bun add foo\"`;\n\nexports[`lockfile > bun > nlx 1`] = `\"bun x foo\"`;\n\nexports[`lockfile > bun > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > bun > nun foo 1`] = `\"bun remove foo\"`;\n\nexports[`lockfile > bun > nup -i 1`] = `\"bun update -i\"`;\n\nexports[`lockfile > bun > nup 1`] = `\"bun update\"`;\n\nexports[`lockfile > deno > na 1`] = `\"deno\"`;\n\nexports[`lockfile > deno > na run foo 1`] = `\"deno run foo\"`;\n\nexports[`lockfile > deno > ni --frozen 1`] = `\"deno install --frozen\"`;\n\nexports[`lockfile > deno > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > deno > ni 1`] = `\"deno install\"`;\n\nexports[`lockfile > deno > ni foo -D 1`] = `\"deno add foo -D\"`;\n\nexports[`lockfile > deno > ni foo 1`] = `\"deno add foo\"`;\n\nexports[`lockfile > deno > nlx 1`] = `\"deno run npm:foo\"`;\n\nexports[`lockfile > deno > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > deno > nun foo 1`] = `\"deno remove foo\"`;\n\nexports[`lockfile > deno > nup -i 1`] = `\"deno outdated --update\"`;\n\nexports[`lockfile > deno > nup 1`] = `\"deno outdated --update\"`;\n\nexports[`lockfile > npm > na 1`] = `\"npm\"`;\n\nexports[`lockfile > npm > na run foo 1`] = `\"npm run foo\"`;\n\nexports[`lockfile > npm > ni --frozen 1`] = `\"npm ci\"`;\n\nexports[`lockfile > npm > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > npm > ni 1`] = `\"npm i\"`;\n\nexports[`lockfile > npm > ni foo -D 1`] = `\"npm i foo -D\"`;\n\nexports[`lockfile > npm > ni foo 1`] = `\"npm i foo\"`;\n\nexports[`lockfile > npm > nlx 1`] = `\"npx foo\"`;\n\nexports[`lockfile > npm > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > npm > nun foo 1`] = `\"npm uninstall foo\"`;\n\nexports[`lockfile > npm > nup -i 1`] = `\"Command \"upgrade-interactive\" is not support by agent \"npm\"\"`;\n\nexports[`lockfile > npm > nup 1`] = `\"npm update\"`;\n\nexports[`lockfile > pnpm > na 1`] = `\"pnpm\"`;\n\nexports[`lockfile > pnpm > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`lockfile > pnpm > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`lockfile > pnpm > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > pnpm > ni 1`] = `\"pnpm i\"`;\n\nexports[`lockfile > pnpm > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`lockfile > pnpm > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`lockfile > pnpm > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`lockfile > pnpm > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > pnpm > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`lockfile > pnpm > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`lockfile > pnpm > nup 1`] = `\"pnpm update\"`;\n\nexports[`lockfile > pnpm@6 > na 1`] = `\"pnpm\"`;\n\nexports[`lockfile > pnpm@6 > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`lockfile > pnpm@6 > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`lockfile > pnpm@6 > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > pnpm@6 > ni 1`] = `\"pnpm i\"`;\n\nexports[`lockfile > pnpm@6 > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`lockfile > pnpm@6 > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`lockfile > pnpm@6 > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`lockfile > pnpm@6 > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > pnpm@6 > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`lockfile > pnpm@6 > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`lockfile > pnpm@6 > nup 1`] = `\"pnpm update\"`;\n\nexports[`lockfile > unknown > na 1`] = `\"pnpm\"`;\n\nexports[`lockfile > unknown > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`lockfile > unknown > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`lockfile > unknown > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > unknown > ni 1`] = `\"pnpm i\"`;\n\nexports[`lockfile > unknown > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`lockfile > unknown > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`lockfile > unknown > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`lockfile > unknown > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > unknown > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`lockfile > unknown > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`lockfile > unknown > nup 1`] = `\"pnpm update\"`;\n\nexports[`lockfile > yarn > na 1`] = `\"yarn\"`;\n\nexports[`lockfile > yarn > na run foo 1`] = `\"yarn run foo\"`;\n\nexports[`lockfile > yarn > ni --frozen 1`] = `\"yarn install --frozen-lockfile\"`;\n\nexports[`lockfile > yarn > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > yarn > ni 1`] = `\"yarn install\"`;\n\nexports[`lockfile > yarn > ni foo -D 1`] = `\"yarn add foo -D\"`;\n\nexports[`lockfile > yarn > ni foo 1`] = `\"yarn add foo\"`;\n\nexports[`lockfile > yarn > nlx 1`] = `\"npx foo\"`;\n\nexports[`lockfile > yarn > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > yarn > nun foo 1`] = `\"yarn remove foo\"`;\n\nexports[`lockfile > yarn > nup -i 1`] = `\"yarn upgrade-interactive\"`;\n\nexports[`lockfile > yarn > nup 1`] = `\"yarn upgrade\"`;\n\nexports[`lockfile > yarn@berry > na 1`] = `\"yarn\"`;\n\nexports[`lockfile > yarn@berry > na run foo 1`] = `\"yarn run foo\"`;\n\nexports[`lockfile > yarn@berry > ni --frozen 1`] = `\"yarn install --frozen-lockfile\"`;\n\nexports[`lockfile > yarn@berry > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`lockfile > yarn@berry > ni 1`] = `\"yarn install\"`;\n\nexports[`lockfile > yarn@berry > ni foo -D 1`] = `\"yarn add foo -D\"`;\n\nexports[`lockfile > yarn@berry > ni foo 1`] = `\"yarn add foo\"`;\n\nexports[`lockfile > yarn@berry > nlx 1`] = `\"npx foo\"`;\n\nexports[`lockfile > yarn@berry > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`lockfile > yarn@berry > nun foo 1`] = `\"yarn remove foo\"`;\n\nexports[`lockfile > yarn@berry > nup -i 1`] = `\"yarn upgrade-interactive\"`;\n\nexports[`lockfile > yarn@berry > nup 1`] = `\"yarn upgrade\"`;\n\nexports[`packager > bun > na 1`] = `\"bun\"`;\n\nexports[`packager > bun > na run foo 1`] = `\"bun run foo\"`;\n\nexports[`packager > bun > ni --frozen 1`] = `\"bun install --frozen-lockfile\"`;\n\nexports[`packager > bun > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > bun > ni 1`] = `\"bun install\"`;\n\nexports[`packager > bun > ni foo -D 1`] = `\"bun add foo -d\"`;\n\nexports[`packager > bun > ni foo 1`] = `\"bun add foo\"`;\n\nexports[`packager > bun > nlx 1`] = `\"bun x foo\"`;\n\nexports[`packager > bun > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > bun > nun foo 1`] = `\"bun remove foo\"`;\n\nexports[`packager > bun > nup -i 1`] = `\"bun update -i\"`;\n\nexports[`packager > bun > nup 1`] = `\"bun update\"`;\n\nexports[`packager > deno > na 1`] = `\"deno\"`;\n\nexports[`packager > deno > na run foo 1`] = `\"deno run foo\"`;\n\nexports[`packager > deno > ni --frozen 1`] = `\"deno install --frozen\"`;\n\nexports[`packager > deno > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > deno > ni 1`] = `\"deno install\"`;\n\nexports[`packager > deno > ni foo -D 1`] = `\"deno add foo -D\"`;\n\nexports[`packager > deno > ni foo 1`] = `\"deno add foo\"`;\n\nexports[`packager > deno > nlx 1`] = `\"deno run npm:foo\"`;\n\nexports[`packager > deno > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > deno > nun foo 1`] = `\"deno remove foo\"`;\n\nexports[`packager > deno > nup -i 1`] = `\"deno outdated --update\"`;\n\nexports[`packager > deno > nup 1`] = `\"deno outdated --update\"`;\n\nexports[`packager > npm > na 1`] = `\"npm\"`;\n\nexports[`packager > npm > na run foo 1`] = `\"npm run foo\"`;\n\nexports[`packager > npm > ni --frozen 1`] = `\"npm ci\"`;\n\nexports[`packager > npm > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > npm > ni 1`] = `\"npm i\"`;\n\nexports[`packager > npm > ni foo -D 1`] = `\"npm i foo -D\"`;\n\nexports[`packager > npm > ni foo 1`] = `\"npm i foo\"`;\n\nexports[`packager > npm > nlx 1`] = `\"npx foo\"`;\n\nexports[`packager > npm > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > npm > nun foo 1`] = `\"npm uninstall foo\"`;\n\nexports[`packager > npm > nup -i 1`] = `\"Command \"upgrade-interactive\" is not support by agent \"npm\"\"`;\n\nexports[`packager > npm > nup 1`] = `\"npm update\"`;\n\nexports[`packager > pnpm > na 1`] = `\"pnpm\"`;\n\nexports[`packager > pnpm > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`packager > pnpm > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`packager > pnpm > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > pnpm > ni 1`] = `\"pnpm i\"`;\n\nexports[`packager > pnpm > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`packager > pnpm > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`packager > pnpm > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`packager > pnpm > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > pnpm > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`packager > pnpm > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`packager > pnpm > nup 1`] = `\"pnpm update\"`;\n\nexports[`packager > pnpm@6 > na 1`] = `\"pnpm\"`;\n\nexports[`packager > pnpm@6 > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`packager > pnpm@6 > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`packager > pnpm@6 > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > pnpm@6 > ni 1`] = `\"pnpm i\"`;\n\nexports[`packager > pnpm@6 > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`packager > pnpm@6 > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`packager > pnpm@6 > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`packager > pnpm@6 > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > pnpm@6 > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`packager > pnpm@6 > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`packager > pnpm@6 > nup 1`] = `\"pnpm update\"`;\n\nexports[`packager > unknown > na 1`] = `\"pnpm\"`;\n\nexports[`packager > unknown > na run foo 1`] = `\"pnpm run foo\"`;\n\nexports[`packager > unknown > ni --frozen 1`] = `\"pnpm i --frozen-lockfile\"`;\n\nexports[`packager > unknown > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > unknown > ni 1`] = `\"pnpm i\"`;\n\nexports[`packager > unknown > ni foo -D 1`] = `\"pnpm add foo -D\"`;\n\nexports[`packager > unknown > ni foo 1`] = `\"pnpm add foo\"`;\n\nexports[`packager > unknown > nlx 1`] = `\"pnpm dlx foo\"`;\n\nexports[`packager > unknown > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > unknown > nun foo 1`] = `\"pnpm remove foo\"`;\n\nexports[`packager > unknown > nup -i 1`] = `\"pnpm update -i\"`;\n\nexports[`packager > unknown > nup 1`] = `\"pnpm update\"`;\n\nexports[`packager > yarn > na 1`] = `\"yarn\"`;\n\nexports[`packager > yarn > na run foo 1`] = `\"yarn run foo\"`;\n\nexports[`packager > yarn > ni --frozen 1`] = `\"yarn install --frozen-lockfile\"`;\n\nexports[`packager > yarn > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > yarn > ni 1`] = `\"yarn install\"`;\n\nexports[`packager > yarn > ni foo -D 1`] = `\"yarn add foo -D\"`;\n\nexports[`packager > yarn > ni foo 1`] = `\"yarn add foo\"`;\n\nexports[`packager > yarn > nlx 1`] = `\"npx foo\"`;\n\nexports[`packager > yarn > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > yarn > nun foo 1`] = `\"yarn remove foo\"`;\n\nexports[`packager > yarn > nup -i 1`] = `\"yarn upgrade-interactive\"`;\n\nexports[`packager > yarn > nup 1`] = `\"yarn upgrade\"`;\n\nexports[`packager > yarn@berry > na 1`] = `\"yarn\"`;\n\nexports[`packager > yarn@berry > na run foo 1`] = `\"yarn run foo\"`;\n\nexports[`packager > yarn@berry > ni --frozen 1`] = `\"yarn install --immutable\"`;\n\nexports[`packager > yarn@berry > ni -g foo 1`] = `\"npm i -g foo\"`;\n\nexports[`packager > yarn@berry > ni 1`] = `\"yarn install\"`;\n\nexports[`packager > yarn@berry > ni foo -D 1`] = `\"yarn add foo -D\"`;\n\nexports[`packager > yarn@berry > ni foo 1`] = `\"yarn add foo\"`;\n\nexports[`packager > yarn@berry > nlx 1`] = `\"yarn dlx foo\"`;\n\nexports[`packager > yarn@berry > nun -g foo 1`] = `\"npm uninstall -g foo\"`;\n\nexports[`packager > yarn@berry > nun foo 1`] = `\"yarn remove foo\"`;\n\nexports[`packager > yarn@berry > nup -i 1`] = `\"yarn up -i\"`;\n\nexports[`packager > yarn@berry > nup 1`] = `\"yarn up\"`;\n"
  },
  {
    "path": "test/programmatic/catalog.spec.ts",
    "content": "import fs from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nvi.mock('../../src/detect', () => ({\n  detect: vi.fn(() => 'pnpm'),\n}))\n\nvi.mock('../../src/config', async (importOriginal) => {\n  const original = await importOriginal<typeof import('../../src/config')>()\n  return {\n    ...original,\n    getConfig: vi.fn(async () => ({\n      defaultAgent: 'pnpm',\n      globalAgent: 'npm',\n      runAgent: undefined,\n      useSfw: false,\n      catalog: true,\n    })),\n    getDefaultAgent: vi.fn(async () => 'pnpm'),\n    getGlobalAgent: vi.fn(async () => 'npm'),\n    getRunAgent: vi.fn(async () => undefined),\n    getUseSfw: vi.fn(async () => false),\n    getCatalog: vi.fn(async () => true),\n  }\n})\n\nvi.mock('fast-npm-meta', () => ({\n  getLatestVersion: vi.fn(async (name: string) => ({\n    name,\n    version: '1.0.0',\n  })),\n}))\n\nvi.mock('@posva/prompts', () => ({\n  default: vi.fn(async () => ({})),\n}))\n\nasync function createTempDir(fixture: string): Promise<string> {\n  const tmp = await fs.promises.mkdtemp(path.join(tmpdir(), 'ni-catalog-'))\n  const fixtureDir = path.join(__dirname, '..', 'fixtures', 'catalog', fixture)\n  await fs.promises.cp(fixtureDir, tmp, { recursive: true })\n  return tmp\n}\n\nfunction readJson(filePath: string) {\n  return JSON.parse(fs.readFileSync(filePath, 'utf-8'))\n}\n\nbeforeEach(() => {\n  vi.restoreAllMocks()\n})\n\ndescribe('catalog handler - named catalogs', () => {\n  it('package found in catalog → updates package.json, returns pnpm install', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })\n\n    expect(result).toBeDefined()\n    expect(result!.command).toBe('pnpm')\n    expect(result!.args).toEqual(['i'])\n\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.dependencies.react).toBe('catalog:prod')\n  })\n\n  it('multiple packages in different catalogs', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react', 'typescript'], { cwd, programmatic: true })\n\n    expect(result).toBeDefined()\n    expect(result!.command).toBe('pnpm')\n    expect(result!.args).toEqual(['i'])\n\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.dependencies.react).toBe('catalog:prod')\n    expect(pkg.dependencies.typescript).toBe('catalog:dev')\n  })\n\n  it('-D flag → writes to devDependencies', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react', '-D'], { cwd, programmatic: true })\n\n    expect(result).toBeDefined()\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.devDependencies.react).toBe('catalog:prod')\n    expect(pkg.dependencies?.react).toBeUndefined()\n  })\n\n  it('unknown package in programmatic mode → skips catalog', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['unknown-pkg'], { cwd, programmatic: true })\n\n    // In programmatic mode, unknown packages are skipped → falls through\n    expect(result).toBeUndefined()\n  })\n\n  it('mixed known/unknown packages in programmatic mode', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react', 'unknown-pkg'], { cwd, programmatic: true })\n\n    // react is cataloged, unknown-pkg is skipped → add command for skipped ones\n    expect(result).toBeDefined()\n    expect(result!.command).toBe('pnpm')\n    expect(result!.args).toContain('unknown-pkg')\n\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.dependencies.react).toBe('catalog:prod')\n  })\n})\n\ndescribe('catalog handler - default catalog only', () => {\n  it('package found → uses catalog: ref (no name)', async () => {\n    const cwd = await createTempDir('pnpm-default-only')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })\n\n    expect(result).toBeDefined()\n    expect(result!.args).toEqual(['i'])\n\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.dependencies.react).toBe('catalog:')\n  })\n\n  it('new package → adds to default catalog without prompt', async () => {\n    const cwd = await createTempDir('pnpm-default-only')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['lodash'], { cwd, programmatic: true })\n\n    expect(result).toBeDefined()\n    expect(result!.args).toEqual(['i'])\n\n    // Check workspace yaml was updated\n    const yamlContent = fs.readFileSync(path.join(cwd, 'pnpm-workspace.yaml'), 'utf-8')\n    expect(yamlContent).toContain('lodash')\n\n    // Check package.json uses catalog:\n    const pkg = readJson(path.join(cwd, 'package.json'))\n    expect(pkg.dependencies.lodash).toBe('catalog:')\n  })\n})\n\ndescribe('catalog handler - skip conditions', () => {\n  it('returns undefined for non-pnpm agent', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('npm', ['react'], { cwd, programmatic: true })\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined when no packages in args (bare install)', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', [], { cwd, programmatic: true })\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined when only flags', async () => {\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['--frozen'], { cwd, programmatic: true })\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined when catalog config disabled', async () => {\n    const { getCatalog } = await import('../../src/config')\n    vi.mocked(getCatalog).mockResolvedValueOnce(false)\n\n    const cwd = await createTempDir('pnpm')\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n\n    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })\n    expect(result).toBeUndefined()\n  })\n})\n\ndescribe('catalog handler - subdirectory', () => {\n  it('finds closest package.json from subdirectory', async () => {\n    const cwd = await createTempDir('pnpm')\n    const subDir = path.join(cwd, 'packages', 'app')\n\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n    const result = await handleCatalogInstall('pnpm', ['react'], { cwd: subDir, programmatic: true })\n\n    expect(result).toBeDefined()\n\n    // Should write to the subdirectory's package.json (closest)\n    const pkg = readJson(path.join(subDir, 'package.json'))\n    expect(pkg.dependencies.react).toBe('catalog:prod')\n  })\n\n  it('-w flag targets workspace root package.json', async () => {\n    const cwd = await createTempDir('pnpm')\n    const subDir = path.join(cwd, 'packages', 'app')\n\n    const { handleCatalogInstall } = await import('../../src/catalog/handler')\n    const result = await handleCatalogInstall('pnpm', ['react', '-w'], { cwd: subDir, programmatic: true })\n\n    expect(result).toBeDefined()\n\n    // Should write to root package.json, not subdirectory\n    const rootPkg = readJson(path.join(cwd, 'package.json'))\n    expect(rootPkg.dependencies.react).toBe('catalog:prod')\n\n    // Subdirectory package.json should be unchanged\n    const subPkg = readJson(path.join(subDir, 'package.json'))\n    expect(subPkg.dependencies.react).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "test/programmatic/detect.spec.ts",
    "content": "import type { MockInstance } from 'vitest'\nimport fs from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'\nimport { AGENTS, detect } from '../../src'\n\nlet basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance\n\nfunction detectTest(fixture: string, agent: string) {\n  return async () => {\n    const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))\n    const dir = path.join(__dirname, '..', 'fixtures', fixture, agent)\n    await fs.cp(dir, cwd, { recursive: true })\n\n    expect(await detect({ programmatic: true, cwd })).toMatchSnapshot()\n  }\n}\n\nbeforeAll(() => {\n  basicLog = vi.spyOn(console, 'log')\n  warnLog = vi.spyOn(console, 'warn')\n  errorLog = vi.spyOn(console, 'error')\n  infoLog = vi.spyOn(console, 'info')\n})\n\nafterAll(() => {\n  vi.resetAllMocks()\n})\n\nconst agents = [...AGENTS, 'unknown']\nconst fixtures = ['lockfile', 'packager']\nconst skippedAgents: string[] = []\n\n// matrix testing of: fixtures x agents\nfixtures.forEach(fixture => describe(fixture, () => agents.forEach((agent) => {\n  if (skippedAgents.includes(agent))\n    return it.skip(`skipped for ${agent}`, () => {})\n\n  it(agent, detectTest(fixture, agent))\n\n  it('no logs', () => {\n    expect(basicLog).not.toHaveBeenCalled()\n    expect(warnLog).not.toHaveBeenCalled()\n    expect(errorLog).not.toHaveBeenCalled()\n    expect(infoLog).not.toHaveBeenCalled()\n  })\n})))\n"
  },
  {
    "path": "test/programmatic/runCli.spec.ts",
    "content": "import type { MockInstance } from 'vitest'\nimport type { Runner } from '../../src'\nimport fs from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'\n\nimport { AGENTS, parseNa, parseNi, parseNlx, parseNun, parseNup, runCli } from '../../src'\n\nlet basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance\n\nfunction runCliTest(fixtureName: string, agent: string, runner: Runner, args: string[]) {\n  return async () => {\n    const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))\n    const fixture = path.join(__dirname, '..', 'fixtures', fixtureName, agent)\n    await fs.cp(fixture, cwd, { recursive: true })\n\n    await runCli(\n      async (agent, _, ctx) => {\n        // we override the args to be test specific\n        return runner(agent, args, ctx)\n      },\n      {\n        programmatic: true,\n        cwd,\n        args,\n      },\n    ).catch((e) => {\n      // it will always throw if ezspawn is mocked\n      if (e.command)\n        expect(e.command).toMatchSnapshot()\n      else\n        expect(e.message).toMatchSnapshot()\n    })\n  }\n}\n\nbeforeAll(() => {\n  basicLog = vi.spyOn(console, 'log')\n  warnLog = vi.spyOn(console, 'warn')\n  errorLog = vi.spyOn(console, 'error')\n  infoLog = vi.spyOn(console, 'info')\n\n  vi.mock('tinyexec', async (importOriginal) => {\n    const mod = await importOriginal() as any\n    return {\n      ...mod,\n      x: (cmd: string, args?: string[]) => {\n        // break execution flow for easier snapshotting\n        // eslint-disable-next-line no-throw-literal\n        throw { command: [cmd, ...(args ?? [])].join(' ') }\n      },\n    }\n  })\n})\n\nafterAll(() => {\n  vi.resetAllMocks()\n})\n\nconst agents = [...AGENTS, 'unknown']\nconst fixtures = ['lockfile', 'packager']\nconst skippedAgents: string[] = []\n\n// matrix testing of: fixtures x agents x commands\nfixtures.forEach(fixture => describe(fixture, () => agents.forEach(agent => describe(agent, () => {\n  if (skippedAgents.includes(agent))\n    return it.skip(`skipped for ${agent}`, () => {})\n\n  /** na */\n  it('na', runCliTest(fixture, agent, parseNa, []))\n  it('na run foo', runCliTest(fixture, agent, parseNa, ['run', 'foo']))\n\n  /** ni */\n  it('ni', runCliTest(fixture, agent, parseNi, []))\n  it('ni foo', runCliTest(fixture, agent, parseNi, ['foo']))\n  it('ni foo -D', runCliTest(fixture, agent, parseNi, ['foo', '-D']))\n  it('ni --frozen', runCliTest(fixture, agent, parseNi, ['--frozen']))\n  it('ni -g foo', runCliTest(fixture, agent, parseNi, ['-g', 'foo']))\n\n  /** nlx */\n  it('nlx', runCliTest(fixture, agent, parseNlx, ['foo']))\n\n  /** nup */\n  it('nup', runCliTest(fixture, agent, parseNup, []))\n  it('nup -i', runCliTest(fixture, agent, parseNup, ['-i']))\n\n  /** nun */\n  it('nun foo', runCliTest(fixture, agent, parseNun, ['foo']))\n  it('nun -g foo', runCliTest(fixture, agent, parseNun, ['-g', 'foo']))\n\n  it('no logs', () => {\n    expect(basicLog).not.toHaveBeenCalled()\n    expect(warnLog).not.toHaveBeenCalled()\n    expect(errorLog).not.toHaveBeenCalled()\n    expect(infoLog).not.toHaveBeenCalled()\n  })\n}))))\n\n// https://github.com/antfu-collective/ni/issues/266\ndescribe('debug mode', () => {\n  beforeAll(() => basicLog.mockClear())\n\n  it('ni', runCliTest('lockfile', 'npm', parseNi, ['@antfu/ni', '?']))\n  it('should return command results in plain text format', () => {\n    expect(basicLog).toHaveBeenCalled()\n\n    expect(basicLog.mock.calls[0][0]).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/runner/runCli.test.ts",
    "content": "import type { Runner } from '../../src'\nimport { afterEach, describe, expect, it, vi } from 'vitest'\nimport { runCli } from '../../src'\n\n// Mock detect to see what options are passed to it\nconst mocks = vi.hoisted(() => ({\n  detectSpy: vi.fn(() => Promise.resolve('npm')),\n  baseRunFnSpy: vi.fn<Runner>(() => Promise.resolve(undefined)),\n}))\nvi.mock('../../src/detect', () => ({\n  detect: mocks.detectSpy,\n}))\n\ndescribe('runCli', () => {\n  afterEach(() => {\n    vi.clearAllMocks()\n    vi.unstubAllEnvs()\n  })\n\n  it('run without errors', async () => {\n    const result = await runCli(mocks.baseRunFnSpy, {})\n    expect(result).toBe(undefined)\n  })\n\n  it('handle errors in programmatic mode', async () => {\n    await expect(\n      runCli(() => {\n        throw new Error('test error')\n      }, { programmatic: true }),\n    ).rejects.toThrow('test error')\n  })\n\n  it('calls detect with the correct options', async () => {\n    await runCli(mocks.baseRunFnSpy)\n    expect(mocks.detectSpy).toHaveBeenCalledWith(({ autoInstall: false, programmatic: false, cwd: expect.any(String) }))\n  })\n\n  it('detects environment options', async () => {\n    vi.stubEnv('NI_AUTO_INSTALL', 'true')\n    await runCli(mocks.baseRunFnSpy)\n    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: false, cwd: expect.any(String) })\n  })\n\n  it('accepts options as input', async () => {\n    await runCli(mocks.baseRunFnSpy, { autoInstall: true, programmatic: true })\n    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: true, cwd: expect.any(String) })\n  })\n\n  it('merges inputs and environment prioritizing inputs', async () => {\n    vi.stubEnv('NI_AUTO_INSTALL', 'true')\n    await runCli(mocks.baseRunFnSpy, { autoInstall: false, programmatic: true })\n    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, programmatic: true, cwd: expect.any(String) })\n  })\n\n  it('parses --programmatic flag from args', async () => {\n    await runCli(mocks.baseRunFnSpy, { args: ['--programmatic'] })\n    expect(mocks.detectSpy).toHaveBeenCalledWith(expect.objectContaining({ autoInstall: false, programmatic: true, cwd: expect.any(String) }))\n  })\n\n  it('removes --programmatic from args before passing to runner', async () => {\n    await runCli(mocks.baseRunFnSpy, { args: ['--programmatic', 'foo'] })\n    expect(mocks.baseRunFnSpy).toHaveBeenCalledWith('npm', ['foo'], { programmatic: true, hasLock: true, cwd: expect.any(String) })\n  })\n\n  describe('onBeforeCommand', () => {\n    it('skips running the command when exit() is called', async () => {\n      await runCli(mocks.baseRunFnSpy, { onBeforeCommand: (_args, ctx) => ctx.exit() })\n      expect(mocks.baseRunFnSpy).not.toHaveBeenCalled()\n      // https://github.com/antfu-collective/ni/issues/308\n      expect(mocks.detectSpy).not.toHaveBeenCalled()\n    })\n\n    it('continues to run the command when exit() is not called', async () => {\n      await runCli(mocks.baseRunFnSpy, { onBeforeCommand: () => Promise.resolve() })\n      expect(mocks.baseRunFnSpy).toHaveBeenCalledOnce()\n    })\n  })\n})\n"
  },
  {
    "path": "test/sfw/sfw.spec.ts",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { parseNi, runCli } from '../../src'\n\nconst mocks = vi.hoisted(() => ({\n  cmdExistsSpy: vi.fn(),\n  execSpy: vi.fn(),\n}))\n\nvi.mock('../../src/utils', async (importOriginal) => {\n  const mod = await importOriginal() as any\n  return {\n    ...mod,\n    cmdExists: mocks.cmdExistsSpy,\n  }\n})\n\nvi.mock('tinyexec', () => ({\n  x: mocks.execSpy,\n}))\n\ndescribe('sfw', () => {\n  afterEach(() => {\n    vi.clearAllMocks()\n    vi.unstubAllEnvs()\n    vi.resetModules()\n  })\n\n  it('wraps command with sfw when enabled and installed', async () => {\n    vi.stubEnv('NI_USE_SFW', 'true')\n    mocks.cmdExistsSpy.mockImplementation(() => true)\n    mocks.execSpy.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })\n\n    await runCli(parseNi, {\n      programmatic: true,\n      args: ['axios'],\n      detectVolta: false,\n    })\n\n    expect(mocks.execSpy).toHaveBeenCalledWith(\n      'sfw',\n      ['pnpm', 'add', 'axios'],\n      expect.objectContaining({\n        nodeOptions: expect.objectContaining({\n          stdio: 'inherit',\n        }),\n      }),\n    )\n  })\n\n  it('throws error when sfw is not installed', async () => {\n    vi.stubEnv('NI_USE_SFW', 'true')\n    mocks.cmdExistsSpy.mockImplementation(() => false)\n\n    await expect(\n      runCli(parseNi, {\n        programmatic: true,\n        args: ['axios'],\n        detectVolta: false,\n      }),\n    ).rejects.toThrow(/sfw is enabled but not installed/)\n\n    expect(mocks.execSpy).not.toHaveBeenCalled()\n  })\n\n  it('wraps command with sfw and volta', async () => {\n    vi.stubEnv('NI_USE_SFW', 'true')\n    mocks.cmdExistsSpy.mockImplementation(() => true)\n    mocks.execSpy.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })\n\n    await runCli(parseNi, {\n      programmatic: true,\n      args: ['axios'],\n      detectVolta: true,\n    })\n\n    expect(mocks.execSpy).toHaveBeenCalledWith(\n      'volta',\n      ['run', 'sfw', 'pnpm', 'add', 'axios'],\n      expect.objectContaining({\n        nodeOptions: expect.objectContaining({\n          stdio: 'inherit',\n        }),\n      }),\n    )\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"esnext\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown'\n\nexport default defineConfig({\n  entry: ['src/commands/*.ts'],\n  clean: true,\n  dts: true,\n  exports: true,\n  deps: {\n    onlyBundle: [\n      'which',\n      'ini',\n      '@posva/prompts',\n      'pnpm-workspace-yaml',\n      'yaml',\n      'fast-npm-meta',\n      'isexe',\n      'kleur',\n      'sisteransi',\n    ],\n  },\n})\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import process from 'node:process'\nimport { defineConfig } from 'vitest/config'\n\n// Disable global ni config in test to make the results more predictable\nprocess.env.NI_CONFIG_FILE = 'false'\n\nexport default defineConfig({\n  test: {\n\n  },\n})\n"
  }
]