Full Code of antfu-collective/ni for AI

main f96fe50f9669 cached
115 files
124.3 KB
38.2k tokens
117 symbols
1 requests
Download .txt
Repository: antfu-collective/ni
Branch: main
Commit: f96fe50f9669
Files: 115
Total size: 124.3 KB

Directory structure:
gitextract_i7fr5ot5/

├── .github/
│   └── workflows/
│       ├── publish-commit.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── bin/
│   ├── na.mjs
│   ├── nci.mjs
│   ├── nd.mjs
│   ├── ni.mjs
│   ├── nlx.mjs
│   ├── nr.mjs
│   ├── nun.mjs
│   └── nup.mjs
├── eslint.config.js
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── catalog/
│   │   ├── detect.ts
│   │   ├── handler.ts
│   │   ├── package-json.ts
│   │   ├── pnpm.ts
│   │   ├── prompt.ts
│   │   └── types.ts
│   ├── commands/
│   │   ├── index.ts
│   │   ├── na.ts
│   │   ├── nci.ts
│   │   ├── nd.ts
│   │   ├── ni.ts
│   │   ├── nlx.ts
│   │   ├── nr.ts
│   │   ├── nun.ts
│   │   └── nup.ts
│   ├── completion.ts
│   ├── config.ts
│   ├── detect.ts
│   ├── environment.ts
│   ├── fetch.ts
│   ├── fs.ts
│   ├── index.ts
│   ├── monorepo.ts
│   ├── package.ts
│   ├── parse.ts
│   ├── runner.ts
│   ├── storage.ts
│   └── utils.ts
├── taze.config.ts
├── test/
│   ├── config/
│   │   ├── .nirc
│   │   └── config.test.ts
│   ├── fixtures/
│   │   ├── catalog/
│   │   │   ├── pnpm/
│   │   │   │   ├── package.json
│   │   │   │   ├── packages/
│   │   │   │   │   └── app/
│   │   │   │   │       └── package.json
│   │   │   │   └── pnpm-workspace.yaml
│   │   │   └── pnpm-default-only/
│   │   │       ├── package.json
│   │   │       └── pnpm-workspace.yaml
│   │   ├── lockfile/
│   │   │   ├── bun/
│   │   │   │   └── bun.lockb
│   │   │   └── unknown/
│   │   │       └── future-package-manager.json
│   │   └── packager/
│   │       ├── bun/
│   │       │   └── package.json
│   │       ├── deno/
│   │       │   └── deno.json
│   │       ├── npm/
│   │       │   └── package.json
│   │       ├── pnpm/
│   │       │   └── package.json
│   │       ├── pnpm-version-range/
│   │       │   └── package.json
│   │       ├── pnpm@6/
│   │       │   └── package.json
│   │       ├── unknown/
│   │       │   └── package.json
│   │       ├── yarn/
│   │       │   └── package.json
│   │       └── yarn@berry/
│   │           └── package.json
│   ├── na/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── ng.spec.ts
│   ├── ni/
│   │   ├── bun.spec.ts
│   │   ├── catalog.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── interactive.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nlx/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nr/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── nodeRunAgent.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nun/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nup/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── programmatic/
│   │   ├── __snapshots__/
│   │   │   ├── detect.spec.ts.snap
│   │   │   └── runCli.spec.ts.snap
│   │   ├── catalog.spec.ts
│   │   ├── detect.spec.ts
│   │   └── runCli.spec.ts
│   ├── runner/
│   │   └── runCli.test.ts
│   └── sfw/
│       └── sfw.spec.ts
├── tsconfig.json
├── tsdown.config.ts
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/publish-commit.yml
================================================
name: Publish Any Commit

on:
  pull_request:
  push:
    branches:
      - main
    tags:
      - '!**'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Install pnpm
        uses: pnpm/action-setup@v4.0.0

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm build

      - run: pnpm dlx pkg-pr-new publish --pnpm


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    uses: sxzz/workflows/.github/workflows/release.yml@v1
    with:
      publish: true
    permissions:
      contents: write
      id-token: write


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x, lts/*]

    steps:
      - uses: actions/checkout@v5
      - name: Install pnpm
        uses: pnpm/action-setup@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
      - name: Install
        run: pnpm install
      - name: Lint
        run: pnpm run lint
      - name: Typecheck
        run: pnpm run typecheck
      - name: Test
        run: pnpm run test


================================================
FILE: .gitignore
================================================
_storage.json
.cache
.DS_Store
.env
.eslintcache
.ghfs
.grunt
.idea
.lock-wscript
.next
.node_repl_history
.npm
.nuxt
.nyc_output
.serverless
.vuepress/dist
.yarn-integrity
*.log
*.pid
*.pid.lock
*.seed
bower_components
build/Release
coverage
dist
jspm_packages
lib-cov
logs
node_modules
npm-debug.log*
pids
typings
yarn-debug.log*
yarn-error.log*


================================================
FILE: .vscode/settings.json
================================================
{
  // Enable the ESlint flat config support
  "eslint.experimental.useFlatConfig": true,

  // Disable the default formatter, use eslint instead
  "prettier.enable": false,
  "editor.formatOnSave": false,

  // Auto fix
  "editor.codeActionsOnSave": {
    "source.fixAll": "explicit",
    "source.organizeImports": "never"
  },

  // Silent the stylistic rules in you IDE, but still auto fix them
  "eslint.rules.customizations": [
    { "rule": "style/*", "severity": "off" },
    { "rule": "*-indent", "severity": "off" },
    { "rule": "*-spacing", "severity": "off" },
    { "rule": "*-spaces", "severity": "off" },
    { "rule": "*-order", "severity": "off" },
    { "rule": "*-dangle", "severity": "off" },
    { "rule": "*-newline", "severity": "off" },
    { "rule": "*quotes", "severity": "off" },
    { "rule": "*semi", "severity": "off" }
  ],

  // Enable eslint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml"
  ]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Anthony Fu <https://github.com/antfu>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# ni

~~*`npm i` in a yarn project, again? F\*\*k!*~~

**ni** - use the right package manager

<br>

<pre>
<code>
npm i -g <b>@antfu/ni</b>
</code>
</pre>

<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>

<br>

### `ni` - install

```bash
ni

# npm install
# yarn install
# pnpm install
# bun install
# deno install
```

```bash
ni vite

# npm i vite
# yarn add vite
# pnpm add vite
# bun add vite
# deno add vite
```

```bash
ni @types/node -D

# npm i @types/node -D
# yarn add @types/node -D
# pnpm add -D @types/node
# bun add -d @types/node
# deno add -D @types/node
```

```bash
ni -P

# npm i --omit=dev
# yarn install --production
# pnpm i --production
# bun install --production
# (deno not supported)
```

```bash
ni --frozen

# npm ci
# yarn install --frozen-lockfile (Yarn 1)
# yarn install --immutable (Yarn Berry)
# pnpm install --frozen-lockfile
# bun install --frozen-lockfile
# deno install --frozen
```

```bash
ni -g eslint

# npm i -g eslint
# yarn global add eslint (Yarn 1)
# pnpm add -g eslint
# bun add -g eslint
# deno install eslint

# this uses default agent, regardless your current working directory
```

```bash
ni -i

# interactively select the dependency to install
# search for packages by name
```

<details>
<summary>catalogs support</summary>

> Since v29.0.0

When 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.

```bash
# Given pnpm-workspace.yaml with:
#   catalogs:
#     prod:
#       react: ^18.3.0

ni react
# → detects react in "prod" catalog
# → writes "react": "catalog:prod" to package.json
# → runs pnpm install

ni lodash
# → lodash not in any catalog
# → prompts to select a catalog (or skip)
# → fetches latest version, updates pnpm-workspace.yaml
# → writes "lodash": "catalog:prod" to package.json
# → runs pnpm install
```

When 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.

Flags like `-D` are respected — the catalog ref is written to the correct `package.json` section:

```bash
ni typescript -D
# → writes "typescript": "catalog:dev" to devDependencies
```

Use `-w` / `--workspace` to target the workspace root `package.json`:

```bash
ni react -w
# → writes catalog ref to workspace root package.json
```

To disable catalog mode, set `catalog=false` in `~/.nirc` or `NI_CATALOG=false` environment variable.

</details>

<br>

### `nr` - run

```bash
nr dev --port=3000

# npm run dev -- --port=3000
# yarn run dev --port=3000
# pnpm run dev --port=3000
# bun run dev --port=3000
# deno task dev --port=3000
```

```bash
nr

# interactively select the script to run
# supports https://www.npmjs.com/package/npm-scripts-info convention
```

```bash
nr -

# rerun the last command
```

```bash
nr -p
nr -p dev

# interactively select the package and script to run
```

<details>
<summary>shell completion scripts</summary>

```bash
# Add completion script for bash
nr --completion-bash >> ~/.bashrc

# Add completion script for zsh
# For zim:fw
mkdir -p ~/.zim/custom/ni-completions
nr --completion-zsh > ~/.zim/custom/ni-completions/_ni
echo "zmodule $HOME/.zim/custom/ni-completions --fpath ." >> ~/.zimrc
zimfw install

# Add completion script for fish
mkdir -p ~/.config/fish/completions
nr --completion-fish > ~/.config/fish/completions/nr.fish
```

</details>

<br>

### `nlx` - download & execute

```bash
nlx vitest

# npx vitest
# yarn dlx vitest
# pnpm dlx vitest
# bunx vitest
# deno run npm:vitest
```

<br>

### `nup` - upgrade

```bash
nup

# npm upgrade
# yarn upgrade (Yarn 1)
# yarn up (Yarn Berry)
# pnpm update
# bun update
# deno upgrade
```

```bash
nup -i

# (not available for npm)
# yarn upgrade-interactive (Yarn 1)
# yarn up -i (Yarn Berry)
# pnpm update -i
# bun update -i
# deno outdated -u -i
```

<br>

### `nun` - uninstall

```bash
nun webpack

# npm uninstall webpack
# yarn remove webpack
# pnpm remove webpack
# bun remove webpack
# deno remove webpack
```

```bash
nun

# interactively multi-select
# the dependencies to remove
```

```bash
nun -g silent

# npm uninstall -g silent
# yarn global remove silent
# pnpm remove -g silent
# bun remove -g silent
# deno uninstall -g silent
```

<br>

### `nci` - clean install

```bash
nci

# npm ci
# yarn install --frozen-lockfile
# pnpm install --frozen-lockfile
# bun install --frozen-lockfile
# deno cache --reload
```

<br>

### `nd` - dedupe dependencies

```bash
nd

# npm dedupe
# yarn dedupe
# pnpm dedupe
```

<br>

### `na` - agent alias

```bash
na

# npm
# yarn
# pnpm
# bun
# deno
```

```bash
na run foo

# npm run foo
# yarn run foo
# pnpm run foo
# bun run foo
# deno task foo
```

<br>

### Global Flags

```bash
# ?               | Print the command execution depends on the agent
ni vite ?

# -C              | Change directory before running the command
ni -C packages/foo vite
nr -C playground dev

# -v, --version   | Show version number
ni -v

# -h, --help      | Show help
ni -h
```

<br>

### Config

```ini
; ~/.nirc

; fallback when no lock found
defaultAgent=npm # default "prompt"

; for global installs
globalAgent=npm

; use node --run instead of package manager run command (requires Node.js 22+)
runAgent=node

; prefix commands with sfw
useSfw=true

; use catalog mode when catalogs are detected (default true)
catalog=true
```

```bash
# ~/.bashrc

# custom configuration file path
export NI_CONFIG_FILE="$HOME/.config/ni/nirc"

# environment variables have higher priority than config file if presented
export NI_DEFAULT_AGENT="npm" # default "prompt"
export NI_GLOBAL_AGENT="npm"
export NI_USE_SFW="true"
export NI_CATALOG="false" # disable catalog mode
```

```ps
# for Windows

# custom configuration file path in PowerShell accessible within the `$profile` path
$Env:NI_CONFIG_FILE = 'C:\to\your\config\location'
```

<br>

### Automatic installation

You can set `NI_AUTO_INSTALL=true` to enable automatic installation.

If the corresponding package manager (**npm**, **yarn**, **pnpm**, **bun**, or **deno**) is not installed, it will install it globally before running the command.

### Integrations

#### Homebrew

You can install ni with [Homebrew](https://brew.sh/):

```bash
brew install ni
```

#### asdf

You can also install ni via the [3rd-party asdf-plugin](https://github.com/CanRau/asdf-ni.git) maintained by [CanRau](https://github.com/CanRau)

```bash
# first add the plugin
asdf plugin add ni https://github.com/CanRau/asdf-ni.git

# then install the latest version
asdf install ni latest

# and make it globally available
asdf global ni latest
```

### How?

**ni** assumes that you work with lock-files (and you should).

Before `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).

### Trouble shooting

#### Conflicts with PowerShell

PowerShell 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:

```PowerShell
'Remove-Item Alias:ni -Force -ErrorAction Ignore'
```

If 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

- PowerShell 5 (Windows PowerShell): `C:\Users\USERNAME\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
- PowerShell 7: `C:\Users\USERNAME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1`
- VSCode: `C:\Users\USERNAME\Documents\PowerShell\Microsoft.VSCode_profile.ps1`

You can use the following script to remove the alias at shell start by adding the above command to your profile:

```PowerShell
if (-not (Test-Path $profile)) {
  New-Item -ItemType File -Path (Split-Path $profile) -Force -Name (Split-Path $profile -Leaf)
}

$profileEntry = 'Remove-Item Alias:ni -Force -ErrorAction Ignore'
$profileContent = Get-Content $profile
if ($profileContent -notcontains $profileEntry) {
  ("`n" + $profileEntry) | Out-File $profile -Append -Force -Encoding UTF8
}
```

#### `nx`, `nix` and `nu` are no longer available

We 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).

```bash
alias nx="nlx"
# or
alias nix="nlx"
# or
alias nu="nup"
```


================================================
FILE: bin/na.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/na.mjs'


================================================
FILE: bin/nci.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nci.mjs'


================================================
FILE: bin/nd.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nd.mjs'


================================================
FILE: bin/ni.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/ni.mjs'


================================================
FILE: bin/nlx.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nlx.mjs'


================================================
FILE: bin/nr.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nr.mjs'


================================================
FILE: bin/nun.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nun.mjs'


================================================
FILE: bin/nup.mjs
================================================
#!/usr/bin/env node
'use strict'
import '../dist/nup.mjs'


================================================
FILE: eslint.config.js
================================================
// @ts-check
import antfu from '@antfu/eslint-config'

export default antfu({
  pnpm: true,
})
  .removeRules(
    'markdown/heading-increment',
    'e18e/prefer-static-regex',
  )


================================================
FILE: package.json
================================================
{
  "name": "@antfu/ni",
  "type": "module",
  "version": "29.0.0",
  "packageManager": "pnpm@10.32.1",
  "description": "Use the right package manager",
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
  "license": "MIT",
  "homepage": "https://github.com/antfu-collective/ni#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/antfu-collective/ni.git"
  },
  "bugs": {
    "url": "https://github.com/antfu-collective/ni/issues"
  },
  "exports": {
    ".": "./dist/index.mjs",
    "./na": "./dist/na.mjs",
    "./nci": "./dist/nci.mjs",
    "./nd": "./dist/nd.mjs",
    "./ni": "./dist/ni.mjs",
    "./nlx": "./dist/nlx.mjs",
    "./nr": "./dist/nr.mjs",
    "./nun": "./dist/nun.mjs",
    "./nup": "./dist/nup.mjs",
    "./package.json": "./package.json"
  },
  "types": "./dist/index.d.mts",
  "bin": {
    "ni": "bin/ni.mjs",
    "nci": "bin/nci.mjs",
    "nr": "bin/nr.mjs",
    "nup": "bin/nup.mjs",
    "nd": "bin/nd.mjs",
    "nlx": "bin/nlx.mjs",
    "na": "bin/na.mjs",
    "nun": "bin/nun.mjs"
  },
  "files": [
    "bin",
    "dist"
  ],
  "engines": {
    "node": ">=20.19.0"
  },
  "scripts": {
    "prepublishOnly": "npm run build",
    "dev": "tsx src/commands/ni.ts",
    "nr": "tsx src/commands/nr.ts",
    "build": "tsdown",
    "release": "bumpp",
    "typecheck": "tsc --noEmit",
    "prepare": "simple-git-hooks",
    "lint": "eslint",
    "test": "vitest"
  },
  "dependencies": {
    "fzf": "catalog:prod",
    "package-manager-detector": "catalog:prod",
    "tinyexec": "catalog:prod",
    "tinyglobby": "catalog:prod"
  },
  "inlinedDependencies": {
    "fast-npm-meta": "1.4.2",
    "yaml": "2.8.2",
    "pnpm-workspace-yaml": "1.6.0",
    "ini": "6.0.0",
    "kleur": "4.1.5",
    "@posva/prompts": "2.4.4",
    "sisteransi": "1.0.5",
    "isexe": "4.0.0",
    "which": "6.0.1"
  },
  "devDependencies": {
    "@antfu/eslint-config": "catalog:dev",
    "@posva/prompts": "catalog:prod-inlined",
    "@types/ini": "catalog:dev",
    "@types/node": "catalog:dev",
    "@types/which": "catalog:dev",
    "bumpp": "catalog:dev",
    "eslint": "catalog:dev",
    "fast-npm-meta": "catalog:prod-inlined",
    "ini": "catalog:prod-inlined",
    "lint-staged": "catalog:dev",
    "pnpm-workspace-yaml": "catalog:prod-inlined",
    "simple-git-hooks": "catalog:dev",
    "taze": "catalog:dev",
    "tsdown": "catalog:dev",
    "tsx": "catalog:dev",
    "typescript": "catalog:dev",
    "vitest": "catalog:dev",
    "which": "catalog:prod-inlined"
  },
  "simple-git-hooks": {
    "pre-commit": "pnpm lint-staged"
  },
  "lint-staged": {
    "*": "eslint --fix"
  }

}


================================================
FILE: pnpm-workspace.yaml
================================================
shellEmulator: true

trustPolicy: no-downgrade
trustPolicyIgnoreAfter: 604800 # 7 days

packages: []
catalogs:
  dev:
    '@antfu/eslint-config': ^7.7.2
    '@types/ini': ^4.1.1
    '@types/node': ^25.5.0
    '@types/which': ^3.0.4
    bumpp: ^10.4.1
    eslint: ^10.0.3
    lint-staged: ^16.4.0
    simple-git-hooks: ^2.13.1
    taze: ^19.10.0
    tsdown: ^0.21.2
    tsx: ^4.21.0
    typescript: ^5.9.3
    vitest: ^4.1.0
  prod:
    fzf: ^0.5.2
    package-manager-detector: ^1.6.0
    tinyexec: ^1.0.4
    tinyglobby: ^0.2.15
  prod-inlined:
    '@posva/prompts': ^2.4.4
    fast-npm-meta: ^1.4.2
    ini: ^6.0.0
    pnpm-workspace-yaml: ^1.6.0
    which: ^6.0.1
onlyBuiltDependencies:
  - esbuild
  - simple-git-hooks
  - unrs-resolver


================================================
FILE: src/catalog/detect.ts
================================================
import type { Agent } from 'package-manager-detector'
import type { CatalogProvider } from './types'
import { pnpmCatalogProvider } from './pnpm'

export function getCatalogProvider(agent: Agent): CatalogProvider | null {
  if (agent === 'pnpm')
    return pnpmCatalogProvider
  return null
}


================================================
FILE: src/catalog/handler.ts
================================================
import type { Agent } from 'package-manager-detector'
import type { ExtendedResolvedCommand, RunnerContext } from '../runner'
import type { DepType } from './package-json'
import type { CatalogConfig, CatalogProvider } from './types'
import path from 'node:path'
import process from 'node:process'
import { styleText } from 'node:util'
import { getLatestVersion } from 'fast-npm-meta'
import { getCatalog } from '../config'
import { getCommand } from '../parse'
import { getCatalogProvider } from './detect'
import { findClosestPackageJson, updatePackageJsonCatalogRefs } from './package-json'
import { promptSelectCatalog } from './prompt'
import { getCatalogRef } from './types'

function splitPackagesAndFlags(args: string[]): { packages: string[], flags: string[] } {
  const packages: string[] = []
  const flags: string[] = []
  for (const arg of args) {
    if (arg.startsWith('-'))
      flags.push(arg)
    else
      packages.push(arg)
  }
  return { packages, flags }
}

function getDepType(flags: string[]): DepType {
  if (flags.includes('-D') || flags.includes('-d'))
    return 'devDependencies'
  if (flags.includes('--save-peer'))
    return 'peerDependencies'
  return 'dependencies'
}

async function resolveVersion(pkgName: string): Promise<string> {
  const meta = await getLatestVersion(pkgName)
  return `^${meta.version}`
}

async function resolveCatalogForPackage(
  provider: CatalogProvider,
  config: CatalogConfig,
  pkgName: string,
  programmatic?: boolean,
): Promise<{ catalogName: string | undefined, version?: string }> {
  // Check if already in a catalog
  const existing = provider.findPackage(config, pkgName)
  if (existing) {
    return { catalogName: existing.name }
  }

  // Prompt user to select catalog
  const { catalogName } = await promptSelectCatalog(config, pkgName, programmatic)
  if (!catalogName)
    return { catalogName: undefined }

  // Fetch latest version for new catalog entry
  const version = await resolveVersion(pkgName)
  return { catalogName, version }
}

export async function handleCatalogInstall(
  agent: Agent,
  args: string[],
  ctx?: RunnerContext,
): Promise<ExtendedResolvedCommand | undefined> {
  const catalogEnabled = await getCatalog()
  if (!catalogEnabled)
    return undefined

  const provider = getCatalogProvider(agent)
  if (!provider)
    return undefined

  // Check for workspace flag
  const hasWorkspaceFlag = args.includes('-w') || args.includes('--workspace')
  const cleanArgs = args.filter(a => a !== '-w' && a !== '--workspace')

  const { packages, flags } = splitPackagesAndFlags(cleanArgs)

  // No packages to add (bare install, frozen, etc.)
  if (packages.length === 0)
    return undefined

  const cwd = ctx?.cwd ?? process.cwd()
  const config = await provider.detect(cwd)
  if (!config)
    return undefined

  const depType = getDepType(flags)
  const catalogEntries: { name: string, catalogRef: string }[] = []
  const skippedPackages: string[] = []

  for (const pkg of packages) {
    const result = await resolveCatalogForPackage(provider, config, pkg, ctx?.programmatic)

    if (result.catalogName) {
      // Add to catalog file if it's a new entry
      if (result.version) {
        await provider.addPackage(config, result.catalogName, pkg, result.version)
        if (!ctx?.programmatic) {
          // eslint-disable-next-line no-console
          console.log(`${styleText('green', '+')} ${styleText('cyan', pkg)} ${styleText('dim', `→ ${result.catalogName} catalog (${result.version})`)}`)
        }
      }
      else if (!ctx?.programmatic) {
        const existingCatalog = provider.findPackage(config, pkg)
        // eslint-disable-next-line no-console
        console.log(`${styleText('green', '✓')} ${styleText('cyan', pkg)} ${styleText('dim', `→ found in ${existingCatalog!.name} catalog`)}`)
      }
      catalogEntries.push({ name: pkg, catalogRef: getCatalogRef(result.catalogName) })
    }
    else {
      skippedPackages.push(pkg)
    }
  }

  if (catalogEntries.length === 0)
    return undefined

  // Determine target package.json
  let pkgJsonPath: string | null
  if (hasWorkspaceFlag) {
    pkgJsonPath = path.join(path.dirname(config.filePath), 'package.json')
  }
  else {
    pkgJsonPath = findClosestPackageJson(cwd)
  }

  if (!pkgJsonPath) {
    if (!ctx?.programmatic) {
      console.error(styleText('red', '✗ No package.json found'))
      process.exit(1)
    }
    throw new Error('No package.json found')
  }

  // Update package.json with catalog refs
  updatePackageJsonCatalogRefs(pkgJsonPath, catalogEntries, depType)

  // If some packages were skipped, add them normally alongside install
  if (skippedPackages.length > 0) {
    return getCommand(agent, 'add', [...skippedPackages, ...flags])
  }

  // All packages handled via catalogs, just run install
  return getCommand(agent, 'install', [])
}


================================================
FILE: src/catalog/package-json.ts
================================================
import fs from 'node:fs'
import path from 'node:path'

export function findClosestPackageJson(cwd: string): string | null {
  let dir = path.resolve(cwd)
  while (true) {
    const filePath = path.join(dir, 'package.json')
    if (fs.existsSync(filePath))
      return filePath
    const parent = path.dirname(dir)
    if (parent === dir)
      return null
    dir = parent
  }
}

function detectIndent(content: string): string {
  const match = content.match(/^(\s+)"/m)
  return match?.[1] ?? '  '
}

export type DepType = 'dependencies' | 'devDependencies' | 'peerDependencies'

export function updatePackageJsonCatalogRefs(
  pkgJsonPath: string,
  entries: { name: string, catalogRef: string }[],
  depType: DepType,
): void {
  const content = fs.readFileSync(pkgJsonPath, 'utf-8')
  const indent = detectIndent(content)
  const data = JSON.parse(content)

  if (!data[depType])
    data[depType] = {}

  for (const entry of entries) {
    data[depType][entry.name] = entry.catalogRef
  }

  fs.writeFileSync(pkgJsonPath, `${JSON.stringify(data, null, indent)}\n`)
}


================================================
FILE: src/catalog/pnpm.ts
================================================
import type { CatalogConfig, CatalogInfo, CatalogProvider } from './types'
import fs from 'node:fs'
import path from 'node:path'
import { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml'

function findPnpmWorkspaceYaml(cwd: string): string | null {
  let dir = path.resolve(cwd)
  while (true) {
    const filePath = path.join(dir, 'pnpm-workspace.yaml')
    if (fs.existsSync(filePath))
      return filePath
    const parent = path.dirname(dir)
    if (parent === dir)
      return null
    dir = parent
  }
}

export const pnpmCatalogProvider: CatalogProvider = {
  async detect(cwd: string): Promise<CatalogConfig | null> {
    const filePath = findPnpmWorkspaceYaml(cwd)
    if (!filePath)
      return null

    const content = fs.readFileSync(filePath, 'utf-8')
    const workspace = parsePnpmWorkspaceYaml(content)
    const json = workspace.toJSON()

    const catalogs: CatalogInfo[] = []
    const hasDefaultCatalog = json.catalog != null && Object.keys(json.catalog).length > 0
    const hasNamedCatalogs = json.catalogs != null && Object.keys(json.catalogs).length > 0

    if (!hasDefaultCatalog && !hasNamedCatalogs)
      return null

    if (hasDefaultCatalog) {
      catalogs.push({
        name: 'default',
        packages: json.catalog!,
      })
    }

    if (hasNamedCatalogs) {
      for (const [name, packages] of Object.entries(json.catalogs!)) {
        catalogs.push({ name, packages })
      }
    }

    return {
      filePath,
      catalogs,
      hasDefaultCatalog,
      hasNamedCatalogs,
    }
  },

  findPackage(config: CatalogConfig, pkgName: string): CatalogInfo | undefined {
    return config.catalogs.find(c => pkgName in c.packages)
  },

  async addPackage(config: CatalogConfig, catalogName: string, pkgName: string, version: string): Promise<void> {
    const content = fs.readFileSync(config.filePath, 'utf-8')
    const workspace = parsePnpmWorkspaceYaml(content)
    workspace.setPackage(catalogName, pkgName, version)
    fs.writeFileSync(config.filePath, workspace.toString())

    // Update the in-memory config
    const existing = config.catalogs.find(c => c.name === catalogName)
    if (existing) {
      existing.packages[pkgName] = version
    }
    else {
      config.catalogs.push({ name: catalogName, packages: { [pkgName]: version } })
    }
  },
}


================================================
FILE: src/catalog/prompt.ts
================================================
import type { CatalogConfig } from './types'
import { styleText } from 'node:util'
import prompts from '@posva/prompts'

const SKIP = '__skip__'
const CREATE_NEW = '__create_new__'

export interface CatalogSelection {
  catalogName: string | undefined
}

export async function promptSelectCatalog(
  config: CatalogConfig,
  pkgName: string,
  programmatic?: boolean,
): Promise<CatalogSelection> {
  // Only default catalog: no prompt needed
  if (config.hasDefaultCatalog && !config.hasNamedCatalogs) {
    return { catalogName: 'default' }
  }

  if (programmatic) {
    return { catalogName: undefined }
  }

  const catalogChoices = config.catalogs.map(c => ({
    title: c.name,
    value: c.name,
  }))

  const { catalog } = await prompts({
    type: 'select',
    name: 'catalog',
    message: `select catalog for ${styleText('yellow', pkgName)}`,
    choices: [
      ...catalogChoices,
      { title: styleText('dim', 'create new catalog'), value: CREATE_NEW },
      { title: styleText('dim', 'skip (install without catalog)'), value: SKIP },
    ],
  })

  if (catalog === undefined || catalog === SKIP) {
    return { catalogName: undefined }
  }

  if (catalog === CREATE_NEW) {
    const newName = await promptNewCatalogName()
    return { catalogName: newName }
  }

  return { catalogName: catalog }
}

async function promptNewCatalogName(): Promise<string | undefined> {
  const { name } = await prompts({
    type: 'text',
    name: 'name',
    message: 'new catalog name',
  })
  return name || undefined
}


================================================
FILE: src/catalog/types.ts
================================================
export interface CatalogInfo {
  name: string
  packages: Record<string, string>
}

export interface CatalogConfig {
  filePath: string
  catalogs: CatalogInfo[]
  hasDefaultCatalog: boolean
  hasNamedCatalogs: boolean
}

export interface CatalogProvider {
  detect: (cwd: string) => Promise<CatalogConfig | null>
  findPackage: (config: CatalogConfig, pkgName: string) => CatalogInfo | undefined
  addPackage: (config: CatalogConfig, catalogName: string, pkgName: string, version: string) => Promise<void>
}

export function getCatalogRef(catalogName: string): string {
  return catalogName === 'default' ? 'catalog:' : `catalog:${catalogName}`
}


================================================
FILE: src/commands/index.ts
================================================
export * from '../index'


================================================
FILE: src/commands/na.ts
================================================
import { parseNa } from '../parse'
import { runCli } from '../runner'

runCli(parseNa)


================================================
FILE: src/commands/nci.ts
================================================
import { parseNi } from '../parse'
import { runCli } from '../runner'

runCli(
  (agent, args, hasLock) => parseNi(agent, [...args, '--frozen-if-present'], hasLock),
  { autoInstall: true },
)


================================================
FILE: src/commands/nd.ts
================================================
import { parseNd } from '../parse'
import { runCli } from '../runner'

runCli(parseNd)


================================================
FILE: src/commands/ni.ts
================================================
import type { Choice } from '@posva/prompts'
import process from 'node:process'
import { styleText } from 'node:util'
import prompts from '@posva/prompts'
import { Fzf } from 'fzf'
import { handleCatalogInstall } from '../catalog/handler'
import { fetchNpmPackages } from '../fetch'
import { parseNi } from '../parse'
import { runCli } from '../runner'
import { exclude } from '../utils'

runCli(async (agent, args, ctx) => {
  const isInteractive = args[0] === '-i'

  if (isInteractive) {
    let fetchPattern: string

    if (args[1] && !args[1].startsWith('-')) {
      fetchPattern = args[1]
    }
    else {
      const { pattern } = await prompts({
        type: 'text',
        name: 'pattern',
        message: 'search for package',
      })

      fetchPattern = pattern
    }

    if (!fetchPattern) {
      process.exitCode = 1
      return
    }

    const packages = await fetchNpmPackages(fetchPattern)

    if (!packages.length) {
      console.error('No results found')
      process.exitCode = 1
      return
    }

    const fzf = new Fzf(packages, {
      selector: (item: Choice) => item.title,
      casing: 'case-insensitive',
    })

    const { dependency } = await prompts({
      type: 'autocomplete',
      name: 'dependency',
      choices: packages,
      instructions: false,
      message: 'choose a package to install',
      limit: 15,
      async suggest(input: string, choices: Choice[]) {
        const results = fzf.find(input)
        return results.map(r => choices.find((c: any) => c.value === r.item.value))
      },
    })

    if (!dependency) {
      process.exitCode = 1
      return
    }

    args = exclude(args, '-d', '-p', '-i')

    /**
     * yarn and bun do not support
     * the installation of peers programmatically
     */
    const canInstallPeers = ['npm', 'pnpm'].includes(agent)

    const { mode } = await prompts({
      type: 'select',
      name: 'mode',
      message: `install ${styleText('yellow', dependency.name)} as`,
      choices: [
        {
          title: 'prod',
          value: '',
          selected: true,
        },
        {
          title: 'dev',
          value: '-D',
        },
        {
          title: `peer`,
          value: '--save-peer',
          disabled: !canInstallPeers,
        },
      ],
    })

    if (mode === undefined) {
      process.exitCode = 1
      return
    }

    args.push(dependency.name, mode)
  }

  // Catalog mode: intercept before normal add
  if (!args.includes('-g')) {
    const catalogCmd = await handleCatalogInstall(agent, args, ctx)
    if (catalogCmd !== undefined)
      return catalogCmd
  }

  return parseNi(agent, args, ctx)
})


================================================
FILE: src/commands/nlx.ts
================================================
import { parseNlx } from '../parse'
import { runCli } from '../runner'

runCli(parseNlx)


================================================
FILE: src/commands/nr.ts
================================================
import type { Choice } from '@posva/prompts'
import type { PackageScript } from '../package'
import process from 'node:process'
import prompts from '@posva/prompts'
import { byLengthAsc, Fzf } from 'fzf'
import { getCompletionSuggestions, rawBashCompletionScript, rawFishCompletionScript, rawZshCompletionScript } from '../completion'
import { readPackageScripts, readWorkspaceScripts } from '../package'
import { parseNr } from '../parse'
import { runCli } from '../runner'
import { dump, load } from '../storage'
import { limitText } from '../utils'

runCli(async (agent, args, ctx) => {
  const storage = await load()

  const promptSelectScript = async (raw: PackageScript[]) => {
    const terminalColumns = process.stdout?.columns || 80

    const last = storage.lastRunCommand
    const choices = raw.reduce<Choice[]>((acc, { key, description }) => {
      const item = {
        title: key,
        value: key,
        description: limitText(description, terminalColumns - 15),
      }
      if (last && key === last) {
        return [item, ...acc]
      }
      return [...acc, item]
    }, [])

    const fzf = new Fzf(raw, {
      selector: item => `${item.key} ${item.description}`,
      casing: 'case-insensitive',
      tiebreakers: [byLengthAsc],
    })

    // Workaround for @posva/prompts autocomplete bug where ESC key submits instead of canceling
    // https://github.com/terkelg/prompts/issues/362
    let isExited = false

    try {
      const { fn } = await prompts({
        name: 'fn',
        message: 'script to run',
        type: 'autocomplete',
        choices,
        async suggest(input: string, choices: Choice[]) {
          if (!input)
            return choices
          const results = fzf.find(input)
          return results.map(r => choices.find(c => c.value === r.item.key))
        },
        onState(state) {
          if (state.exited)
            isExited = true
        },
      })
      if (!fn || isExited)
        process.exit(1)
      args.push(fn)
    }
    catch {
      process.exit(1)
    }
  }

  // Use --completion to generate completion script and do completion logic
  // (No package manager would have an argument named --completion)
  if (args[0] === '--completion') {
    const compLine = process.env.COMP_LINE
    const rawCompCword = process.env.COMP_CWORD
    // In bash
    if (compLine !== undefined && rawCompCword !== undefined) {
      const compCword = Number.parseInt(rawCompCword, 10)
      const compWords = args.slice(1)
      // Only complete the second word (nr __here__ ...)
      if (compCword === 1) {
        const suggestions = getCompletionSuggestions(compWords, ctx)

        // eslint-disable-next-line no-console
        console.log(suggestions.join('\n'))
      }
    }
    // In other shells, return suggestions directly
    else {
      const suggestions = getCompletionSuggestions(args, ctx)

      // eslint-disable-next-line no-console
      console.log(suggestions.join('\n'))
    }
    return
  }

  // -p is a flag attempt to read scripts from monorepo
  if (args[0] === '-p') {
    const raw = await readWorkspaceScripts(ctx, args)
    // Show prompt if there are multiple scripts
    if (raw.length > 1) {
      await promptSelectScript(raw)
    }
  }

  if (args[0] === '-') {
    if (!storage.lastRunCommand) {
      if (!ctx?.programmatic) {
        console.error('No last command found')
        process.exit(1)
      }

      throw new Error('No last command found')
    }
    args[0] = storage.lastRunCommand
  }

  if (args.length === 0 && !ctx?.programmatic) {
    const raw = readPackageScripts(ctx)
    await promptSelectScript(raw)
  }

  if (storage.lastRunCommand !== args[0]) {
    storage.lastRunCommand = args[0]
    dump()
  }

  return parseNr(agent, args, ctx)
}, {
  onBeforeCommand: (args, ctx) => {
    // Print ZSH completion script.
    if (args[0] === '--completion-zsh') {
    // eslint-disable-next-line no-console
      console.log(rawZshCompletionScript)
      return ctx.exit()
    }

    // Print Bash completion script
    if (args[0] === '--completion-bash') {
    // eslint-disable-next-line no-console
      console.log(rawBashCompletionScript)
      return ctx.exit()
    }

    // Print Fish completion script
    if (args[0] === '--completion-fish') {
    // eslint-disable-next-line no-console
      console.log(rawFishCompletionScript)
      return ctx.exit()
    }
  },
})


================================================
FILE: src/commands/nun.ts
================================================
import type { Choice } from '@posva/prompts'
import process from 'node:process'
import prompts from '@posva/prompts'
import { Fzf } from 'fzf'
import { getPackageJSON } from '../fs'
import { parseNun } from '../parse'
import { runCli } from '../runner'
import { exclude } from '../utils'

runCli(async (agent, args, ctx) => {
  const isMultiple = args[0] === '-m' // Compatible with issue/311
  const isGlobal = args.includes('-g')

  const isInteractive = !args.length && !ctx?.programmatic

  if ((isInteractive || isMultiple) && !isGlobal) {
    const pkg = getPackageJSON(ctx)

    const allDependencies = { ...pkg.dependencies, ...pkg.devDependencies }

    const raw = Object.entries(allDependencies) as [string, string][]

    if (!raw.length) {
      console.error('No dependencies found')
      return
    }

    const fzf = new Fzf(raw, {
      selector: ([dep, version]) => `${dep} ${version}`,
      casing: 'case-insensitive',
    })

    const choices: Choice[] = raw.map(([dependency, version]) => ({
      title: dependency,
      value: dependency,
      description: version,
    }))

    if (isMultiple)
      args = exclude(args, '-m')

    try {
      const { depsToRemove } = await prompts({
        type: 'autocompleteMultiselect',
        name: 'depsToRemove',
        choices,
        min: 1,
        instructions: false,
        message: 'remove dependencies',
        async suggest(input: string, choices: Choice[]) {
          const results = fzf.find(input)
          return results.map(r => choices.find(c => c.value === r.item[0]))
        },
      })

      if (Array.isArray(depsToRemove))
        args.push(...depsToRemove)
    }
    catch {
      process.exit(1)
    }
  }

  return parseNun(agent, args, ctx)
})


================================================
FILE: src/commands/nup.ts
================================================
import { parseNup } from '../parse'
import { runCli } from '../runner'

runCli(parseNup)


================================================
FILE: src/completion.ts
================================================
import type { RunnerContext } from '.'
import { byLengthAsc, Fzf } from 'fzf'
import { readPackageScripts } from './package'

// Print completion script
export const rawBashCompletionScript = `
###-begin-nr-completion-###

if type complete &>/dev/null; then
  _nr_completion() {
    local words
    local cur
    local cword
    _get_comp_words_by_ref -n =: cur words cword
    IFS=$'\\n'
    COMPREPLY=($(COMP_CWORD=$cword COMP_LINE=$cur nr --completion \${words[@]}))
  }
  complete -F _nr_completion nr
fi

###-end-nr-completion-###
`.trim()

export const rawZshCompletionScript = `
#compdef nr

_nr_completion() {
  local -a completions
  completions=("\${(f)$(nr --completion $words[2,-1])}")
  
  compadd -a completions
}

_nr_completion
`.trim()

export const rawFishCompletionScript = `
function _nr_completion
  set -l tokens (commandline -xpc)
  if test (count $tokens) -ge 1
    set tokens $tokens[2..-1]
  end
  nr --completion $tokens 2>/dev/null
end

complete -c nr -f -a '(_nr_completion)' -d 'package.json scripts'
`.trim()

export function getCompletionSuggestions(args: string[], ctx: RunnerContext | undefined) {
  const raw = readPackageScripts(ctx)
  const fzf = new Fzf(raw, {
    selector: item => item.key,
    casing: 'case-insensitive',
    tiebreakers: [byLengthAsc],
  })

  const results = fzf.find(args[1] || '')

  return results.map(r => r.item.key)
}


================================================
FILE: src/config.ts
================================================
import type { Agent } from 'package-manager-detector'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import ini from 'ini'
import { detect } from './detect'

const customRcPath = process.env.NI_CONFIG_FILE

const home = process.platform === 'win32'
  ? process.env.USERPROFILE
  : process.env.HOME

const defaultRcPath = path.join(home || '~/', '.nirc')

const rcPath = customRcPath || defaultRcPath

interface Config {
  defaultAgent: Agent | 'prompt'
  globalAgent: Agent
  runAgent: 'node' | undefined
  useSfw: boolean
  catalog: boolean
}

const defaultConfig: Config = {
  defaultAgent: 'prompt',
  globalAgent: 'npm',
  runAgent: undefined,
  useSfw: false,
  catalog: true,
}

let config: Config | undefined

export async function getConfig(): Promise<Config> {
  if (!config) {
    config = { ...defaultConfig, ...fs.existsSync(rcPath)
      ? ini.parse(fs.readFileSync(rcPath, 'utf-8'))
      : null }

    if (process.env.NI_DEFAULT_AGENT)
      config.defaultAgent = process.env.NI_DEFAULT_AGENT as Agent

    if (process.env.NI_GLOBAL_AGENT)
      config.globalAgent = process.env.NI_GLOBAL_AGENT as Agent

    if (process.env.NI_RUN_AGENT === 'node')
      config.runAgent = process.env.NI_RUN_AGENT

    if (process.env.NI_USE_SFW !== undefined)
      config.useSfw = process.env.NI_USE_SFW === 'true'

    if (process.env.NI_CATALOG !== undefined)
      config.catalog = process.env.NI_CATALOG !== 'false'

    const agent = await detect({ programmatic: true })
    if (agent)
      config.defaultAgent = agent
  }

  return config
}

export async function getDefaultAgent(programmatic?: boolean) {
  const { defaultAgent } = await getConfig()
  if (defaultAgent === 'prompt' && (programmatic || process.env.CI))
    return 'npm'
  return defaultAgent
}

export async function getGlobalAgent() {
  const { globalAgent } = await getConfig()
  return globalAgent
}

export async function getRunAgent() {
  const { runAgent } = await getConfig()
  return runAgent
}

export async function getUseSfw() {
  const { useSfw } = await getConfig()
  return useSfw
}

export async function getCatalog() {
  const { catalog } = await getConfig()
  return catalog
}


================================================
FILE: src/detect.ts
================================================
import { existsSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import prompts from '@posva/prompts'
import { detect as detectPM } from 'package-manager-detector'
import { INSTALL_PAGE } from 'package-manager-detector/constants'
import { x } from 'tinyexec'
import { cmdExists, terminalLink } from './utils'

export interface DetectOptions {
  autoInstall?: boolean
  programmatic?: boolean
  cwd?: string
  /**
   * Should use Volta when present
   *
   * @see https://volta.sh/
   * @default true
   */
  detectVolta?: boolean
}

export async function detect({ autoInstall, programmatic, cwd }: DetectOptions = {}) {
  const targetDir = cwd ?? process.cwd()

  // Check for deno.json or deno.jsonc before using package-manager-detector
  if (existsSync(path.join(targetDir, 'deno.json')) || existsSync(path.join(targetDir, 'deno.jsonc'))) {
    // Return early with deno agent if deno.json/deno.jsonc is present
    return 'deno'
  }

  const {
    name,
    agent,
    version,
  } = await detectPM({
    cwd,
    onUnknown: (packageManager) => {
      if (!programmatic) {
        console.warn('[ni] Unknown packageManager:', packageManager)
      }
      return undefined
    },
  }) || {}

  // auto install
  if (name && !cmdExists(name) && !programmatic) {
    if (!autoInstall) {
      console.warn(`[ni] Detected ${name} but it doesn't seem to be installed.\n`)

      if (process.env.CI)
        process.exit(1)

      const link = terminalLink(name, INSTALL_PAGE[name])
      const { tryInstall } = await prompts({
        name: 'tryInstall',
        type: 'confirm',
        message: `Would you like to globally install ${link}?`,
      })
      if (!tryInstall)
        process.exit(1)
    }

    await x(
      'npm',
      ['i', '-g', `${name}${version ? `@${version}` : ''}`],
      {
        nodeOptions: {
          stdio: 'inherit',
          cwd,
        },
        throwOnError: true,
      },
    )
  }

  return agent
}


================================================
FILE: src/environment.ts
================================================
import process from 'node:process'

export interface EnvironmentOptions {
  autoInstall: boolean
}

const DEFAULT_ENVIRONMENT_OPTIONS: EnvironmentOptions = {
  autoInstall: false,
}

export function getEnvironmentOptions(): EnvironmentOptions {
  return {
    ...DEFAULT_ENVIRONMENT_OPTIONS,
    autoInstall: process.env.NI_AUTO_INSTALL === 'true',
  }
}


================================================
FILE: src/fetch.ts
================================================
import type { Choice } from '@posva/prompts'
import process from 'node:process'
import { styleText } from 'node:util'
import { formatPackageWithUrl } from './utils'

export interface NpmPackage {
  name: string
  description: string
  version: string
  keywords: string[]
  date: string
  links: {
    npm: string
    homepage: string
    repository: string
  }
}

interface NpmRegistryResponse {
  objects: { package: NpmPackage }[]
}

export async function fetchNpmPackages(pattern: string): Promise<Choice[]> {
  const registryLink = (pattern: string) =>
    `https://registry.npmjs.com/-/v1/search?text=${pattern}&size=35`

  const terminalColumns = process.stdout?.columns || 80

  try {
    const result = await fetch(registryLink(pattern))
      .then(res => res.json()) as NpmRegistryResponse

    return result.objects.map(({ package: pkg }) => ({
      title: formatPackageWithUrl(
        `${pkg.name.padEnd(30, ' ')} ${styleText('blue', `v${pkg.version}`)}`,
        pkg.links.repository ?? pkg.links.npm,
        terminalColumns,
      ),
      value: pkg,
    }))
  }
  catch {
    console.error('Error when fetching npm registry')
    process.exit(1)
  }
}


================================================
FILE: src/fs.ts
================================================
import type { RunnerContext } from './runner'
import fs from 'node:fs'
import { resolve } from 'node:path'
import process from 'node:process'

export function getPackageJSON(ctx?: RunnerContext): any {
  const cwd = ctx?.cwd ?? process.cwd()
  const path = resolve(cwd, 'package.json')

  if (fs.existsSync(path)) {
    try {
      const raw = fs.readFileSync(path, 'utf-8')
      const data = JSON.parse(raw)
      return data
    }
    catch (e) {
      if (!ctx?.programmatic) {
        console.warn('Failed to parse package.json')
        process.exit(1)
      }

      throw e
    }
  }
}


================================================
FILE: src/index.ts
================================================
export * from './config'
export * from './detect'

export * from './parse'
export * from './runner'
export * from './utils'
export * from 'package-manager-detector/commands'
export * from 'package-manager-detector/constants'


================================================
FILE: src/monorepo.ts
================================================
import type { Choice } from '@posva/prompts'
import type { RunnerContext } from './runner'
import { existsSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import process from 'node:process'
import prompts from '@posva/prompts'
import { byLengthAsc, Fzf } from 'fzf'
import { globSync } from 'tinyglobby'
import { getPackageJSON } from './fs'

export const IGNORE_PATHS = [
  '**/node_modules/**',
  '**/dist/**',
  '**/public/**',
  '**/fixture/**',
  '**/fixtures/**',
]

export function findPackages(ctx?: RunnerContext) {
  const { cwd = process.cwd() } = ctx ?? {}
  const packagePath = resolve(cwd, 'package.json')
  if (!existsSync(packagePath))
    return []

  const pkgs = globSync('**/package.json', {
    ignore: IGNORE_PATHS,
    cwd,
    onlyFiles: true,
    dot: false,
    expandDirectories: false,
  })

  if (pkgs.length <= 1)
    return [packagePath]
  return pkgs
}

export async function promptSelectPackage(ctx?: RunnerContext, command?: string): Promise<RunnerContext | undefined> {
  const cwd = ctx?.cwd ?? process.cwd()
  const packagePaths = findPackages(ctx)
  if (packagePaths.length <= 1) {
    return ctx
  }

  const blank = ' '.repeat(process.stdout?.columns || 80)
  // Prompt the user to select a package
  let choices: (Choice & { scripts: Record<string, string> })[] = packagePaths.map((item) => {
    const filePath = resolve(cwd, item)
    const dir = dirname(filePath)
    const pkg = getPackageJSON({ ...ctx, cwd: dir, programmatic: true })

    return {
      title: pkg.name ?? item,
      value: dir,
      description: `${pkg.description ?? filePath}${blank}`,
      scripts: pkg.scripts,
    }
  })

  // Filter packages that have the command
  if (command) {
    choices = choices.filter(c => c.scripts?.[command])
  }
  if (!choices.length) {
    return ctx
  }
  if (choices.length === 1) {
    return { ...ctx, cwd: choices[0].value }
  }

  const fzf = new Fzf(choices, {
    selector: item => `${item.title} ${item.description}`,
    casing: 'case-insensitive',
    tiebreakers: [byLengthAsc],
  })

  let res: string
  try {
    const { pkg } = await prompts({
      name: 'pkg',
      message: 'select a package',
      type: 'autocomplete',
      choices,
      async suggest(input: string, choices: Choice[]) {
        if (!input)
          return choices
        const results = fzf.find(input)
        return results.map(r => choices.find(c => c.value === r.item.value))
      },
    })
    if (!pkg)
      throw new Error('No package selected')
    res = pkg
  }
  catch (error) {
    if (!ctx?.programmatic)
      process.exit(1)
    throw error
  }

  return { ...ctx, cwd: res }
}


================================================
FILE: src/package.ts
================================================
import type { RunnerContext } from '.'
import { getPackageJSON } from './fs'
import { promptSelectPackage } from './monorepo'

export interface PackageScript {
  key: string
  cmd: string
  description: string
}

export async function readWorkspaceScripts(ctx: RunnerContext | undefined, args: string[]): Promise<PackageScript[]> {
  const index = args.findIndex(i => i === '-p')
  let command: string = ''
  if (index !== -1) {
    command = args[index + 1]
  }

  const context = await promptSelectPackage(ctx, command)
  // Change cwd to the selected package
  if (ctx && context?.cwd) {
    ctx.cwd = context.cwd
  }
  const scripts = readPackageScripts(context)
  const cmdIndex = scripts.findIndex(i => i.key === command)
  if (command && cmdIndex !== -1) {
    return [scripts[cmdIndex]]
  }
  return scripts
}

export function readPackageScripts(ctx: RunnerContext | undefined): PackageScript[] {
  // support https://www.npmjs.com/package/npm-scripts-info conventions
  const pkg = getPackageJSON(ctx)
  const rawScripts = pkg.scripts || {}
  const scriptsInfo = pkg['scripts-info'] || {}

  const scripts = Object.entries(rawScripts)
    .filter(i => !i[0].startsWith('?'))
    .map(([key, cmd]) => ({
      key,
      cmd,
      description: scriptsInfo[key] || rawScripts[`?${key}`] || cmd,
    }))

  if (scripts.length === 0 && !ctx?.programmatic) {
    console.warn('No scripts found in package.json')
  }

  return scripts as PackageScript[]
}


================================================
FILE: src/parse.ts
================================================
import type { Agent, Command, ResolvedCommand } from 'package-manager-detector'
import type { ExtendedResolvedCommand, Runner } from './runner'
import process from 'node:process'
import { COMMANDS, constructCommand, getRunAgent } from '.'
import { exclude } from './utils'

export class UnsupportedCommand extends Error {
  constructor({ agent, command }: { agent: Agent, command: Command }) {
    super(`Command "${command}" is not support by agent "${agent}"`)
  }
}

export function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
): ExtendedResolvedCommand {
  if (!COMMANDS[agent])
    throw new Error(`Unsupported agent "${agent}"`)
  if (!COMMANDS[agent][command])
    throw new UnsupportedCommand({ agent, command })

  return constructCommand(COMMANDS[agent][command], args)!
}

export const parseNi = <Runner>((agent, args, ctx) => {
  // bun use `-d` instead of `-D`, #90
  if (agent === 'bun')
    args = args.map(i => i === '-D' ? '-d' : i)

  // npm use `--omit=dev` instead of `--production`
  if (agent === 'npm')
    args = args.map(i => i === '-P' ? '--omit=dev' : i)

  if (args.includes('-P'))
    args = args.map(i => i === '-P' ? '--production' : i)

  if (args.includes('-g'))
    return getCommand(agent, 'global', exclude(args, '-g'))

  if (args.includes('--frozen-if-present')) {
    args = exclude(args, '--frozen-if-present')
    return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
  }

  if (args.includes('--frozen'))
    return getCommand(agent, 'frozen', exclude(args, '--frozen'))

  if (args.length === 0 || args.every(i => i.startsWith('-')))
    return getCommand(agent, 'install', args)

  return getCommand(agent, 'add', args)
})

export const parseNr = <Runner>(async (agent, args, ctx) => {
  if (args.length === 0)
    args.push('start')

  const runAgent = await getRunAgent()

  let runWithNode = false
  if (runAgent === 'node') {
    const [majorNodeVersion] = process.versions.node.split('.').map(Number)
    if (majorNodeVersion < 22) {
      throw new Error('The runAgent "node" requires Node.js 22.0.0 or higher')
    }
    runWithNode = true
    args = ['--run', ...args]
  }

  let hasIfPresent = false
  if (args.includes('--if-present')) {
    args = exclude(args, '--if-present')
    hasIfPresent = true
  }

  if (args.includes('-p'))
    args = exclude(args, '-p')

  const cmd = runWithNode ? { command: 'node', args } : getCommand(agent, 'run', args)

  if (ctx?.cwd)
    cmd.cwd = ctx.cwd

  if (!cmd)
    return cmd

  if (hasIfPresent && !runWithNode)
    cmd.args.splice(1, 0, '--if-present')

  return cmd
})

export const parseNup = <Runner>((agent, args) => {
  if (args.includes('-i'))
    return getCommand(agent, 'upgrade-interactive', exclude(args, '-i'))

  return getCommand(agent, 'upgrade', args)
})

export const parseNd = <Runner>((agent, args) => {
  // https://yarnpkg.com/cli/dedupe#options
  // https://pnpm.io/cli/dedupe#--check
  if (agent === 'pnpm')
    args = args.map(i => i === '-c' ? '--check' : i)

  // https://docs.npmjs.com/cli/v11/commands/npm-dedupe#dry-run
  if (agent === 'npm')
    args = args.map(i => i === '-c' ? '--dry-run' : i)

  return getCommand(agent, 'dedupe', args)
})

export const parseNun = <Runner>((agent, args) => {
  if (args.includes('-g'))
    return getCommand(agent, 'global_uninstall', exclude(args, '-g'))
  return getCommand(agent, 'uninstall', args)
})

export const parseNlx = <Runner>((agent, args) => {
  return getCommand(agent, 'execute', args)
})

export const parseNa = <Runner>((agent, args) => {
  return getCommand(agent, 'agent', args)
})

export function serializeCommand(command?: ResolvedCommand) {
  if (!command)
    return undefined
  if (command.args.length === 0)
    return command.command
  return `${command.command} ${command.args.map(i => i.includes(' ') ? `"${i}"` : i).join(' ')}`
}


================================================
FILE: src/runner.ts
================================================
import type { Agent, ResolvedCommand } from 'package-manager-detector'
import type { Options as TinyExecOptions } from 'tinyexec'
import type { DetectOptions } from './detect'
/* eslint-disable no-console */
import { resolve } from 'node:path'
import process from 'node:process'
import { styleText } from 'node:util'
import prompts from '@posva/prompts'
import { AGENTS } from 'package-manager-detector'
import { x } from 'tinyexec'
import { version } from '../package.json'
import { getDefaultAgent, getGlobalAgent, getUseSfw } from './config'
import { detect } from './detect'
import { getEnvironmentOptions } from './environment'
import { getCommand, UnsupportedCommand } from './parse'
import { cmdExists, remove } from './utils'

const DEBUG_SIGN = '?'
const PROGRAMMATIC_SIGN = '--programmatic'

export interface RunnerContext {
  programmatic?: boolean
  hasLock?: boolean
  cwd?: string
}

export interface ExtendedResolvedCommand extends ResolvedCommand {
  cwd?: string
}

interface RunOptions {
  /**
   * Called before agent detection and command execution.
   *
   * Useful for performing concrete, agent-agnostic operations.
   */
  onBeforeCommand?: (args: string[], ctx: Pick<RunnerContext, 'cwd' | 'programmatic'> & {
    /**
     * Skips subsequent command execution.
     *
     * This is useful for operations such as generating shell-completion scripts.
     */
    exit: () => void
  }) => void | Promise<void>
}

export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<ExtendedResolvedCommand | undefined> | ExtendedResolvedCommand | undefined

export async function runCli(fn: Runner, options: DetectOptions & RunOptions & { args?: string[] } = {}) {
  options = {
    ...getEnvironmentOptions(),
    ...options,
  }
  const {
    args = process.argv.slice(2).filter(Boolean),
  } = options
  try {
    await run(fn, args, options)
  }
  catch (error) {
    if (error instanceof UnsupportedCommand && !options.programmatic)
      console.log(styleText('red', `\u2717 ${error.message}`))

    if (!options.programmatic)
      process.exit(1)

    throw error
  }
}

export async function getCliCommand(
  fn: Runner,
  args: string[],
  options: DetectOptions = {},
  cwd: string = options.cwd ?? process.cwd(),
) {
  const isGlobal = args.includes('-g')
  if (isGlobal)
    return await fn(await getGlobalAgent(), args)

  let agent = (await detect({ ...options, cwd })) || (await getDefaultAgent(options.programmatic))
  if (agent === 'prompt') {
    agent = (
      await prompts({
        name: 'agent',
        type: 'select',
        message: 'Choose the agent',
        choices: AGENTS.filter(i => !i.includes('@')).map(value => ({ title: value, value })),
      })
    ).agent
    if (!agent)
      return
  }

  return await fn(agent as Agent, args, {
    programmatic: options.programmatic,
    hasLock: Boolean(agent),
    cwd,
  })
}

export async function run(fn: Runner, args: string[], options: DetectOptions & RunOptions = {}) {
  const { detectVolta = true } = options

  const debug = args.includes(DEBUG_SIGN)
  if (debug)
    remove(args, DEBUG_SIGN)

  const programmaticFromArgs = args.includes(PROGRAMMATIC_SIGN)
  if (programmaticFromArgs)
    remove(args, PROGRAMMATIC_SIGN)

  const programmatic = options.programmatic || programmaticFromArgs

  let cwd = options.cwd ?? process.cwd()
  if (args[0] === '-C') {
    cwd = resolve(cwd, args[1])
    args.splice(0, 2)
  }

  if (args.length === 1 && (args[0]?.toLowerCase() === '-v' || args[0] === '--version')) {
    const getCmd = (a: Agent) => AGENTS.includes(a)
      ? getCommand(a, 'agent', ['-v'])
      : { command: a, args: ['-v'] }
    const xVersionOptions = {
      nodeOptions: {
        cwd,
      },
      throwOnError: true,
    } satisfies Partial<TinyExecOptions>
    const getV = (a: string) => {
      const { command, args } = getCmd(a as Agent)
      return x(command, args, xVersionOptions)
        .then(e => e.stdout)
        .then(e => e.startsWith('v') ? e : `v${e}`)
    }
    const globalAgentPromise = getGlobalAgent()
    const globalAgentVersionPromise = globalAgentPromise.then(getV)
    const agentPromise = detect({ ...options, cwd }).then(a => a || '')
    const agentVersionPromise = agentPromise.then(a => a && getV(a))
    const nodeVersionPromise = getV('node')

    console.log(`@antfu/ni  ${styleText('cyan', `v${version}`)}`)
    console.log(`node       ${styleText('green', await nodeVersionPromise)}`)
    const [agent, agentVersion] = await Promise.all([agentPromise, agentVersionPromise])
    if (agent)
      console.log(`${agent.padEnd(10)} ${styleText('blue', agentVersion)}`)
    else
      console.log('agent      no lock file')
    const [globalAgent, globalAgentVersion] = await Promise.all([globalAgentPromise, globalAgentVersionPromise])
    console.log(`${(`${globalAgent} -g`).padEnd(10)} ${styleText('blue', globalAgentVersion)}`)
    return
  }

  if (args.length === 1 && ['-h', '--help'].includes(args[0])) {
    const dash = styleText('dim', '-')
    console.log(`${styleText(['green', 'bold'], '@antfu/ni')} ${styleText('dim', `use the right package manager v${version}`)}\n`)
    console.log(`ni    ${dash}  install`)
    console.log(`nr    ${dash}  run`)
    console.log(`nlx   ${dash}  execute`)
    console.log(`nup   ${dash}  upgrade`)
    console.log(`nun   ${dash}  uninstall`)
    console.log(`nci   ${dash}  clean install`)
    console.log(`na    ${dash}  agent alias`)
    console.log(`nd    ${dash}  dedupe dependencies`)
    console.log(`ni -v ${dash}  show used agent`)
    console.log(`ni -i ${dash}  interactive package management`)
    console.log(styleText('yellow', '\ncheck https://github.com/antfu/ni for more documentation.'))
    return
  }

  if (options.onBeforeCommand) {
    let shouldExit = false
    await options.onBeforeCommand(args, {
      cwd,
      programmatic,
      exit: () => { shouldExit = true },
    })
    if (shouldExit)
      return
  }

  const command = await getCliCommand(fn, args, { ...options, programmatic }, cwd)

  if (!command)
    return

  const useSfw = await getUseSfw()
  if (useSfw && cmdExists('sfw')) {
    command.args = [command.command, ...command.args]
    command.command = 'sfw'
  }
  else if (useSfw) {
    if (programmatic)
      throw new Error('sfw is enabled but not installed')

    console.error('[ni] sfw is enabled but not installed.')
    console.error('[ni] Install it with: npm install -g sfw')
    process.exit(1)
  }

  if (detectVolta && cmdExists('volta')) {
    command.args = ['run', command.command, ...command.args]
    command.command = 'volta'
  }

  if (debug) {
    const commandStr = [command.command, ...command.args].join(' ')
    console.log(commandStr)
    return
  }

  const proc = x(
    command.command,
    command.args,
    {
      nodeOptions: {
        stdio: 'inherit',
        cwd: command.cwd ?? cwd,
      },
      throwOnError: true,
    },
  )

  process.once('SIGINT', async () => {
    // Ensure the proc finishes cleanup before exiting
    await proc
    process.exit(proc.exitCode)
  })

  await proc
}


================================================
FILE: src/storage.ts
================================================
import { existsSync, promises as fs } from 'node:fs'
import { resolve } from 'node:path'
import { CLI_TEMP_DIR, writeFileSafe } from './utils'

export interface Storage {
  lastRunCommand?: string
}

let storage: Storage | undefined

const storagePath = resolve(CLI_TEMP_DIR, '_storage.json')

export async function load(fn?: (storage: Storage) => Promise<boolean> | boolean) {
  if (!storage) {
    storage = existsSync(storagePath)
      ? (JSON.parse(await fs.readFile(storagePath, 'utf-8') || '{}') || {})
      : {}
  }

  if (fn) {
    if (await fn(storage!))
      await dump()
  }

  return storage!
}

export async function dump() {
  if (storage)
    await writeFileSafe(storagePath, JSON.stringify(storage))
}


================================================
FILE: src/utils.ts
================================================
import type { Buffer } from 'node:buffer'
import { existsSync, promises as fs } from 'node:fs'
import os from 'node:os'
import { dirname, join } from 'node:path'
import process from 'node:process'
import { styleText } from 'node:util'
import which from 'which'

export const CLI_TEMP_DIR = join(os.tmpdir(), 'antfu-ni')

export function remove<T>(arr: T[], v: T) {
  const index = arr.indexOf(v)
  if (index >= 0)
    arr.splice(index, 1)

  return arr
}

export function exclude<T>(arr: T[], ...v: T[]) {
  return arr.slice().filter(item => !v.includes(item))
}

export function cmdExists(cmd: string) {
  return which.sync(cmd, { nothrow: true }) !== null
}

interface TempFile {
  path: string
  fd: fs.FileHandle
  cleanup: () => void
}

let counter = 0

async function openTemp(): Promise<TempFile | undefined> {
  if (!existsSync(CLI_TEMP_DIR))
    await fs.mkdir(CLI_TEMP_DIR, { recursive: true })

  const competitivePath = join(CLI_TEMP_DIR, `.${process.pid}.${counter}`)
  counter += 1

  return fs.open(competitivePath, 'wx')
    .then(fd => ({
      fd,
      path: competitivePath,
      cleanup() {
        fd.close().then(() => {
          if (existsSync(competitivePath))
            fs.unlink(competitivePath)
        })
      },
    }))
    .catch((error: any) => {
      if (error && error.code === 'EEXIST')
        return openTemp()

      else
        return undefined
    })
}

/**
 * Write file safely avoiding conflicts
 */
export async function writeFileSafe(
  path: string,
  data: string | Buffer = '',
): Promise<boolean> {
  const temp = await openTemp()

  if (temp) {
    fs.writeFile(temp.path, data)
      .then(() => {
        const directory = dirname(path)
        if (!existsSync(directory))
          fs.mkdir(directory, { recursive: true })

        return fs.rename(temp.path, path)
          .then(() => true)
          .catch(() => false)
      })
      .catch(() => false)
      .finally(temp.cleanup)
  }

  return false
}

export function limitText(text: string, maxWidth: number) {
  if (text.length <= maxWidth)
    return text
  return `${text.slice(0, maxWidth)}${styleText('dim', '…')}`
}

export function terminalLink(text: string, url: string, options?: { fallback?: (text: string, url: string) => string }): string {
  // Use OSC 8 hyperlink escape sequence
  // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
  if (
    process.env.FORCE_HYPERLINK
    || (process.stdout.isTTY && !process.env.NO_HYPERLINK)
  ) {
    return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`
  }
  if (options?.fallback)
    return options.fallback(text, url)
  return `${text} (${url})`
}

export function formatPackageWithUrl(pkg: string, url?: string, limits = 80) {
  return url
    ? terminalLink(
        pkg,
        url,
        {
          fallback: (_, url) => (pkg.length + url.length > limits)
            ? pkg
            : `${pkg} ${styleText('dim', `- ${url}`)}`,
        },
      )
    : pkg
}


================================================
FILE: taze.config.ts
================================================
import { defineConfig } from 'taze'

export default defineConfig({
  ignorePaths: [
    'test/fixtures',
  ],
})


================================================
FILE: test/config/.nirc
================================================
defaultAgent=npm
globalAgent=pnpm
useSfw=true



================================================
FILE: test/config/config.test.ts
================================================
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { beforeEach, expect, it, vi } from 'vitest'

const __dirname = dirname(fileURLToPath(import.meta.url))

beforeEach(() => {
  vi.unstubAllEnvs()
  vi.resetModules()
})

vi.mock('../../src/detect', () => ({
  detect: vi.fn(),
}))

it('has correct defaults', async () => {
  const { getConfig } = await import('../../src/config')
  const config = await getConfig()

  expect(config).toEqual({
    defaultAgent: 'prompt',
    globalAgent: 'npm',
    runAgent: undefined,
    useSfw: false,
    catalog: true,
  })
})

it('loads .nirc', async () => {
  vi.stubEnv('NI_CONFIG_FILE', join(__dirname, './.nirc'))

  const { getConfig } = await import('../../src/config')
  const config = await getConfig()

  expect(config).toEqual({
    defaultAgent: 'npm',
    globalAgent: 'pnpm',
    runAgent: undefined,
    useSfw: true,
    catalog: true,
  })
})

it('reads environment variable config', async () => {
  vi.stubEnv('NI_DEFAULT_AGENT', 'npm')
  vi.stubEnv('NI_GLOBAL_AGENT', 'pnpm')
  vi.stubEnv('NI_USE_SFW', 'true')

  const { getConfig } = await import('../../src/config')
  const config = await getConfig()

  expect(config).toEqual({
    defaultAgent: 'npm',
    globalAgent: 'pnpm',
    runAgent: undefined,
    useSfw: true,
    catalog: true,
  })
})


================================================
FILE: test/fixtures/catalog/pnpm/package.json
================================================
{
  "name": "test-workspace",
  "private": true,
  "dependencies": {}
}


================================================
FILE: test/fixtures/catalog/pnpm/packages/app/package.json
================================================
{
  "name": "test-app",
  "private": true,
  "dependencies": {}
}


================================================
FILE: test/fixtures/catalog/pnpm/pnpm-workspace.yaml
================================================
packages:
  - packages/*

# Named catalogs
catalogs:
  prod:
    react: ^18.3.0
    express: ^4.21.0
  dev:
    typescript: ^5.6.0
    vitest: ^2.1.0


================================================
FILE: test/fixtures/catalog/pnpm-default-only/package.json
================================================
{
  "name": "test-workspace-default",
  "private": true,
  "dependencies": {}
}


================================================
FILE: test/fixtures/catalog/pnpm-default-only/pnpm-workspace.yaml
================================================
packages:
  - packages/*

catalog:
  react: ^18.3.0
  express: ^4.21.0


================================================
FILE: test/fixtures/lockfile/bun/bun.lockb
================================================


================================================
FILE: test/fixtures/lockfile/unknown/future-package-manager.json
================================================
{}


================================================
FILE: test/fixtures/packager/bun/package.json
================================================
{
  "packageManager": "bun@0"
}


================================================
FILE: test/fixtures/packager/deno/deno.json
================================================
{}


================================================
FILE: test/fixtures/packager/npm/package.json
================================================
{
  "packageManager": "npm@7"
}


================================================
FILE: test/fixtures/packager/pnpm/package.json
================================================
{
  "packageManager": "pnpm@8"
}


================================================
FILE: test/fixtures/packager/pnpm-version-range/package.json
================================================
{
  "packageManager": "^pnpm@8.0.0"
}


================================================
FILE: test/fixtures/packager/pnpm@6/package.json
================================================
{
  "packageManager": "pnpm@6"
}


================================================
FILE: test/fixtures/packager/unknown/package.json
================================================
{
  "packageManager": "future-package-manager"
}


================================================
FILE: test/fixtures/packager/yarn/package.json
================================================
{
  "packageManager": "yarn@1"
}


================================================
FILE: test/fixtures/packager/yarn@berry/package.json
================================================
{
  "packageManager": "yarn@3"
}


================================================
FILE: test/na/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'bun'))
it('foo', _('foo', 'bun foo'))
it('run test', _('run test', 'bun run test'))


================================================
FILE: test/na/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'deno'))
it('foo', _('foo', 'deno foo'))
it('run test', _('run test', 'deno run test'))
it('task dev', _('task dev', 'deno task dev'))
it('install', _('install', 'deno install'))
it('add package', _('add package', 'deno add package'))


================================================
FILE: test/na/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'npm'))
it('foo', _('foo', 'npm foo'))
it('run test', _('run test', 'npm run test'))


================================================
FILE: test/na/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'pnpm'))
it('foo', _('foo', 'pnpm foo'))
it('run test', _('run test', 'pnpm run test'))


================================================
FILE: test/na/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn'))
it('foo', _('foo', 'yarn foo'))
it('run test', _('run test', 'yarn run test'))


================================================
FILE: test/na/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNa, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn'))
it('foo', _('foo', 'yarn foo'))
it('run test', _('run test', 'yarn run test'))


================================================
FILE: test/ng.spec.ts
================================================
import { expect, it } from 'vitest'
import { getCommand } from '../src/commands'

it('wrong agent', () => {
  expect(() => {
    getCommand('idk' as any, 'install', [])
  }).toThrow('Unsupported agent "idk"')
})


================================================
FILE: test/ni/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'bun install'))

it('single add', _('axios', 'bun add axios'))

it('add dev', _('vite -D', 'bun add vite -d'))

it('multiple', _('eslint @types/node', 'bun add eslint @types/node'))

it('global', _('eslint -g', 'bun add -g eslint'))

it('frozen', _('--frozen', 'bun install --frozen-lockfile'))

it('production', _('-P', 'bun install --production'))

it('frozen production', _('--frozen -P', 'bun install --frozen-lockfile --production'))


================================================
FILE: test/ni/catalog.spec.ts
================================================
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { pnpmCatalogProvider } from '../../src/catalog/pnpm'
import { getCatalogRef } from '../../src/catalog/types'

const __dirname = dirname(fileURLToPath(import.meta.url))
const fixtureDir = join(__dirname, '..', 'fixtures', 'catalog', 'pnpm')
const defaultOnlyFixtureDir = join(__dirname, '..', 'fixtures', 'catalog', 'pnpm-default-only')

describe('getCatalogRef', () => {
  it('returns "catalog:" for default', () => {
    expect(getCatalogRef('default')).toBe('catalog:')
  })

  it('returns "catalog:name" for named', () => {
    expect(getCatalogRef('dev')).toBe('catalog:dev')
    expect(getCatalogRef('prod')).toBe('catalog:prod')
  })
})

describe('pnpmCatalogProvider.detect', () => {
  it('detects named catalogs', async () => {
    const config = await pnpmCatalogProvider.detect(fixtureDir)
    expect(config).not.toBeNull()
    expect(config!.hasDefaultCatalog).toBe(false)
    expect(config!.hasNamedCatalogs).toBe(true)
    expect(config!.catalogs).toHaveLength(2)
    expect(config!.catalogs.map(c => c.name)).toEqual(['prod', 'dev'])
  })

  it('detects default catalog only', async () => {
    const config = await pnpmCatalogProvider.detect(defaultOnlyFixtureDir)
    expect(config).not.toBeNull()
    expect(config!.hasDefaultCatalog).toBe(true)
    expect(config!.hasNamedCatalogs).toBe(false)
    expect(config!.catalogs).toHaveLength(1)
    expect(config!.catalogs[0].name).toBe('default')
  })

  it('detects from subdirectory', async () => {
    const subDir = join(fixtureDir, 'packages', 'app')
    const config = await pnpmCatalogProvider.detect(subDir)
    expect(config).not.toBeNull()
    expect(config!.catalogs).toHaveLength(2)
  })

  it('returns null when no workspace file', async () => {
    const config = await pnpmCatalogProvider.detect('/tmp')
    expect(config).toBeNull()
  })
})

describe('pnpmCatalogProvider.findPackage', () => {
  it('finds package in named catalog', async () => {
    const config = (await pnpmCatalogProvider.detect(fixtureDir))!
    const result = pnpmCatalogProvider.findPackage(config, 'react')
    expect(result).not.toBeUndefined()
    expect(result!.name).toBe('prod')
  })

  it('finds package in dev catalog', async () => {
    const config = (await pnpmCatalogProvider.detect(fixtureDir))!
    const result = pnpmCatalogProvider.findPackage(config, 'typescript')
    expect(result).not.toBeUndefined()
    expect(result!.name).toBe('dev')
  })

  it('returns undefined for unknown package', async () => {
    const config = (await pnpmCatalogProvider.detect(fixtureDir))!
    const result = pnpmCatalogProvider.findPackage(config, 'unknown-pkg')
    expect(result).toBeUndefined()
  })

  it('finds package in default catalog', async () => {
    const config = (await pnpmCatalogProvider.detect(defaultOnlyFixtureDir))!
    const result = pnpmCatalogProvider.findPackage(config, 'react')
    expect(result).not.toBeUndefined()
    expect(result!.name).toBe('default')
  })
})


================================================
FILE: test/ni/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'deno install'))

it('single add', _('axios', 'deno add axios'))

it('multiple', _('eslint @types/node', 'deno add eslint @types/node'))

it('-D', _('eslint @types/node -D', 'deno add eslint @types/node -D'))

it('global', _('eslint -g', 'deno install -g eslint'))

it('frozen', _('--frozen', 'deno install --frozen'))

it('production', _('-P', 'deno install --production'))

it('frozen production', _('--frozen -P', 'deno install --frozen --production'))


================================================
FILE: test/ni/interactive.spec.ts
================================================
import type { Agent } from 'package-manager-detector'
import type { RunnerContext } from '../../src'
import process from 'node:process'
import prompts from '@posva/prompts'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fetchNpmPackages } from '../../src/fetch'
import { parseNi } from '../../src/parse'
import { exclude } from '../../src/utils'

vi.mock('@posva/prompts')
vi.mock('../../src/fetch')

describe('interactive mode - Ctrl+C cancellation', () => {
  let originalExitCode: typeof process.exitCode

  beforeEach(() => {
    originalExitCode = process.exitCode
    process.exitCode = 0
    vi.clearAllMocks()
  })

  afterEach(() => {
    process.exitCode = originalExitCode
  })

  async function niRunner(agent: Agent, args: string[], ctx?: RunnerContext) {
    const isInteractive = args[0] === '-i'

    if (isInteractive) {
      let fetchPattern: string

      if (args[1] && !args[1].startsWith('-')) {
        fetchPattern = args[1]
      }
      else {
        const { pattern } = await prompts({
          type: 'text',
          name: 'pattern',
          message: 'search for package',
        })

        fetchPattern = pattern
      }

      if (!fetchPattern) {
        process.exitCode = 1
        return
      }

      const packages = await fetchNpmPackages(fetchPattern)

      if (!packages.length) {
        console.error('No results found')
        process.exitCode = 1
        return
      }

      const { dependency } = await prompts({
        type: 'autocomplete',
        name: 'dependency',
        choices: packages,
        instructions: false,
        message: 'choose a package to install',
        limit: 15,
      })

      if (!dependency) {
        process.exitCode = 1
        return
      }

      args = exclude(args, '-d', '-p', '-i')

      const canInstallPeers = ['npm', 'pnpm'].includes(agent)

      const { mode } = await prompts({
        type: 'select',
        name: 'mode',
        message: `install ${dependency.name} as`,
        choices: [
          {
            title: 'prod',
            value: '',
            selected: true,
          },
          {
            title: 'dev',
            value: '-D',
          },
          {
            title: `peer`,
            value: '--save-peer',
            disabled: !canInstallPeers,
          },
        ],
      })

      if (mode === undefined) {
        process.exitCode = 1
        return
      }

      args.push(dependency.name, mode)
    }

    return parseNi(agent, args, ctx)
  }

  it('should exit gracefully when user cancels package selection with Ctrl+C', async () => {
    vi.mocked(prompts)
      .mockResolvedValueOnce({ pattern: 'react' }) // First prompt: search pattern
      .mockResolvedValueOnce({ dependency: undefined }) // Second prompt: cancelled with Ctrl+C

    vi.mocked(fetchNpmPackages).mockResolvedValue([
      { title: 'react', value: 'react' },
      { title: 'react-dom', value: 'react-dom' },
    ])

    const result = await niRunner('npm', ['-i'])

    expect(process.exitCode).toBe(1)
    expect(result).toBeUndefined()
  })

  it('should exit gracefully when user cancels installation mode selection with Ctrl+C', async () => {
    vi.mocked(prompts)
      .mockResolvedValueOnce({ pattern: 'react' }) // First prompt: search pattern
      .mockResolvedValueOnce({ dependency: { name: 'react', value: 'react' } }) // Second prompt: select package
      .mockResolvedValueOnce({ mode: undefined }) // Third prompt: cancelled with Ctrl+C

    vi.mocked(fetchNpmPackages).mockResolvedValue([
      { title: 'react', value: 'react' },
    ])

    const result = await niRunner('npm', ['-i'])

    expect(process.exitCode).toBe(1)
    expect(result).toBeUndefined()
  })

  it('should exit gracefully when user cancels initial search pattern with Ctrl+C', async () => {
    vi.mocked(prompts)
      .mockResolvedValueOnce({ pattern: undefined }) // First prompt: cancelled with Ctrl+C

    const result = await niRunner('npm', ['-i'])

    expect(process.exitCode).toBe(1)
    expect(result).toBeUndefined()
  })
})


================================================
FILE: test/ni/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'npm i'))

it('single add', _('axios', 'npm i axios'))

it('multiple', _('eslint @types/node', 'npm i eslint @types/node'))

it('-D', _('eslint @types/node -D', 'npm i eslint @types/node -D'))

it('global', _('eslint -g', 'npm i -g eslint'))

it('frozen', _('--frozen', 'npm ci'))

it('production', _('-P', 'npm i --omit=dev'))

it('frozen production', _('--frozen -P', 'npm ci --omit=dev'))


================================================
FILE: test/ni/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'pnpm i'))

it('single add', _('axios', 'pnpm add axios'))

it('multiple', _('eslint @types/node', 'pnpm add eslint @types/node'))

it('-D', _('-D eslint @types/node', 'pnpm add -D eslint @types/node'))

it('global', _('eslint -g', 'pnpm add -g eslint'))

it('frozen', _('--frozen', 'pnpm i --frozen-lockfile'))

it('forward1', _('--anything', 'pnpm i --anything'))
it('forward2', _('-a', 'pnpm i -a'))

it('production', _('-P', 'pnpm i --production'))

it('frozen production', _('--frozen -P', 'pnpm i --frozen-lockfile --production'))


================================================
FILE: test/ni/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn install'))

it('single add', _('axios', 'yarn add axios'))

it('multiple', _('eslint @types/node', 'yarn add eslint @types/node'))

it('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D'))

it('global', _('eslint ni -g', 'yarn global add eslint ni'))

it('frozen', _('--frozen', 'yarn install --frozen-lockfile'))

it('production', _('-P', 'yarn install --production'))

it('frozen production', _('--frozen -P', 'yarn install --frozen-lockfile --production'))


================================================
FILE: test/ni/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNi, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn install'))

it('single add', _('axios', 'yarn add axios'))

it('multiple', _('eslint @types/node', 'yarn add eslint @types/node'))

it('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D'))

it('global', _('eslint ni -g', 'npm i -g eslint ni'))

it('frozen', _('--frozen', 'yarn install --immutable'))

it('production', _('-P', 'yarn install --production'))

it('frozen production', _('--frozen -P', 'yarn install --immutable --production'))


================================================
FILE: test/nlx/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'bun x esbuild'))
it('multiple', _('esbuild --version', 'bun x esbuild --version'))


================================================
FILE: test/nlx/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'deno run npm:esbuild'))
it('multiple', _('esbuild --version', 'deno run npm:esbuild --version'))
it('vitest', _('vitest', 'deno run npm:vitest'))
it('with args', _('typescript --version', 'deno run npm:typescript --version'))


================================================
FILE: test/nlx/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'npx esbuild'))
it('multiple', _('esbuild --version', 'npx esbuild --version'))


================================================
FILE: test/nlx/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'pnpm dlx esbuild'))
it('multiple', _('esbuild --version', 'pnpm dlx esbuild --version'))


================================================
FILE: test/nlx/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'npx esbuild'))
it('multiple', _('esbuild --version', 'npx esbuild --version'))


================================================
FILE: test/nlx/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNlx, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('esbuild', 'yarn dlx esbuild'))
it('multiple', _('esbuild --version', 'yarn dlx esbuild --version'))


================================================
FILE: test/nr/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'bun run start'))

it('script', _('dev', 'bun run dev'))

it('script with arguments', _('build --watch -o', 'bun run build --watch -o'))

it('colon', _('build:dev', 'bun run build:dev'))


================================================
FILE: test/nr/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'deno task start'))

it('if-present', _('test --if-present', 'deno task --if-present test'))

it('script', _('dev', 'deno task dev'))

it('script with arguments', _('build --watch -o', 'deno task build --watch -o'))

it('colon', _('build:dev', 'deno task build:dev'))


================================================
FILE: test/nr/nodeRunAgent.spec.ts
================================================
import type { ResolvedCommand } from 'package-manager-detector'
import { beforeEach, expect, it, vi } from 'vitest'
import { parseNr } from '../../src/commands'

const agent = 'npm'
const [majorNodeVersion] = process.versions.node.split('.').map(Number)
const supportsNodeRun = majorNodeVersion >= 22

function _(arg: string, expected: ResolvedCommand) {
  return async () => {
    expect(
      await parseNr(agent, arg.split(' ').filter(Boolean)),
    ).toEqual(
      expected,
    )
  }
}

function expectError(arg: string) {
  return async () => {
    await expect(
      parseNr(agent, arg.split(' ').filter(Boolean)),
    ).rejects.toThrow('requires Node.js 22.0.0 or higher')
  }
}

beforeEach(() => {
  vi.stubEnv('NI_RUN_AGENT', 'node')
})

it('empty', supportsNodeRun ? _('', { command: 'node', args: ['--run', 'start'] }) : expectError(''))

it('if-present', supportsNodeRun ? _('test --if-present', { command: 'node', args: ['--run', 'test'] }) : expectError('test --if-present'))

it('script', supportsNodeRun ? _('dev', { command: 'node', args: ['--run', 'dev'] }) : expectError('dev'))

it('script with arguments', supportsNodeRun ? _('build --watch -o', { command: 'node', args: ['--run', 'build', '--watch', '-o'] }) : expectError('build --watch -o'))

it('colon', supportsNodeRun ? _('build:dev', { command: 'node', args: ['--run', 'build:dev'] }) : expectError('build:dev'))


================================================
FILE: test/nr/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'npm run start'))

it('if-present', _('test --if-present', 'npm run --if-present test'))

it('script', _('dev', 'npm run dev'))

it('script with arguments', _('build --watch -o', 'npm run build -- --watch -o'))

it('colon', _('build:dev', 'npm run build:dev'))


================================================
FILE: test/nr/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'pnpm run start'))

it('if-present', _('test --if-present', 'pnpm run --if-present test'))

it('script', _('dev', 'pnpm run dev'))

it('script with arguments', _('build --watch -o', 'pnpm run build --watch -o'))

it('colon', _('build:dev', 'pnpm run build:dev'))


================================================
FILE: test/nr/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn run start'))

it('if-present', _('test --if-present', 'yarn run --if-present test'))

it('script', _('dev', 'yarn run dev'))

it('script with arguments', _('build --watch -o', 'yarn run build --watch -o'))

it('colon', _('build:dev', 'yarn run build:dev'))


================================================
FILE: test/nr/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNr, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNr(agent, arg.split(/\s/g).filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn run start'))

it('if-present', _('test --if-present', 'yarn run --if-present test'))

it('script', _('dev', 'yarn run dev'))

it('script with arguments', _('build --watch -o', 'yarn run build --watch -o'))

it('colon', _('build:dev', 'yarn run build:dev'))


================================================
FILE: test/nun/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('axios', 'bun remove axios'))

it('multiple', _('eslint @types/node', 'bun remove eslint @types/node'))

it('global', _('eslint ni -g', 'bun remove -g eslint ni'))


================================================
FILE: test/nun/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('webpack', 'deno remove webpack'))
it('multiple', _('webpack eslint', 'deno remove webpack eslint'))
it('global', _('webpack -g', 'deno uninstall -g webpack'))
it('forward', _('webpack --save-dev', 'deno remove webpack --save-dev'))


================================================
FILE: test/nun/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('axios', 'npm uninstall axios'))

it('multiple', _('eslint @types/node', 'npm uninstall eslint @types/node'))

it('-D', _('eslint @types/node -D', 'npm uninstall eslint @types/node -D'))

it('global', _('eslint -g', 'npm uninstall -g eslint'))


================================================
FILE: test/nun/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single add', _('axios', 'pnpm remove axios'))

it('multiple', _('eslint @types/node', 'pnpm remove eslint @types/node'))

it('-D', _('-D eslint @types/node', 'pnpm remove -D eslint @types/node'))

it('global', _('eslint -g', 'pnpm remove --global eslint'))


================================================
FILE: test/nun/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single uninstall', _('axios', 'yarn remove axios'))

it('multiple', _('eslint @types/node', 'yarn remove eslint @types/node'))

it('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D'))

it('global', _('eslint ni -g', 'yarn global remove eslint ni'))


================================================
FILE: test/nun/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNun, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('single add', _('axios', 'yarn remove axios'))

it('multiple', _('eslint @types/node', 'yarn remove eslint @types/node'))

it('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D'))

it('global', _('eslint ni -g', 'npm uninstall -g eslint ni'))


================================================
FILE: test/nup/bun.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'bun'
function _(arg: string, expected: string | null) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'bun update'))

it('interactive', _('-i', 'bun update -i'))

it('interactive latest', _('-i --latest', 'bun update -i --latest'))


================================================
FILE: test/nup/deno.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'deno'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'deno outdated --update'))

it('interactive', _('-i', 'deno outdated --update'))

it('interactive latest', _('-i --latest', 'deno outdated --update --latest'))


================================================
FILE: test/nup/npm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'npm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'npm update'))


================================================
FILE: test/nup/pnpm.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'pnpm'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'pnpm update'))

it('interactive', _('-i', 'pnpm update -i'))

it('interactive latest', _('-i --latest', 'pnpm update -i --latest'))


================================================
FILE: test/nup/yarn.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'yarn'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn upgrade'))

it('interactive', _('-i', 'yarn upgrade-interactive'))

it('interactive latest', _('-i --latest', 'yarn upgrade-interactive --latest'))


================================================
FILE: test/nup/yarn@berry.spec.ts
================================================
import { expect, it } from 'vitest'
import { parseNup, serializeCommand } from '../../src/commands'

const agent = 'yarn@berry'
function _(arg: string, expected: string) {
  return async () => {
    expect(
      serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))),
    ).toBe(
      expected,
    )
  }
}

it('empty', _('', 'yarn up'))

it('interactive', _('-i', 'yarn up -i'))


================================================
FILE: test/programmatic/__snapshots__/detect.spec.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`lockfile > bun 1`] = `"bun"`;

exports[`lockfile > deno 1`] = `"deno"`;

exports[`lockfile > npm 1`] = `"npm"`;

exports[`lockfile > pnpm 1`] = `"pnpm"`;

exports[`lockfile > pnpm@6 1`] = `"pnpm"`;

exports[`lockfile > unknown 1`] = `undefined`;

exports[`lockfile > yarn 1`] = `"yarn"`;

exports[`lockfile > yarn@berry 1`] = `"yarn"`;

exports[`packager > bun 1`] = `"bun"`;

exports[`packager > deno 1`] = `"deno"`;

exports[`packager > npm 1`] = `"npm"`;

exports[`packager > pnpm 1`] = `"pnpm"`;

exports[`packager > pnpm@6 1`] = `"pnpm@6"`;

exports[`packager > unknown 1`] = `undefined`;

exports[`packager > yarn 1`] = `"yarn"`;

exports[`packager > yarn@berry 1`] = `"yarn@berry"`;


================================================
FILE: test/programmatic/__snapshots__/runCli.spec.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`debug mode > should return command results in plain text format 1`] = `"npm i @antfu/ni"`;

exports[`lockfile > bun > na 1`] = `"bun"`;

exports[`lockfile > bun > na run foo 1`] = `"bun run foo"`;

exports[`lockfile > bun > ni --frozen 1`] = `"bun install --frozen-lockfile"`;

exports[`lockfile > bun > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > bun > ni 1`] = `"bun install"`;

exports[`lockfile > bun > ni foo -D 1`] = `"bun add foo -d"`;

exports[`lockfile > bun > ni foo 1`] = `"bun add foo"`;

exports[`lockfile > bun > nlx 1`] = `"bun x foo"`;

exports[`lockfile > bun > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > bun > nun foo 1`] = `"bun remove foo"`;

exports[`lockfile > bun > nup -i 1`] = `"bun update -i"`;

exports[`lockfile > bun > nup 1`] = `"bun update"`;

exports[`lockfile > deno > na 1`] = `"deno"`;

exports[`lockfile > deno > na run foo 1`] = `"deno run foo"`;

exports[`lockfile > deno > ni --frozen 1`] = `"deno install --frozen"`;

exports[`lockfile > deno > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > deno > ni 1`] = `"deno install"`;

exports[`lockfile > deno > ni foo -D 1`] = `"deno add foo -D"`;

exports[`lockfile > deno > ni foo 1`] = `"deno add foo"`;

exports[`lockfile > deno > nlx 1`] = `"deno run npm:foo"`;

exports[`lockfile > deno > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > deno > nun foo 1`] = `"deno remove foo"`;

exports[`lockfile > deno > nup -i 1`] = `"deno outdated --update"`;

exports[`lockfile > deno > nup 1`] = `"deno outdated --update"`;

exports[`lockfile > npm > na 1`] = `"npm"`;

exports[`lockfile > npm > na run foo 1`] = `"npm run foo"`;

exports[`lockfile > npm > ni --frozen 1`] = `"npm ci"`;

exports[`lockfile > npm > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > npm > ni 1`] = `"npm i"`;

exports[`lockfile > npm > ni foo -D 1`] = `"npm i foo -D"`;

exports[`lockfile > npm > ni foo 1`] = `"npm i foo"`;

exports[`lockfile > npm > nlx 1`] = `"npx foo"`;

exports[`lockfile > npm > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > npm > nun foo 1`] = `"npm uninstall foo"`;

exports[`lockfile > npm > nup -i 1`] = `"Command "upgrade-interactive" is not support by agent "npm""`;

exports[`lockfile > npm > nup 1`] = `"npm update"`;

exports[`lockfile > pnpm > na 1`] = `"pnpm"`;

exports[`lockfile > pnpm > na run foo 1`] = `"pnpm run foo"`;

exports[`lockfile > pnpm > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`lockfile > pnpm > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > pnpm > ni 1`] = `"pnpm i"`;

exports[`lockfile > pnpm > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`lockfile > pnpm > ni foo 1`] = `"pnpm add foo"`;

exports[`lockfile > pnpm > nlx 1`] = `"pnpm dlx foo"`;

exports[`lockfile > pnpm > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > pnpm > nun foo 1`] = `"pnpm remove foo"`;

exports[`lockfile > pnpm > nup -i 1`] = `"pnpm update -i"`;

exports[`lockfile > pnpm > nup 1`] = `"pnpm update"`;

exports[`lockfile > pnpm@6 > na 1`] = `"pnpm"`;

exports[`lockfile > pnpm@6 > na run foo 1`] = `"pnpm run foo"`;

exports[`lockfile > pnpm@6 > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`lockfile > pnpm@6 > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > pnpm@6 > ni 1`] = `"pnpm i"`;

exports[`lockfile > pnpm@6 > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`lockfile > pnpm@6 > ni foo 1`] = `"pnpm add foo"`;

exports[`lockfile > pnpm@6 > nlx 1`] = `"pnpm dlx foo"`;

exports[`lockfile > pnpm@6 > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > pnpm@6 > nun foo 1`] = `"pnpm remove foo"`;

exports[`lockfile > pnpm@6 > nup -i 1`] = `"pnpm update -i"`;

exports[`lockfile > pnpm@6 > nup 1`] = `"pnpm update"`;

exports[`lockfile > unknown > na 1`] = `"pnpm"`;

exports[`lockfile > unknown > na run foo 1`] = `"pnpm run foo"`;

exports[`lockfile > unknown > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`lockfile > unknown > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > unknown > ni 1`] = `"pnpm i"`;

exports[`lockfile > unknown > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`lockfile > unknown > ni foo 1`] = `"pnpm add foo"`;

exports[`lockfile > unknown > nlx 1`] = `"pnpm dlx foo"`;

exports[`lockfile > unknown > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > unknown > nun foo 1`] = `"pnpm remove foo"`;

exports[`lockfile > unknown > nup -i 1`] = `"pnpm update -i"`;

exports[`lockfile > unknown > nup 1`] = `"pnpm update"`;

exports[`lockfile > yarn > na 1`] = `"yarn"`;

exports[`lockfile > yarn > na run foo 1`] = `"yarn run foo"`;

exports[`lockfile > yarn > ni --frozen 1`] = `"yarn install --frozen-lockfile"`;

exports[`lockfile > yarn > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > yarn > ni 1`] = `"yarn install"`;

exports[`lockfile > yarn > ni foo -D 1`] = `"yarn add foo -D"`;

exports[`lockfile > yarn > ni foo 1`] = `"yarn add foo"`;

exports[`lockfile > yarn > nlx 1`] = `"npx foo"`;

exports[`lockfile > yarn > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > yarn > nun foo 1`] = `"yarn remove foo"`;

exports[`lockfile > yarn > nup -i 1`] = `"yarn upgrade-interactive"`;

exports[`lockfile > yarn > nup 1`] = `"yarn upgrade"`;

exports[`lockfile > yarn@berry > na 1`] = `"yarn"`;

exports[`lockfile > yarn@berry > na run foo 1`] = `"yarn run foo"`;

exports[`lockfile > yarn@berry > ni --frozen 1`] = `"yarn install --frozen-lockfile"`;

exports[`lockfile > yarn@berry > ni -g foo 1`] = `"npm i -g foo"`;

exports[`lockfile > yarn@berry > ni 1`] = `"yarn install"`;

exports[`lockfile > yarn@berry > ni foo -D 1`] = `"yarn add foo -D"`;

exports[`lockfile > yarn@berry > ni foo 1`] = `"yarn add foo"`;

exports[`lockfile > yarn@berry > nlx 1`] = `"npx foo"`;

exports[`lockfile > yarn@berry > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`lockfile > yarn@berry > nun foo 1`] = `"yarn remove foo"`;

exports[`lockfile > yarn@berry > nup -i 1`] = `"yarn upgrade-interactive"`;

exports[`lockfile > yarn@berry > nup 1`] = `"yarn upgrade"`;

exports[`packager > bun > na 1`] = `"bun"`;

exports[`packager > bun > na run foo 1`] = `"bun run foo"`;

exports[`packager > bun > ni --frozen 1`] = `"bun install --frozen-lockfile"`;

exports[`packager > bun > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > bun > ni 1`] = `"bun install"`;

exports[`packager > bun > ni foo -D 1`] = `"bun add foo -d"`;

exports[`packager > bun > ni foo 1`] = `"bun add foo"`;

exports[`packager > bun > nlx 1`] = `"bun x foo"`;

exports[`packager > bun > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > bun > nun foo 1`] = `"bun remove foo"`;

exports[`packager > bun > nup -i 1`] = `"bun update -i"`;

exports[`packager > bun > nup 1`] = `"bun update"`;

exports[`packager > deno > na 1`] = `"deno"`;

exports[`packager > deno > na run foo 1`] = `"deno run foo"`;

exports[`packager > deno > ni --frozen 1`] = `"deno install --frozen"`;

exports[`packager > deno > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > deno > ni 1`] = `"deno install"`;

exports[`packager > deno > ni foo -D 1`] = `"deno add foo -D"`;

exports[`packager > deno > ni foo 1`] = `"deno add foo"`;

exports[`packager > deno > nlx 1`] = `"deno run npm:foo"`;

exports[`packager > deno > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > deno > nun foo 1`] = `"deno remove foo"`;

exports[`packager > deno > nup -i 1`] = `"deno outdated --update"`;

exports[`packager > deno > nup 1`] = `"deno outdated --update"`;

exports[`packager > npm > na 1`] = `"npm"`;

exports[`packager > npm > na run foo 1`] = `"npm run foo"`;

exports[`packager > npm > ni --frozen 1`] = `"npm ci"`;

exports[`packager > npm > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > npm > ni 1`] = `"npm i"`;

exports[`packager > npm > ni foo -D 1`] = `"npm i foo -D"`;

exports[`packager > npm > ni foo 1`] = `"npm i foo"`;

exports[`packager > npm > nlx 1`] = `"npx foo"`;

exports[`packager > npm > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > npm > nun foo 1`] = `"npm uninstall foo"`;

exports[`packager > npm > nup -i 1`] = `"Command "upgrade-interactive" is not support by agent "npm""`;

exports[`packager > npm > nup 1`] = `"npm update"`;

exports[`packager > pnpm > na 1`] = `"pnpm"`;

exports[`packager > pnpm > na run foo 1`] = `"pnpm run foo"`;

exports[`packager > pnpm > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`packager > pnpm > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > pnpm > ni 1`] = `"pnpm i"`;

exports[`packager > pnpm > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`packager > pnpm > ni foo 1`] = `"pnpm add foo"`;

exports[`packager > pnpm > nlx 1`] = `"pnpm dlx foo"`;

exports[`packager > pnpm > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > pnpm > nun foo 1`] = `"pnpm remove foo"`;

exports[`packager > pnpm > nup -i 1`] = `"pnpm update -i"`;

exports[`packager > pnpm > nup 1`] = `"pnpm update"`;

exports[`packager > pnpm@6 > na 1`] = `"pnpm"`;

exports[`packager > pnpm@6 > na run foo 1`] = `"pnpm run foo"`;

exports[`packager > pnpm@6 > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`packager > pnpm@6 > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > pnpm@6 > ni 1`] = `"pnpm i"`;

exports[`packager > pnpm@6 > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`packager > pnpm@6 > ni foo 1`] = `"pnpm add foo"`;

exports[`packager > pnpm@6 > nlx 1`] = `"pnpm dlx foo"`;

exports[`packager > pnpm@6 > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > pnpm@6 > nun foo 1`] = `"pnpm remove foo"`;

exports[`packager > pnpm@6 > nup -i 1`] = `"pnpm update -i"`;

exports[`packager > pnpm@6 > nup 1`] = `"pnpm update"`;

exports[`packager > unknown > na 1`] = `"pnpm"`;

exports[`packager > unknown > na run foo 1`] = `"pnpm run foo"`;

exports[`packager > unknown > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`;

exports[`packager > unknown > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > unknown > ni 1`] = `"pnpm i"`;

exports[`packager > unknown > ni foo -D 1`] = `"pnpm add foo -D"`;

exports[`packager > unknown > ni foo 1`] = `"pnpm add foo"`;

exports[`packager > unknown > nlx 1`] = `"pnpm dlx foo"`;

exports[`packager > unknown > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > unknown > nun foo 1`] = `"pnpm remove foo"`;

exports[`packager > unknown > nup -i 1`] = `"pnpm update -i"`;

exports[`packager > unknown > nup 1`] = `"pnpm update"`;

exports[`packager > yarn > na 1`] = `"yarn"`;

exports[`packager > yarn > na run foo 1`] = `"yarn run foo"`;

exports[`packager > yarn > ni --frozen 1`] = `"yarn install --frozen-lockfile"`;

exports[`packager > yarn > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > yarn > ni 1`] = `"yarn install"`;

exports[`packager > yarn > ni foo -D 1`] = `"yarn add foo -D"`;

exports[`packager > yarn > ni foo 1`] = `"yarn add foo"`;

exports[`packager > yarn > nlx 1`] = `"npx foo"`;

exports[`packager > yarn > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > yarn > nun foo 1`] = `"yarn remove foo"`;

exports[`packager > yarn > nup -i 1`] = `"yarn upgrade-interactive"`;

exports[`packager > yarn > nup 1`] = `"yarn upgrade"`;

exports[`packager > yarn@berry > na 1`] = `"yarn"`;

exports[`packager > yarn@berry > na run foo 1`] = `"yarn run foo"`;

exports[`packager > yarn@berry > ni --frozen 1`] = `"yarn install --immutable"`;

exports[`packager > yarn@berry > ni -g foo 1`] = `"npm i -g foo"`;

exports[`packager > yarn@berry > ni 1`] = `"yarn install"`;

exports[`packager > yarn@berry > ni foo -D 1`] = `"yarn add foo -D"`;

exports[`packager > yarn@berry > ni foo 1`] = `"yarn add foo"`;

exports[`packager > yarn@berry > nlx 1`] = `"yarn dlx foo"`;

exports[`packager > yarn@berry > nun -g foo 1`] = `"npm uninstall -g foo"`;

exports[`packager > yarn@berry > nun foo 1`] = `"yarn remove foo"`;

exports[`packager > yarn@berry > nup -i 1`] = `"yarn up -i"`;

exports[`packager > yarn@berry > nup 1`] = `"yarn up"`;


================================================
FILE: test/programmatic/catalog.spec.ts
================================================
import fs from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

vi.mock('../../src/detect', () => ({
  detect: vi.fn(() => 'pnpm'),
}))

vi.mock('../../src/config', async (importOriginal) => {
  const original = await importOriginal<typeof import('../../src/config')>()
  return {
    ...original,
    getConfig: vi.fn(async () => ({
      defaultAgent: 'pnpm',
      globalAgent: 'npm',
      runAgent: undefined,
      useSfw: false,
      catalog: true,
    })),
    getDefaultAgent: vi.fn(async () => 'pnpm'),
    getGlobalAgent: vi.fn(async () => 'npm'),
    getRunAgent: vi.fn(async () => undefined),
    getUseSfw: vi.fn(async () => false),
    getCatalog: vi.fn(async () => true),
  }
})

vi.mock('fast-npm-meta', () => ({
  getLatestVersion: vi.fn(async (name: string) => ({
    name,
    version: '1.0.0',
  })),
}))

vi.mock('@posva/prompts', () => ({
  default: vi.fn(async () => ({})),
}))

async function createTempDir(fixture: string): Promise<string> {
  const tmp = await fs.promises.mkdtemp(path.join(tmpdir(), 'ni-catalog-'))
  const fixtureDir = path.join(__dirname, '..', 'fixtures', 'catalog', fixture)
  await fs.promises.cp(fixtureDir, tmp, { recursive: true })
  return tmp
}

function readJson(filePath: string) {
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
}

beforeEach(() => {
  vi.restoreAllMocks()
})

describe('catalog handler - named catalogs', () => {
  it('package found in catalog → updates package.json, returns pnpm install', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })

    expect(result).toBeDefined()
    expect(result!.command).toBe('pnpm')
    expect(result!.args).toEqual(['i'])

    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.dependencies.react).toBe('catalog:prod')
  })

  it('multiple packages in different catalogs', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react', 'typescript'], { cwd, programmatic: true })

    expect(result).toBeDefined()
    expect(result!.command).toBe('pnpm')
    expect(result!.args).toEqual(['i'])

    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.dependencies.react).toBe('catalog:prod')
    expect(pkg.dependencies.typescript).toBe('catalog:dev')
  })

  it('-D flag → writes to devDependencies', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react', '-D'], { cwd, programmatic: true })

    expect(result).toBeDefined()
    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.devDependencies.react).toBe('catalog:prod')
    expect(pkg.dependencies?.react).toBeUndefined()
  })

  it('unknown package in programmatic mode → skips catalog', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['unknown-pkg'], { cwd, programmatic: true })

    // In programmatic mode, unknown packages are skipped → falls through
    expect(result).toBeUndefined()
  })

  it('mixed known/unknown packages in programmatic mode', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react', 'unknown-pkg'], { cwd, programmatic: true })

    // react is cataloged, unknown-pkg is skipped → add command for skipped ones
    expect(result).toBeDefined()
    expect(result!.command).toBe('pnpm')
    expect(result!.args).toContain('unknown-pkg')

    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.dependencies.react).toBe('catalog:prod')
  })
})

describe('catalog handler - default catalog only', () => {
  it('package found → uses catalog: ref (no name)', async () => {
    const cwd = await createTempDir('pnpm-default-only')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })

    expect(result).toBeDefined()
    expect(result!.args).toEqual(['i'])

    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.dependencies.react).toBe('catalog:')
  })

  it('new package → adds to default catalog without prompt', async () => {
    const cwd = await createTempDir('pnpm-default-only')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['lodash'], { cwd, programmatic: true })

    expect(result).toBeDefined()
    expect(result!.args).toEqual(['i'])

    // Check workspace yaml was updated
    const yamlContent = fs.readFileSync(path.join(cwd, 'pnpm-workspace.yaml'), 'utf-8')
    expect(yamlContent).toContain('lodash')

    // Check package.json uses catalog:
    const pkg = readJson(path.join(cwd, 'package.json'))
    expect(pkg.dependencies.lodash).toBe('catalog:')
  })
})

describe('catalog handler - skip conditions', () => {
  it('returns undefined for non-pnpm agent', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('npm', ['react'], { cwd, programmatic: true })
    expect(result).toBeUndefined()
  })

  it('returns undefined when no packages in args (bare install)', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', [], { cwd, programmatic: true })
    expect(result).toBeUndefined()
  })

  it('returns undefined when only flags', async () => {
    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['--frozen'], { cwd, programmatic: true })
    expect(result).toBeUndefined()
  })

  it('returns undefined when catalog config disabled', async () => {
    const { getCatalog } = await import('../../src/config')
    vi.mocked(getCatalog).mockResolvedValueOnce(false)

    const cwd = await createTempDir('pnpm')
    const { handleCatalogInstall } = await import('../../src/catalog/handler')

    const result = await handleCatalogInstall('pnpm', ['react'], { cwd, programmatic: true })
    expect(result).toBeUndefined()
  })
})

describe('catalog handler - subdirectory', () => {
  it('finds closest package.json from subdirectory', async () => {
    const cwd = await createTempDir('pnpm')
    const subDir = path.join(cwd, 'packages', 'app')

    const { handleCatalogInstall } = await import('../../src/catalog/handler')
    const result = await handleCatalogInstall('pnpm', ['react'], { cwd: subDir, programmatic: true })

    expect(result).toBeDefined()

    // Should write to the subdirectory's package.json (closest)
    const pkg = readJson(path.join(subDir, 'package.json'))
    expect(pkg.dependencies.react).toBe('catalog:prod')
  })

  it('-w flag targets workspace root package.json', async () => {
    const cwd = await createTempDir('pnpm')
    const subDir = path.join(cwd, 'packages', 'app')

    const { handleCatalogInstall } = await import('../../src/catalog/handler')
    const result = await handleCatalogInstall('pnpm', ['react', '-w'], { cwd: subDir, programmatic: true })

    expect(result).toBeDefined()

    // Should write to root package.json, not subdirectory
    const rootPkg = readJson(path.join(cwd, 'package.json'))
    expect(rootPkg.dependencies.react).toBe('catalog:prod')

    // Subdirectory package.json should be unchanged
    const subPkg = readJson(path.join(subDir, 'package.json'))
    expect(subPkg.dependencies.react).toBeUndefined()
  })
})


================================================
FILE: test/programmatic/detect.spec.ts
================================================
import type { MockInstance } from 'vitest'
import fs from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { AGENTS, detect } from '../../src'

let basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance

function detectTest(fixture: string, agent: string) {
  return async () => {
    const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))
    const dir = path.join(__dirname, '..', 'fixtures', fixture, agent)
    await fs.cp(dir, cwd, { recursive: true })

    expect(await detect({ programmatic: true, cwd })).toMatchSnapshot()
  }
}

beforeAll(() => {
  basicLog = vi.spyOn(console, 'log')
  warnLog = vi.spyOn(console, 'warn')
  errorLog = vi.spyOn(console, 'error')
  infoLog = vi.spyOn(console, 'info')
})

afterAll(() => {
  vi.resetAllMocks()
})

const agents = [...AGENTS, 'unknown']
const fixtures = ['lockfile', 'packager']
const skippedAgents: string[] = []

// matrix testing of: fixtures x agents
fixtures.forEach(fixture => describe(fixture, () => agents.forEach((agent) => {
  if (skippedAgents.includes(agent))
    return it.skip(`skipped for ${agent}`, () => {})

  it(agent, detectTest(fixture, agent))

  it('no logs', () => {
    expect(basicLog).not.toHaveBeenCalled()
    expect(warnLog).not.toHaveBeenCalled()
    expect(errorLog).not.toHaveBeenCalled()
    expect(infoLog).not.toHaveBeenCalled()
  })
})))


================================================
FILE: test/programmatic/runCli.spec.ts
================================================
import type { MockInstance } from 'vitest'
import type { Runner } from '../../src'
import fs from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'

import { AGENTS, parseNa, parseNi, parseNlx, parseNun, parseNup, runCli } from '../../src'

let basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance

function runCliTest(fixtureName: string, agent: string, runner: Runner, args: string[]) {
  return async () => {
    const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-'))
    const fixture = path.join(__dirname, '..', 'fixtures', fixtureName, agent)
    await fs.cp(fixture, cwd, { recursive: true })

    await runCli(
      async (agent, _, ctx) => {
        // we override the args to be test specific
        return runner(agent, args, ctx)
      },
      {
        programmatic: true,
        cwd,
        args,
      },
    ).catch((e) => {
      // it will always throw if ezspawn is mocked
      if (e.command)
        expect(e.command).toMatchSnapshot()
      else
        expect(e.message).toMatchSnapshot()
    })
  }
}

beforeAll(() => {
  basicLog = vi.spyOn(console, 'log')
  warnLog = vi.spyOn(console, 'warn')
  errorLog = vi.spyOn(console, 'error')
  infoLog = vi.spyOn(console, 'info')

  vi.mock('tinyexec', async (importOriginal) => {
    const mod = await importOriginal() as any
    return {
      ...mod,
      x: (cmd: string, args?: string[]) => {
        // break execution flow for easier snapshotting
        // eslint-disable-next-line no-throw-literal
        throw { command: [cmd, ...(args ?? [])].join(' ') }
      },
    }
  })
})

afterAll(() => {
  vi.resetAllMocks()
})

const agents = [...AGENTS, 'unknown']
const fixtures = ['lockfile', 'packager']
const skippedAgents: string[] = []

// matrix testing of: fixtures x agents x commands
fixtures.forEach(fixture => describe(fixture, () => agents.forEach(agent => describe(agent, () => {
  if (skippedAgents.includes(agent))
    return it.skip(`skipped for ${agent}`, () => {})

  /** na */
  it('na', runCliTest(fixture, agent, parseNa, []))
  it('na run foo', runCliTest(fixture, agent, parseNa, ['run', 'foo']))

  /** ni */
  it('ni', runCliTest(fixture, agent, parseNi, []))
  it('ni foo', runCliTest(fixture, agent, parseNi, ['foo']))
  it('ni foo -D', runCliTest(fixture, agent, parseNi, ['foo', '-D']))
  it('ni --frozen', runCliTest(fixture, agent, parseNi, ['--frozen']))
  it('ni -g foo', runCliTest(fixture, agent, parseNi, ['-g', 'foo']))

  /** nlx */
  it('nlx', runCliTest(fixture, agent, parseNlx, ['foo']))

  /** nup */
  it('nup', runCliTest(fixture, agent, parseNup, []))
  it('nup -i', runCliTest(fixture, agent, parseNup, ['-i']))

  /** nun */
  it('nun foo', runCliTest(fixture, agent, parseNun, ['foo']))
  it('nun -g foo', runCliTest(fixture, agent, parseNun, ['-g', 'foo']))

  it('no logs', () => {
    expect(basicLog).not.toHaveBeenCalled()
    expect(warnLog).not.toHaveBeenCalled()
    expect(errorLog).not.toHaveBeenCalled()
    expect(infoLog).not.toHaveBeenCalled()
  })
}))))

// https://github.com/antfu-collective/ni/issues/266
describe('debug mode', () => {
  beforeAll(() => basicLog.mockClear())

  it('ni', runCliTest('lockfile', 'npm', parseNi, ['@antfu/ni', '?']))
  it('should return command results in plain text format', () => {
    expect(basicLog).toHaveBeenCalled()

    expect(basicLog.mock.calls[0][0]).toMatchSnapshot()
  })
})


================================================
FILE: test/runner/runCli.test.ts
================================================
import type { Runner } from '../../src'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { runCli } from '../../src'

// Mock detect to see what options are passed to it
const mocks = vi.hoisted(() => ({
  detectSpy: vi.fn(() => Promise.resolve('npm')),
  baseRunFnSpy: vi.fn<Runner>(() => Promise.resolve(undefined)),
}))
vi.mock('../../src/detect', () => ({
  detect: mocks.detectSpy,
}))

describe('runCli', () => {
  afterEach(() => {
    vi.clearAllMocks()
    vi.unstubAllEnvs()
  })

  it('run without errors', async () => {
    const result = await runCli(mocks.baseRunFnSpy, {})
    expect(result).toBe(undefined)
  })

  it('handle errors in programmatic mode', async () => {
    await expect(
      runCli(() => {
        throw new Error('test error')
      }, { programmatic: true }),
    ).rejects.toThrow('test error')
  })

  it('calls detect with the correct options', async () => {
    await runCli(mocks.baseRunFnSpy)
    expect(mocks.detectSpy).toHaveBeenCalledWith(({ autoInstall: false, programmatic: false, cwd: expect.any(String) }))
  })

  it('detects environment options', async () => {
    vi.stubEnv('NI_AUTO_INSTALL', 'true')
    await runCli(mocks.baseRunFnSpy)
    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: false, cwd: expect.any(String) })
  })

  it('accepts options as input', async () => {
    await runCli(mocks.baseRunFnSpy, { autoInstall: true, programmatic: true })
    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: true, cwd: expect.any(String) })
  })

  it('merges inputs and environment prioritizing inputs', async () => {
    vi.stubEnv('NI_AUTO_INSTALL', 'true')
    await runCli(mocks.baseRunFnSpy, { autoInstall: false, programmatic: true })
    expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, programmatic: true, cwd: expect.any(String) })
  })

  it('parses --programmatic flag from args', async () => {
    await runCli(mocks.baseRunFnSpy, { args: ['--programmatic'] })
    expect(mocks.detectSpy).toHaveBeenCalledWith(expect.objectContaining({ autoInstall: false, programmatic: true, cwd: expect.any(String) }))
  })

  it('removes --programmatic from args before passing to runner', async () => {
    await runCli(mocks.baseRunFnSpy, { args: ['--programmatic', 'foo'] })
    expect(mocks.baseRunFnSpy).toHaveBeenCalledWith('npm', ['foo'], { programmatic: true, hasLock: true, cwd: expect.any(String) })
  })

  describe('onBeforeCommand', () => {
    it('skips running the command when exit() is called', async () => {
      await runCli(mocks.baseRunFnSpy, { onBeforeCommand: (_args, ctx) => ctx.exit() })
      expect(mocks.baseRunFnSpy).not.toHaveBeenCalled()
      // https://github.com/antfu-collective/ni/issues/308
      expect(mocks.detectSpy).not.toHaveBeenCalled()
    })

    it('continues to run the command when exit() is not called', async () => {
      await runCli(mocks.baseRunFnSpy, { onBeforeCommand: () => Promise.resolve() })
      expect(mocks.baseRunFnSpy).toHaveBeenCalledOnce()
    })
  })
})


================================================
FILE: test/sfw/sfw.spec.ts
================================================
import { afterEach, describe, expect, it, vi } from 'vitest'
import { parseNi, runCli } from '../../src'

const mocks = vi.hoisted(() => ({
  cmdExistsSpy: vi.fn(),
  execSpy: vi.fn(),
}))

vi.mock('../../src/utils', async (importOriginal) => {
  const mod = await importOriginal() as any
  return {
    ...mod,
    cmdExists: mocks.cmdExistsSpy,
  }
})

vi.mock('tinyexec', () => ({
  x: mocks.execSpy,
}))

describe('sfw', () => {
  afterEach(() => {
    vi.clearAllMocks()
    vi.unstubAllEnvs()
    vi.resetModules()
  })

  it('wraps command with sfw when enabled and installed', async () => {
    vi.stubEnv('NI_USE_SFW', 'true')
    mocks.cmdExistsSpy.mockImplementation(() => true)
    mocks.execSpy.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })

    await runCli(parseNi, {
      programmatic: true,
      args: ['axios'],
      detectVolta: false,
    })

    expect(mocks.execSpy).toHaveBeenCalledWith(
      'sfw',
      ['pnpm', 'add', 'axios'],
      expect.objectContaining({
        nodeOptions: expect.objectContaining({
          stdio: 'inherit',
        }),
      }),
    )
  })

  it('throws error when sfw is not installed', async () => {
    vi.stubEnv('NI_USE_SFW', 'true')
    mocks.cmdExistsSpy.mockImplementation(() => false)

    await expect(
      runCli(parseNi, {
        programmatic: true,
        args: ['axios'],
        detectVolta: false,
      }),
    ).rejects.toThrow(/sfw is enabled but not installed/)

    expect(mocks.execSpy).not.toHaveBeenCalled()
  })

  it('wraps command with sfw and volta', async () => {
    vi.stubEnv('NI_USE_SFW', 'true')
    mocks.cmdExistsSpy.mockImplementation(() => true)
    mocks.execSpy.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 })

    await runCli(parseNi, {
      programmatic: true,
      args: ['axios'],
      detectVolta: true,
    })

    expect(mocks.execSpy).toHaveBeenCalledWith(
      'volta',
      ['run', 'sfw', 'pnpm', 'add', 'axios'],
      expect.objectContaining({
        nodeOptions: expect.objectContaining({
          stdio: 'inherit',
        }),
      }),
    )
  })
})


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["esnext"],
    "module": "esnext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "strict": true,
    "strictNullChecks": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}


================================================
FILE: tsdown.config.ts
================================================
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/commands/*.ts'],
  clean: true,
  dts: true,
  exports: true,
  deps: {
    onlyBundle: [
      'which',
      'ini',
      '@posva/prompts',
      'pnpm-workspace-yaml',
      'yaml',
      'fast-npm-meta',
      'isexe',
      'kleur',
      'sisteransi',
    ],
  },
})


================================================
FILE: vitest.config.ts
================================================
import process from 'node:process'
import { defineConfig } from 'vitest/config'

// Disable global ni config in test to make the results more predictable
process.env.NI_CONFIG_FILE = 'false'

export default defineConfig({
  test: {

  },
})
Download .txt
gitextract_i7fr5ot5/

├── .github/
│   └── workflows/
│       ├── publish-commit.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── bin/
│   ├── na.mjs
│   ├── nci.mjs
│   ├── nd.mjs
│   ├── ni.mjs
│   ├── nlx.mjs
│   ├── nr.mjs
│   ├── nun.mjs
│   └── nup.mjs
├── eslint.config.js
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── catalog/
│   │   ├── detect.ts
│   │   ├── handler.ts
│   │   ├── package-json.ts
│   │   ├── pnpm.ts
│   │   ├── prompt.ts
│   │   └── types.ts
│   ├── commands/
│   │   ├── index.ts
│   │   ├── na.ts
│   │   ├── nci.ts
│   │   ├── nd.ts
│   │   ├── ni.ts
│   │   ├── nlx.ts
│   │   ├── nr.ts
│   │   ├── nun.ts
│   │   └── nup.ts
│   ├── completion.ts
│   ├── config.ts
│   ├── detect.ts
│   ├── environment.ts
│   ├── fetch.ts
│   ├── fs.ts
│   ├── index.ts
│   ├── monorepo.ts
│   ├── package.ts
│   ├── parse.ts
│   ├── runner.ts
│   ├── storage.ts
│   └── utils.ts
├── taze.config.ts
├── test/
│   ├── config/
│   │   ├── .nirc
│   │   └── config.test.ts
│   ├── fixtures/
│   │   ├── catalog/
│   │   │   ├── pnpm/
│   │   │   │   ├── package.json
│   │   │   │   ├── packages/
│   │   │   │   │   └── app/
│   │   │   │   │       └── package.json
│   │   │   │   └── pnpm-workspace.yaml
│   │   │   └── pnpm-default-only/
│   │   │       ├── package.json
│   │   │       └── pnpm-workspace.yaml
│   │   ├── lockfile/
│   │   │   ├── bun/
│   │   │   │   └── bun.lockb
│   │   │   └── unknown/
│   │   │       └── future-package-manager.json
│   │   └── packager/
│   │       ├── bun/
│   │       │   └── package.json
│   │       ├── deno/
│   │       │   └── deno.json
│   │       ├── npm/
│   │       │   └── package.json
│   │       ├── pnpm/
│   │       │   └── package.json
│   │       ├── pnpm-version-range/
│   │       │   └── package.json
│   │       ├── pnpm@6/
│   │       │   └── package.json
│   │       ├── unknown/
│   │       │   └── package.json
│   │       ├── yarn/
│   │       │   └── package.json
│   │       └── yarn@berry/
│   │           └── package.json
│   ├── na/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── ng.spec.ts
│   ├── ni/
│   │   ├── bun.spec.ts
│   │   ├── catalog.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── interactive.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nlx/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nr/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── nodeRunAgent.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nun/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── nup/
│   │   ├── bun.spec.ts
│   │   ├── deno.spec.ts
│   │   ├── npm.spec.ts
│   │   ├── pnpm.spec.ts
│   │   ├── yarn.spec.ts
│   │   └── yarn@berry.spec.ts
│   ├── programmatic/
│   │   ├── __snapshots__/
│   │   │   ├── detect.spec.ts.snap
│   │   │   └── runCli.spec.ts.snap
│   │   ├── catalog.spec.ts
│   │   ├── detect.spec.ts
│   │   └── runCli.spec.ts
│   ├── runner/
│   │   └── runCli.test.ts
│   └── sfw/
│       └── sfw.spec.ts
├── tsconfig.json
├── tsdown.config.ts
└── vitest.config.ts
Download .txt
SYMBOL INDEX (117 symbols across 61 files)

FILE: src/catalog/detect.ts
  function getCatalogProvider (line 5) | function getCatalogProvider(agent: Agent): CatalogProvider | null {

FILE: src/catalog/handler.ts
  function splitPackagesAndFlags (line 16) | function splitPackagesAndFlags(args: string[]): { packages: string[], fl...
  function getDepType (line 28) | function getDepType(flags: string[]): DepType {
  function resolveVersion (line 36) | async function resolveVersion(pkgName: string): Promise<string> {
  function resolveCatalogForPackage (line 41) | async function resolveCatalogForPackage(
  function handleCatalogInstall (line 63) | async function handleCatalogInstall(

FILE: src/catalog/package-json.ts
  function findClosestPackageJson (line 4) | function findClosestPackageJson(cwd: string): string | null {
  function detectIndent (line 17) | function detectIndent(content: string): string {
  type DepType (line 22) | type DepType = 'dependencies' | 'devDependencies' | 'peerDependencies'
  function updatePackageJsonCatalogRefs (line 24) | function updatePackageJsonCatalogRefs(

FILE: src/catalog/pnpm.ts
  function findPnpmWorkspaceYaml (line 6) | function findPnpmWorkspaceYaml(cwd: string): string | null {
  method detect (line 20) | async detect(cwd: string): Promise<CatalogConfig | null> {
  method findPackage (line 57) | findPackage(config: CatalogConfig, pkgName: string): CatalogInfo | undef...
  method addPackage (line 61) | async addPackage(config: CatalogConfig, catalogName: string, pkgName: st...

FILE: src/catalog/prompt.ts
  constant SKIP (line 5) | const SKIP = '__skip__'
  constant CREATE_NEW (line 6) | const CREATE_NEW = '__create_new__'
  type CatalogSelection (line 8) | interface CatalogSelection {
  function promptSelectCatalog (line 12) | async function promptSelectCatalog(
  function promptNewCatalogName (line 54) | async function promptNewCatalogName(): Promise<string | undefined> {

FILE: src/catalog/types.ts
  type CatalogInfo (line 1) | interface CatalogInfo {
  type CatalogConfig (line 6) | interface CatalogConfig {
  type CatalogProvider (line 13) | interface CatalogProvider {
  function getCatalogRef (line 19) | function getCatalogRef(catalogName: string): string {

FILE: src/commands/ni.ts
  method suggest (line 56) | async suggest(input: string, choices: Choice[]) {

FILE: src/commands/nr.ts
  method suggest (line 48) | async suggest(input: string, choices: Choice[]) {
  method onState (line 54) | onState(state) {

FILE: src/commands/nun.ts
  method suggest (line 50) | async suggest(input: string, choices: Choice[]) {

FILE: src/completion.ts
  function getCompletionSuggestions (line 49) | function getCompletionSuggestions(args: string[], ctx: RunnerContext | u...

FILE: src/config.ts
  type Config (line 18) | interface Config {
  function getConfig (line 36) | async function getConfig(): Promise<Config> {
  function getDefaultAgent (line 65) | async function getDefaultAgent(programmatic?: boolean) {
  function getGlobalAgent (line 72) | async function getGlobalAgent() {
  function getRunAgent (line 77) | async function getRunAgent() {
  function getUseSfw (line 82) | async function getUseSfw() {
  function getCatalog (line 87) | async function getCatalog() {

FILE: src/detect.ts
  type DetectOptions (line 10) | interface DetectOptions {
  function detect (line 23) | async function detect({ autoInstall, programmatic, cwd }: DetectOptions ...

FILE: src/environment.ts
  type EnvironmentOptions (line 3) | interface EnvironmentOptions {
  constant DEFAULT_ENVIRONMENT_OPTIONS (line 7) | const DEFAULT_ENVIRONMENT_OPTIONS: EnvironmentOptions = {
  function getEnvironmentOptions (line 11) | function getEnvironmentOptions(): EnvironmentOptions {

FILE: src/fetch.ts
  type NpmPackage (line 6) | interface NpmPackage {
  type NpmRegistryResponse (line 19) | interface NpmRegistryResponse {
  function fetchNpmPackages (line 23) | async function fetchNpmPackages(pattern: string): Promise<Choice[]> {

FILE: src/fs.ts
  function getPackageJSON (line 6) | function getPackageJSON(ctx?: RunnerContext): any {

FILE: src/monorepo.ts
  constant IGNORE_PATHS (line 11) | const IGNORE_PATHS = [
  function findPackages (line 19) | function findPackages(ctx?: RunnerContext) {
  function promptSelectPackage (line 38) | async function promptSelectPackage(ctx?: RunnerContext, command?: string...

FILE: src/package.ts
  type PackageScript (line 5) | interface PackageScript {
  function readWorkspaceScripts (line 11) | async function readWorkspaceScripts(ctx: RunnerContext | undefined, args...
  function readPackageScripts (line 31) | function readPackageScripts(ctx: RunnerContext | undefined): PackageScri...

FILE: src/parse.ts
  class UnsupportedCommand (line 7) | class UnsupportedCommand extends Error {
    method constructor (line 8) | constructor({ agent, command }: { agent: Agent, command: Command }) {
  function getCommand (line 13) | function getCommand(
  function serializeCommand (line 128) | function serializeCommand(command?: ResolvedCommand) {

FILE: src/runner.ts
  constant DEBUG_SIGN (line 18) | const DEBUG_SIGN = '?'
  constant PROGRAMMATIC_SIGN (line 19) | const PROGRAMMATIC_SIGN = '--programmatic'
  type RunnerContext (line 21) | interface RunnerContext {
  type ExtendedResolvedCommand (line 27) | interface ExtendedResolvedCommand extends ResolvedCommand {
  type RunOptions (line 31) | interface RunOptions {
  type Runner (line 47) | type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Pro...
  function runCli (line 49) | async function runCli(fn: Runner, options: DetectOptions & RunOptions & ...
  function getCliCommand (line 71) | async function getCliCommand(
  function run (line 102) | async function run(fn: Runner, args: string[], options: DetectOptions & ...

FILE: src/storage.ts
  type Storage (line 5) | interface Storage {
  function load (line 13) | async function load(fn?: (storage: Storage) => Promise<boolean> | boolea...
  function dump (line 28) | async function dump() {

FILE: src/utils.ts
  constant CLI_TEMP_DIR (line 9) | const CLI_TEMP_DIR = join(os.tmpdir(), 'antfu-ni')
  function remove (line 11) | function remove<T>(arr: T[], v: T) {
  function exclude (line 19) | function exclude<T>(arr: T[], ...v: T[]) {
  function cmdExists (line 23) | function cmdExists(cmd: string) {
  type TempFile (line 27) | interface TempFile {
  function openTemp (line 35) | async function openTemp(): Promise<TempFile | undefined> {
  function writeFileSafe (line 65) | async function writeFileSafe(
  function limitText (line 89) | function limitText(text: string, maxWidth: number) {
  function terminalLink (line 95) | function terminalLink(text: string, url: string, options?: { fallback?: ...
  function formatPackageWithUrl (line 109) | function formatPackageWithUrl(pkg: string, url?: string, limits = 80) {

FILE: test/na/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/na/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/na/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/na/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/na/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/na/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/interactive.spec.ts
  function niRunner (line 26) | async function niRunner(agent: Agent, args: string[], ctx?: RunnerContex...

FILE: test/ni/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/ni/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nlx/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/nodeRunAgent.spec.ts
  function _ (line 9) | function _(arg: string, expected: ResolvedCommand) {
  function expectError (line 19) | function expectError(arg: string) {

FILE: test/nr/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nr/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nun/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nup/bun.spec.ts
  function _ (line 5) | function _(arg: string, expected: string | null) {

FILE: test/nup/deno.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nup/npm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nup/pnpm.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nup/yarn.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/nup/yarn@berry.spec.ts
  function _ (line 5) | function _(arg: string, expected: string) {

FILE: test/programmatic/detect.spec.ts
  function detectTest (line 10) | function detectTest(fixture: string, agent: string) {

FILE: test/programmatic/runCli.spec.ts
  function runCliTest (line 12) | function runCliTest(fixtureName: string, agent: string, runner: Runner, ...
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (139K chars).
[
  {
    "path": ".github/workflows/publish-commit.yml",
    "chars": 445,
    "preview": "name: Publish Any Commit\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n    tags:\n      - '!**'\n\njobs:\n  build:"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 217,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    uses: sxzz/workflows/.github/workflows/release.y"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 661,
    "preview": "name: Test\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-"
  },
  {
    "path": ".gitignore",
    "chars": 348,
    "preview": "_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.n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1090,
    "preview": "{\n  // Enable the ESlint flat config support\n  \"eslint.experimental.useFlatConfig\": true,\n\n  // Disable the default form"
  },
  {
    "path": "LICENSE",
    "chars": 1094,
    "preview": "MIT License\n\nCopyright (c) 2020 Anthony Fu <https://github.com/antfu>\n\nPermission is hereby granted, free of charge, to "
  },
  {
    "path": "README.md",
    "chars": 9259,
    "preview": "# 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 "
  },
  {
    "path": "bin/na.mjs",
    "chars": 57,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/na.mjs'\n"
  },
  {
    "path": "bin/nci.mjs",
    "chars": 58,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nci.mjs'\n"
  },
  {
    "path": "bin/nd.mjs",
    "chars": 57,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nd.mjs'\n"
  },
  {
    "path": "bin/ni.mjs",
    "chars": 57,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/ni.mjs'\n"
  },
  {
    "path": "bin/nlx.mjs",
    "chars": 58,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nlx.mjs'\n"
  },
  {
    "path": "bin/nr.mjs",
    "chars": 57,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nr.mjs'\n"
  },
  {
    "path": "bin/nun.mjs",
    "chars": 58,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nun.mjs'\n"
  },
  {
    "path": "bin/nup.mjs",
    "chars": 58,
    "preview": "#!/usr/bin/env node\n'use strict'\nimport '../dist/nup.mjs'\n"
  },
  {
    "path": "eslint.config.js",
    "chars": 181,
    "preview": "// @ts-check\nimport antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  pnpm: true,\n})\n  .removeRules(\n    'mark"
  },
  {
    "path": "package.json",
    "chars": 2621,
    "preview": "{\n  \"name\": \"@antfu/ni\",\n  \"type\": \"module\",\n  \"version\": \"29.0.0\",\n  \"packageManager\": \"pnpm@10.32.1\",\n  \"description\":"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 741,
    "preview": "shellEmulator: true\n\ntrustPolicy: no-downgrade\ntrustPolicyIgnoreAfter: 604800 # 7 days\n\npackages: []\ncatalogs:\n  dev:\n  "
  },
  {
    "path": "src/catalog/detect.ts",
    "chars": 293,
    "preview": "import type { Agent } from 'package-manager-detector'\nimport type { CatalogProvider } from './types'\nimport { pnpmCatalo"
  },
  {
    "path": "src/catalog/handler.ts",
    "chars": 4861,
    "preview": "import type { Agent } from 'package-manager-detector'\nimport type { ExtendedResolvedCommand, RunnerContext } from '../ru"
  },
  {
    "path": "src/catalog/package-json.ts",
    "chars": 1073,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\n\nexport function findClosestPackageJson(cwd: string): string | nul"
  },
  {
    "path": "src/catalog/pnpm.ts",
    "chars": 2318,
    "preview": "import type { CatalogConfig, CatalogInfo, CatalogProvider } from './types'\nimport fs from 'node:fs'\nimport path from 'no"
  },
  {
    "path": "src/catalog/prompt.ts",
    "chars": 1528,
    "preview": "import type { CatalogConfig } from './types'\nimport { styleText } from 'node:util'\nimport prompts from '@posva/prompts'\n"
  },
  {
    "path": "src/catalog/types.ts",
    "chars": 648,
    "preview": "export interface CatalogInfo {\n  name: string\n  packages: Record<string, string>\n}\n\nexport interface CatalogConfig {\n  f"
  },
  {
    "path": "src/commands/index.ts",
    "chars": 25,
    "preview": "export * from '../index'\n"
  },
  {
    "path": "src/commands/na.ts",
    "chars": 87,
    "preview": "import { parseNa } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNa)\n"
  },
  {
    "path": "src/commands/nci.ts",
    "chars": 193,
    "preview": "import { parseNi } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(\n  (agent, args, hasLock) => parseNi(agent"
  },
  {
    "path": "src/commands/nd.ts",
    "chars": 87,
    "preview": "import { parseNd } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNd)\n"
  },
  {
    "path": "src/commands/ni.ts",
    "chars": 2667,
    "preview": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nim"
  },
  {
    "path": "src/commands/nlx.ts",
    "chars": 89,
    "preview": "import { parseNlx } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNlx)\n"
  },
  {
    "path": "src/commands/nr.ts",
    "chars": 4417,
    "preview": "import type { Choice } from '@posva/prompts'\nimport type { PackageScript } from '../package'\nimport process from 'node:p"
  },
  {
    "path": "src/commands/nun.ts",
    "chars": 1748,
    "preview": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport prompts from '@posva/prompts'\nimp"
  },
  {
    "path": "src/commands/nup.ts",
    "chars": 89,
    "preview": "import { parseNup } from '../parse'\nimport { runCli } from '../runner'\n\nrunCli(parseNup)\n"
  },
  {
    "path": "src/completion.ts",
    "chars": 1384,
    "preview": "import type { RunnerContext } from '.'\nimport { byLengthAsc, Fzf } from 'fzf'\nimport { readPackageScripts } from './pack"
  },
  {
    "path": "src/config.ts",
    "chars": 2212,
    "preview": "import type { Agent } from 'package-manager-detector'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport proce"
  },
  {
    "path": "src/detect.ts",
    "chars": 1981,
    "preview": "import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport prompts from"
  },
  {
    "path": "src/environment.ts",
    "chars": 355,
    "preview": "import process from 'node:process'\n\nexport interface EnvironmentOptions {\n  autoInstall: boolean\n}\n\nconst DEFAULT_ENVIRO"
  },
  {
    "path": "src/fetch.ts",
    "chars": 1172,
    "preview": "import type { Choice } from '@posva/prompts'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nim"
  },
  {
    "path": "src/fs.ts",
    "chars": 594,
    "preview": "import type { RunnerContext } from './runner'\nimport fs from 'node:fs'\nimport { resolve } from 'node:path'\nimport proces"
  },
  {
    "path": "src/index.ts",
    "chars": 225,
    "preview": "export * from './config'\nexport * from './detect'\n\nexport * from './parse'\nexport * from './runner'\nexport * from './uti"
  },
  {
    "path": "src/monorepo.ts",
    "chars": 2660,
    "preview": "import type { Choice } from '@posva/prompts'\nimport type { RunnerContext } from './runner'\nimport { existsSync } from 'n"
  },
  {
    "path": "src/package.ts",
    "chars": 1460,
    "preview": "import type { RunnerContext } from '.'\nimport { getPackageJSON } from './fs'\nimport { promptSelectPackage } from './mono"
  },
  {
    "path": "src/parse.ts",
    "chars": 3871,
    "preview": "import type { Agent, Command, ResolvedCommand } from 'package-manager-detector'\nimport type { ExtendedResolvedCommand, R"
  },
  {
    "path": "src/runner.ts",
    "chars": 7107,
    "preview": "import type { Agent, ResolvedCommand } from 'package-manager-detector'\nimport type { Options as TinyExecOptions } from '"
  },
  {
    "path": "src/storage.ts",
    "chars": 721,
    "preview": "import { existsSync, promises as fs } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { CLI_TEMP_DIR, writeFil"
  },
  {
    "path": "src/utils.ts",
    "chars": 2971,
    "preview": "import type { Buffer } from 'node:buffer'\nimport { existsSync, promises as fs } from 'node:fs'\nimport os from 'node:os'\n"
  },
  {
    "path": "taze.config.ts",
    "chars": 113,
    "preview": "import { defineConfig } from 'taze'\n\nexport default defineConfig({\n  ignorePaths: [\n    'test/fixtures',\n  ],\n})\n"
  },
  {
    "path": "test/config/.nirc",
    "chars": 47,
    "preview": "defaultAgent=npm\nglobalAgent=pnpm\nuseSfw=true\n\n"
  },
  {
    "path": "test/config/config.test.ts",
    "chars": 1348,
    "preview": "import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { beforeEach, expect, it, vi }"
  },
  {
    "path": "test/fixtures/catalog/pnpm/package.json",
    "chars": 72,
    "preview": "{\n  \"name\": \"test-workspace\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm/packages/app/package.json",
    "chars": 66,
    "preview": "{\n  \"name\": \"test-app\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm/pnpm-workspace.yaml",
    "chars": 150,
    "preview": "packages:\n  - packages/*\n\n# Named catalogs\ncatalogs:\n  prod:\n    react: ^18.3.0\n    express: ^4.21.0\n  dev:\n    typescri"
  },
  {
    "path": "test/fixtures/catalog/pnpm-default-only/package.json",
    "chars": 80,
    "preview": "{\n  \"name\": \"test-workspace-default\",\n  \"private\": true,\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/pnpm-default-only/pnpm-workspace.yaml",
    "chars": 71,
    "preview": "packages:\n  - packages/*\n\ncatalog:\n  react: ^18.3.0\n  express: ^4.21.0\n"
  },
  {
    "path": "test/fixtures/lockfile/bun/bun.lockb",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/fixtures/lockfile/unknown/future-package-manager.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "test/fixtures/packager/bun/package.json",
    "chars": 32,
    "preview": "{\n  \"packageManager\": \"bun@0\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/deno/deno.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "test/fixtures/packager/npm/package.json",
    "chars": 32,
    "preview": "{\n  \"packageManager\": \"npm@7\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm/package.json",
    "chars": 33,
    "preview": "{\n  \"packageManager\": \"pnpm@8\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm-version-range/package.json",
    "chars": 38,
    "preview": "{\n  \"packageManager\": \"^pnpm@8.0.0\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/pnpm@6/package.json",
    "chars": 33,
    "preview": "{\n  \"packageManager\": \"pnpm@6\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/unknown/package.json",
    "chars": 49,
    "preview": "{\n  \"packageManager\": \"future-package-manager\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/yarn/package.json",
    "chars": 33,
    "preview": "{\n  \"packageManager\": \"yarn@1\"\n}\n"
  },
  {
    "path": "test/fixtures/packager/yarn@berry/package.json",
    "chars": 33,
    "preview": "{\n  \"packageManager\": \"yarn@3\"\n}\n"
  },
  {
    "path": "test/na/bun.spec.ts",
    "chars": 421,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\n"
  },
  {
    "path": "test/na/deno.spec.ts",
    "chars": 572,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'"
  },
  {
    "path": "test/na/npm.spec.ts",
    "chars": 421,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\n"
  },
  {
    "path": "test/na/pnpm.spec.ts",
    "chars": 425,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'"
  },
  {
    "path": "test/na/yarn.spec.ts",
    "chars": 425,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'"
  },
  {
    "path": "test/na/yarn@berry.spec.ts",
    "chars": 431,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNa, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@"
  },
  {
    "path": "test/ng.spec.ts",
    "chars": 212,
    "preview": "import { expect, it } from 'vitest'\nimport { getCommand } from '../src/commands'\n\nit('wrong agent', () => {\n  expect(() "
  },
  {
    "path": "test/ni/bun.spec.ts",
    "chars": 775,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\n"
  },
  {
    "path": "test/ni/catalog.spec.ts",
    "chars": 3085,
    "preview": "import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { describe, expect, it } from "
  },
  {
    "path": "test/ni/deno.spec.ts",
    "chars": 793,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'"
  },
  {
    "path": "test/ni/interactive.spec.ts",
    "chars": 4098,
    "preview": "import type { Agent } from 'package-manager-detector'\nimport type { RunnerContext } from '../../src'\nimport process from"
  },
  {
    "path": "test/ni/npm.spec.ts",
    "chars": 728,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\n"
  },
  {
    "path": "test/ni/pnpm.spec.ts",
    "chars": 874,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'"
  },
  {
    "path": "test/ni/yarn.spec.ts",
    "chars": 817,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'"
  },
  {
    "path": "test/ni/yarn@berry.spec.ts",
    "chars": 804,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNi, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@"
  },
  {
    "path": "test/nlx/bun.spec.ts",
    "chars": 440,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'"
  },
  {
    "path": "test/nlx/deno.spec.ts",
    "chars": 584,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno"
  },
  {
    "path": "test/nlx/npm.spec.ts",
    "chars": 436,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'"
  },
  {
    "path": "test/nlx/pnpm.spec.ts",
    "chars": 447,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm"
  },
  {
    "path": "test/nlx/yarn.spec.ts",
    "chars": 437,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/nlx/yarn@berry.spec.ts",
    "chars": 453,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNlx, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/nr/bun.spec.ts",
    "chars": 523,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'\n"
  },
  {
    "path": "test/nr/deno.spec.ts",
    "chars": 605,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno'"
  },
  {
    "path": "test/nr/nodeRunAgent.spec.ts",
    "chars": 1395,
    "preview": "import type { ResolvedCommand } from 'package-manager-detector'\nimport { beforeEach, expect, it, vi } from 'vitest'\nimpo"
  },
  {
    "path": "test/nr/npm.spec.ts",
    "chars": 597,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'\n"
  },
  {
    "path": "test/nr/pnpm.spec.ts",
    "chars": 600,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm'"
  },
  {
    "path": "test/nr/yarn.spec.ts",
    "chars": 600,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn'"
  },
  {
    "path": "test/nr/yarn@berry.spec.ts",
    "chars": 608,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNr, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn@"
  },
  {
    "path": "test/nun/bun.spec.ts",
    "chars": 509,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'"
  },
  {
    "path": "test/nun/deno.spec.ts",
    "chars": 579,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno"
  },
  {
    "path": "test/nun/npm.spec.ts",
    "chars": 589,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'"
  },
  {
    "path": "test/nun/pnpm.spec.ts",
    "chars": 582,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm"
  },
  {
    "path": "test/nun/yarn.spec.ts",
    "chars": 592,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/nun/yarn@berry.spec.ts",
    "chars": 590,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNun, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/nup/bun.spec.ts",
    "chars": 475,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'bun'"
  },
  {
    "path": "test/nup/deno.spec.ts",
    "chars": 499,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'deno"
  },
  {
    "path": "test/nup/npm.spec.ts",
    "chars": 353,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'npm'"
  },
  {
    "path": "test/nup/pnpm.spec.ts",
    "chars": 472,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'pnpm"
  },
  {
    "path": "test/nup/yarn.spec.ts",
    "chars": 493,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/nup/yarn@berry.spec.ts",
    "chars": 399,
    "preview": "import { expect, it } from 'vitest'\nimport { parseNup, serializeCommand } from '../../src/commands'\n\nconst agent = 'yarn"
  },
  {
    "path": "test/programmatic/__snapshots__/detect.spec.ts.snap",
    "chars": 762,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`lockfile > bun 1`] = `\"bun\"`;\n\nexports[`lockfile"
  },
  {
    "path": "test/programmatic/__snapshots__/runCli.spec.ts.snap",
    "chars": 12143,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`debug mode > should return command results in pl"
  },
  {
    "path": "test/programmatic/catalog.spec.ts",
    "chars": 8354,
    "preview": "import fs from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { fileURLToPath } from 'no"
  },
  {
    "path": "test/programmatic/detect.spec.ts",
    "chars": 1482,
    "preview": "import type { MockInstance } from 'vitest'\nimport fs from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport pat"
  },
  {
    "path": "test/programmatic/runCli.spec.ts",
    "chars": 3513,
    "preview": "import type { MockInstance } from 'vitest'\nimport type { Runner } from '../../src'\nimport fs from 'node:fs/promises'\nimp"
  },
  {
    "path": "test/runner/runCli.test.ts",
    "chars": 3080,
    "preview": "import type { Runner } from '../../src'\nimport { afterEach, describe, expect, it, vi } from 'vitest'\nimport { runCli } f"
  },
  {
    "path": "test/sfw/sfw.spec.ts",
    "chars": 2100,
    "preview": "import { afterEach, describe, expect, it, vi } from 'vitest'\nimport { parseNi, runCli } from '../../src'\n\nconst mocks = "
  },
  {
    "path": "tsconfig.json",
    "chars": 292,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"esnext\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": "
  },
  {
    "path": "tsdown.config.ts",
    "chars": 357,
    "preview": "import { defineConfig } from 'tsdown'\n\nexport default defineConfig({\n  entry: ['src/commands/*.ts'],\n  clean: true,\n  dt"
  },
  {
    "path": "vitest.config.ts",
    "chars": 241,
    "preview": "import process from 'node:process'\nimport { defineConfig } from 'vitest/config'\n\n// Disable global ni config in test to "
  }
]

About this extraction

This page contains the full source code of the antfu-collective/ni GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (124.3 KB), approximately 38.2k tokens, and a symbol index with 117 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!