├── .github └── workflows │ ├── publish-commit.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin ├── na.mjs ├── nci.mjs ├── ni.mjs ├── nlx.mjs ├── nr.mjs ├── nun.mjs └── nup.mjs ├── build.config.ts ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── commands │ ├── index.ts │ ├── na.ts │ ├── nci.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 ├── parse.ts ├── runner.ts ├── storage.ts └── utils.ts ├── taze.config.ts ├── test ├── config │ ├── .nirc │ └── config.test.ts ├── fixtures │ ├── lockfile │ │ ├── bun │ │ │ └── bun.lockb │ │ ├── npm │ │ │ └── package-lock.json │ │ ├── pnpm │ │ │ └── pnpm-lock.yaml │ │ ├── pnpm@6 │ │ │ └── pnpm-lock.yaml │ │ ├── unknown │ │ │ └── future-package-manager.json │ │ ├── yarn │ │ │ └── yarn.lock │ │ └── yarn@berry │ │ │ └── yarn.lock │ └── packager │ │ ├── bun │ │ └── package.json │ │ ├── npm │ │ └── package.json │ │ ├── pnpm-version-range │ │ └── package.json │ │ ├── pnpm │ │ └── package.json │ │ ├── pnpm@6 │ │ └── package.json │ │ ├── unknown │ │ └── package.json │ │ ├── yarn │ │ └── package.json │ │ └── yarn@berry │ │ └── package.json ├── na │ ├── bun.spec.ts │ ├── npm.spec.ts │ ├── pnpm.spec.ts │ ├── yarn.spec.ts │ └── yarn@berry.spec.ts ├── ng.spec.ts ├── ni │ ├── bun.spec.ts │ ├── npm.spec.ts │ ├── pnpm.spec.ts │ ├── yarn.spec.ts │ └── yarn@berry.spec.ts ├── nlx │ ├── bun.spec.ts │ ├── npm.spec.ts │ ├── pnpm.spec.ts │ ├── yarn.spec.ts │ └── yarn@berry.spec.ts ├── nr │ ├── bun.spec.ts │ ├── npm.spec.ts │ ├── pnpm.spec.ts │ ├── yarn.spec.ts │ └── yarn@berry.spec.ts ├── nun │ ├── bun.spec.ts │ ├── npm.spec.ts │ ├── pnpm.spec.ts │ ├── yarn.spec.ts │ └── yarn@berry.spec.ts ├── nup │ ├── bun.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 │ ├── detect.spec.ts │ └── runCli.spec.ts └── runner │ └── runCli.test.ts ├── tsconfig.json └── vitest.config.ts /.github/workflows/publish-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - '!**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v4.0.0 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Build 26 | run: pnpm build 27 | 28 | - run: pnpm dlx pkg-pr-new publish --pnpm 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | 23 | - run: npx changelogithub 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x, lts/*] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Install 29 | run: pnpm install 30 | - name: Lint 31 | run: pnpm run lint 32 | - name: Typecheck 33 | run: pnpm run typecheck 34 | - name: Test 35 | run: pnpm run test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | _storage.json 83 | 84 | # System files 85 | .DS_Store 86 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ni 2 | 3 | ~~*`npm i` in a yarn project, again? F\*\*k!*~~ 4 | 5 | **ni** - use the right package manager 6 | 7 |
8 | 9 | ``` 10 | npm i -g @antfu/ni 11 | ``` 12 | 13 | npm · yarn · pnpm · bun 14 | 15 |
16 | 17 | ### `ni` - install 18 | 19 | ```bash 20 | ni 21 | 22 | # npm install 23 | # yarn install 24 | # pnpm install 25 | # bun install 26 | ``` 27 | 28 | ```bash 29 | ni vite 30 | 31 | # npm i vite 32 | # yarn add vite 33 | # pnpm add vite 34 | # bun add vite 35 | ``` 36 | 37 | ```bash 38 | ni @types/node -D 39 | 40 | # npm i @types/node -D 41 | # yarn add @types/node -D 42 | # pnpm add -D @types/node 43 | # bun add -d @types/node 44 | ``` 45 | 46 | ```bash 47 | ni -P 48 | 49 | # npm i --omit=dev 50 | # yarn install --production 51 | # pnpm i --production 52 | # bun install --production 53 | ``` 54 | 55 | ```bash 56 | ni --frozen 57 | 58 | # npm ci 59 | # yarn install --frozen-lockfile (Yarn 1) 60 | # yarn install --immutable (Yarn Berry) 61 | # pnpm install --frozen-lockfile 62 | # bun install --frozen-lockfile 63 | ``` 64 | 65 | ```bash 66 | ni -g eslint 67 | 68 | # npm i -g eslint 69 | # yarn global add eslint (Yarn 1) 70 | # pnpm add -g eslint 71 | # bun add -g eslint 72 | 73 | # this uses default agent, regardless your current working directory 74 | ``` 75 | 76 | ```bash 77 | ni -i 78 | 79 | # interactively select the dependency to install 80 | # search for packages by name 81 | ``` 82 | 83 |
84 | 85 | ### `nr` - run 86 | 87 | ```bash 88 | nr dev --port=3000 89 | 90 | # npm run dev -- --port=3000 91 | # yarn run dev --port=3000 92 | # pnpm run dev --port=3000 93 | # bun run dev --port=3000 94 | ``` 95 | 96 | ```bash 97 | nr 98 | 99 | # interactively select the script to run 100 | # supports https://www.npmjs.com/package/npm-scripts-info convention 101 | ``` 102 | 103 | ```bash 104 | nr - 105 | 106 | # rerun the last command 107 | ``` 108 | 109 | ```bash 110 | nr --completion >> ~/.bashrc 111 | 112 | # add completion script to your shell (only bash supported for now) 113 | ``` 114 | 115 |
116 | 117 | ### `nlx` - download & execute 118 | 119 | ```bash 120 | nlx vitest 121 | 122 | # npx vitest 123 | # yarn dlx vitest 124 | # pnpm dlx vitest 125 | # bunx vitest 126 | ``` 127 | 128 |
129 | 130 | ### `nup` - upgrade 131 | 132 | ```bash 133 | nup 134 | 135 | # npm upgrade 136 | # yarn upgrade (Yarn 1) 137 | # yarn up (Yarn Berry) 138 | # pnpm update 139 | # bun update 140 | ``` 141 | 142 | ```bash 143 | nup -i 144 | 145 | # (not available for npm & bun) 146 | # yarn upgrade-interactive (Yarn 1) 147 | # yarn up -i (Yarn Berry) 148 | # pnpm update -i 149 | ``` 150 | 151 |
152 | 153 | ### `nun` - uninstall 154 | 155 | ```bash 156 | nun webpack 157 | 158 | # npm uninstall webpack 159 | # yarn remove webpack 160 | # pnpm remove webpack 161 | # bun remove webpack 162 | ``` 163 | 164 | ```bash 165 | nun 166 | 167 | # interactively select 168 | # the dependency to remove 169 | ``` 170 | 171 | ```bash 172 | nun -m 173 | 174 | # interactive select, 175 | # but with multiple dependencies 176 | ``` 177 | 178 | ```bash 179 | nun -g silent 180 | 181 | # npm uninstall -g silent 182 | # yarn global remove silent 183 | # pnpm remove -g silent 184 | # bun remove -g silent 185 | ``` 186 | 187 |
188 | 189 | ### `nci` - clean install 190 | 191 | ```bash 192 | nci 193 | 194 | # npm ci 195 | # yarn install --frozen-lockfile 196 | # pnpm install --frozen-lockfile 197 | # bun install --frozen-lockfile 198 | ``` 199 | 200 | if the corresponding node manager is not present, this command will install it globally along the way. 201 | 202 |
203 | 204 | ### `na` - agent alias 205 | 206 | ```bash 207 | na 208 | 209 | # npm 210 | # yarn 211 | # pnpm 212 | # bun 213 | ``` 214 | 215 | ```bash 216 | na run foo 217 | 218 | # npm run foo 219 | # yarn run foo 220 | # pnpm run foo 221 | # bun run foo 222 | ``` 223 | 224 |
225 | 226 | ### Global Flags 227 | 228 | ```bash 229 | # ? | Print the command execution depends on the agent 230 | ni vite ? 231 | 232 | # -C | Change directory before running the command 233 | ni -C packages/foo vite 234 | nr -C playground dev 235 | 236 | # -v, --version | Show version number 237 | ni -v 238 | 239 | # -h, --help | Show help 240 | ni -h 241 | ``` 242 | 243 |
244 | 245 | ### Config 246 | 247 | ```ini 248 | ; ~/.nirc 249 | 250 | ; fallback when no lock found 251 | defaultAgent=npm # default "prompt" 252 | 253 | ; for global installs 254 | globalAgent=npm 255 | ``` 256 | 257 | ```bash 258 | # ~/.bashrc 259 | 260 | # custom configuration file path 261 | export NI_CONFIG_FILE="$HOME/.config/ni/nirc" 262 | 263 | # environment variables have higher priority than config file if presented 264 | export NI_DEFAULT_AGENT="npm" # default "prompt" 265 | export NI_GLOBAL_AGENT="npm" 266 | ``` 267 | 268 | ```ps 269 | # for Windows 270 | 271 | # custom configuration file path in PowerShell accessible within the `$profile` path 272 | $Env:NI_CONFIG_FILE = 'C:\to\your\config\location' 273 | ``` 274 | 275 |
276 | 277 | ### Integrations 278 | 279 | #### asdf 280 | 281 | 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) 282 | 283 | ```bash 284 | # first add the plugin 285 | asdf plugin add ni https://github.com/CanRau/asdf-ni.git 286 | 287 | # then install the latest version 288 | asdf install ni latest 289 | 290 | # and make it globally available 291 | asdf global ni latest 292 | ``` 293 | 294 | ### How? 295 | 296 | **ni** assumes that you work with lock-files (and you should). 297 | 298 | Before `ni` runs the command, it detects your `yarn.lock` / `pnpm-lock.yaml` / `package-lock.json` / `bun.lock` / `bun.lockb` 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). 299 | 300 | ### Trouble shooting 301 | 302 | #### Conflicts with PowerShell 303 | 304 | 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: 305 | 306 | ```PowerShell 307 | 'Remove-Item Alias:ni -Force -ErrorAction Ignore' 308 | ``` 309 | 310 | 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 311 | 312 | - PowerShell 5 (Windows PowerShell): `C:\Users\USERNAME\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` 313 | - PowerShell 7: `C:\Users\USERNAME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` 314 | - VSCode: `C:\Users\USERNAME\Documents\PowerShell\Microsoft.VSCode_profile.ps1` 315 | 316 | You can use the following script to remove the alias at shell start by adding the above command to your profile: 317 | 318 | ```PowerShell 319 | if (-not (Test-Path $profile)) { 320 | New-Item -ItemType File -Path (Split-Path $profile) -Force -Name (Split-Path $profile -Leaf) 321 | } 322 | 323 | $profileEntry = 'Remove-Item Alias:ni -Force -ErrorAction Ignore' 324 | $profileContent = Get-Content $profile 325 | if ($profileContent -notcontains $profileEntry) { 326 | ("`n" + $profileEntry) | Out-File $profile -Append -Force -Encoding UTF8 327 | } 328 | ``` 329 | 330 | #### `nx`, `nix` and `nu` are no longer available 331 | 332 | 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). 333 | 334 | ```bash 335 | alias nx="nlx" 336 | # or 337 | alias nix="nlx" 338 | # or 339 | alias nu="nup" 340 | ``` 341 | -------------------------------------------------------------------------------- /bin/na.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/na.mjs' 4 | -------------------------------------------------------------------------------- /bin/nci.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/nci.mjs' 4 | -------------------------------------------------------------------------------- /bin/ni.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/ni.mjs' 4 | -------------------------------------------------------------------------------- /bin/nlx.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/nlx.mjs' 4 | -------------------------------------------------------------------------------- /bin/nr.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/nr.mjs' 4 | -------------------------------------------------------------------------------- /bin/nun.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/nun.mjs' 4 | -------------------------------------------------------------------------------- /bin/nup.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import '../dist/nup.mjs' 4 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'node:path' 2 | import { globSync } from 'tinyglobby' 3 | import { defineBuildConfig } from 'unbuild' 4 | 5 | export default defineBuildConfig({ 6 | entries: globSync( 7 | ['src/commands/*.ts'], 8 | { expandDirectories: false }, 9 | ).map(i => ({ 10 | input: i.slice(0, -3), 11 | name: basename(i).slice(0, -3), 12 | })), 13 | clean: true, 14 | declaration: 'node16', 15 | rollup: { 16 | inlineDependencies: [ 17 | 'which', 18 | 'ini', 19 | '@posva/prompts', 20 | 21 | 'terminal-link', 22 | 'ansi-escapes', 23 | 'environment', 24 | 'supports-hyperlinks', 25 | 'isexe', 26 | 'supports-color', 27 | 'has-flag', 28 | 'kleur', 29 | 'sisteransi', 30 | ], 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu({ 5 | pnpm: true, 6 | }) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antfu/ni", 3 | "type": "module", 4 | "version": "25.0.0", 5 | "packageManager": "pnpm@10.11.0", 6 | "description": "Use the right package manager", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "homepage": "https://github.com/antfu-collective/ni#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antfu-collective/ni.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/antfu-collective/ni/issues" 16 | }, 17 | "exports": { 18 | ".": "./dist/index.mjs", 19 | "./ni": "./dist/ni.mjs", 20 | "./nci": "./dist/nci.mjs", 21 | "./nr": "./dist/nr.mjs", 22 | "./nup": "./dist/nup.mjs", 23 | "./nlx": "./dist/nlx.mjs", 24 | "./na": "./dist/na.mjs", 25 | "./nun": "./dist/nun.mjs" 26 | }, 27 | "main": "./dist/index.mjs", 28 | "module": "./dist/index.mjs", 29 | "types": "./dist/index.d.mts", 30 | "bin": { 31 | "ni": "bin/ni.mjs", 32 | "nci": "bin/nci.mjs", 33 | "nr": "bin/nr.mjs", 34 | "nup": "bin/nup.mjs", 35 | "nlx": "bin/nlx.mjs", 36 | "na": "bin/na.mjs", 37 | "nun": "bin/nun.mjs" 38 | }, 39 | "files": [ 40 | "bin", 41 | "dist" 42 | ], 43 | "scripts": { 44 | "prepublishOnly": "npm run build", 45 | "dev": "tsx src/commands/ni.ts", 46 | "nr": "tsx src/commands/nr.ts", 47 | "build": "unbuild", 48 | "stub": "unbuild --stub", 49 | "release": "bumpp && pnpm publish", 50 | "typecheck": "tsc", 51 | "prepare": "npx simple-git-hooks", 52 | "lint": "eslint", 53 | "test": "vitest" 54 | }, 55 | "dependencies": { 56 | "ansis": "catalog:prod", 57 | "fzf": "catalog:prod", 58 | "package-manager-detector": "catalog:prod", 59 | "tinyexec": "catalog:prod" 60 | }, 61 | "devDependencies": { 62 | "@antfu/eslint-config": "catalog:dev", 63 | "@posva/prompts": "catalog:prod-inlined", 64 | "@types/ini": "catalog:dev", 65 | "@types/node": "catalog:dev", 66 | "@types/which": "catalog:dev", 67 | "bumpp": "catalog:dev", 68 | "eslint": "catalog:dev", 69 | "ini": "catalog:prod-inlined", 70 | "lint-staged": "catalog:dev", 71 | "simple-git-hooks": "catalog:dev", 72 | "taze": "catalog:dev", 73 | "terminal-link": "catalog:prod-inlined", 74 | "tinyglobby": "catalog:dev", 75 | "tsx": "catalog:dev", 76 | "typescript": "catalog:dev", 77 | "unbuild": "catalog:dev", 78 | "vitest": "catalog:dev", 79 | "which": "catalog:prod-inlined" 80 | }, 81 | "simple-git-hooks": { 82 | "pre-commit": "pnpm lint-staged" 83 | }, 84 | "lint-staged": { 85 | "*": "eslint --fix" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: [] 2 | catalogs: 3 | dev: 4 | '@antfu/eslint-config': ^4.13.2 5 | '@types/ini': ^4.1.1 6 | '@types/node': ^22.15.21 7 | '@types/which': ^3.0.4 8 | bumpp: ^10.1.1 9 | eslint: ^9.27.0 10 | lint-staged: ^16.0.0 11 | simple-git-hooks: ^2.13.0 12 | taze: ^19.1.0 13 | tinyglobby: ^0.2.14 14 | tsx: ^4.19.4 15 | typescript: ^5.8.3 16 | unbuild: ^3.5.0 17 | vitest: ^3.1.4 18 | prod: 19 | ansis: ^4.0.0 20 | fzf: ^0.5.2 21 | package-manager-detector: ^1.3.0 22 | tinyexec: ^1.0.1 23 | prod-inlined: 24 | '@posva/prompts': ^2.4.4 25 | ini: ^5.0.0 26 | terminal-link: ^4.0.0 27 | which: ^5.0.0 28 | onlyBuiltDependencies: 29 | - simple-git-hooks 30 | - unrs-resolver 31 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../index' 2 | -------------------------------------------------------------------------------- /src/commands/na.ts: -------------------------------------------------------------------------------- 1 | import { parseNa } from '../parse' 2 | import { runCli } from '../runner' 3 | 4 | runCli(parseNa) 5 | -------------------------------------------------------------------------------- /src/commands/nci.ts: -------------------------------------------------------------------------------- 1 | import { parseNi } from '../parse' 2 | import { runCli } from '../runner' 3 | 4 | runCli( 5 | (agent, args, hasLock) => parseNi(agent, [...args, '--frozen-if-present'], hasLock), 6 | { autoInstall: true }, 7 | ) 8 | -------------------------------------------------------------------------------- /src/commands/ni.ts: -------------------------------------------------------------------------------- 1 | import type { Choice } from '@posva/prompts' 2 | import process from 'node:process' 3 | import prompts from '@posva/prompts' 4 | import c from 'ansis' 5 | import { Fzf } from 'fzf' 6 | import { fetchNpmPackages } from '../fetch' 7 | import { parseNi } from '../parse' 8 | import { runCli } from '../runner' 9 | import { exclude } from '../utils' 10 | 11 | runCli(async (agent, args, ctx) => { 12 | const isInteractive = args[0] === '-i' 13 | 14 | if (isInteractive) { 15 | let fetchPattern: string 16 | 17 | if (args[1] && !args[1].startsWith('-')) { 18 | fetchPattern = args[1] 19 | } 20 | else { 21 | const { pattern } = await prompts({ 22 | type: 'text', 23 | name: 'pattern', 24 | message: 'search for package', 25 | }) 26 | 27 | fetchPattern = pattern 28 | } 29 | 30 | if (!fetchPattern) { 31 | process.exitCode = 1 32 | return 33 | } 34 | 35 | const packages = await fetchNpmPackages(fetchPattern) 36 | 37 | if (!packages.length) { 38 | console.error('No results found') 39 | process.exitCode = 1 40 | return 41 | } 42 | 43 | const fzf = new Fzf(packages, { 44 | selector: (item: Choice) => item.title, 45 | casing: 'case-insensitive', 46 | }) 47 | 48 | const { dependency } = await prompts({ 49 | type: 'autocomplete', 50 | name: 'dependency', 51 | choices: packages, 52 | instructions: false, 53 | message: 'choose a package to install', 54 | limit: 15, 55 | async suggest(input: string, choices: Choice[]) { 56 | const results = fzf.find(input) 57 | return results.map(r => choices.find((c: any) => c.value === r.item.value)) 58 | }, 59 | }) 60 | 61 | if (!dependency) { 62 | process.exitCode = 1 63 | return 64 | } 65 | 66 | args = exclude(args, '-d', '-p', '-i') 67 | 68 | /** 69 | * yarn and bun do not support 70 | * the installation of peers programmatically 71 | */ 72 | const canInstallPeers = ['npm', 'pnpm'].includes(agent) 73 | 74 | const { mode } = await prompts({ 75 | type: 'select', 76 | name: 'mode', 77 | message: `install ${c.yellow(dependency.name)} as`, 78 | choices: [ 79 | { 80 | title: 'prod', 81 | value: '', 82 | selected: true, 83 | }, 84 | { 85 | title: 'dev', 86 | value: '-D', 87 | }, 88 | { 89 | title: `peer`, 90 | value: '--save-peer', 91 | disabled: !canInstallPeers, 92 | }, 93 | ], 94 | }) 95 | 96 | args.push(dependency.name, mode) 97 | } 98 | 99 | return parseNi(agent, args, ctx) 100 | }) 101 | -------------------------------------------------------------------------------- /src/commands/nlx.ts: -------------------------------------------------------------------------------- 1 | import { parseNlx } from '../parse' 2 | import { runCli } from '../runner' 3 | 4 | runCli(parseNlx) 5 | -------------------------------------------------------------------------------- /src/commands/nr.ts: -------------------------------------------------------------------------------- 1 | import type { Choice } from '@posva/prompts' 2 | import type { RunnerContext } from '../runner' 3 | import process from 'node:process' 4 | import prompts from '@posva/prompts' 5 | import { byLengthAsc, Fzf } from 'fzf' 6 | import { rawCompletionScript } from '../completion' 7 | import { getPackageJSON } from '../fs' 8 | import { parseNr } from '../parse' 9 | import { runCli } from '../runner' 10 | import { dump, load } from '../storage' 11 | import { limitText } from '../utils' 12 | 13 | function readPackageScripts(ctx: RunnerContext | undefined) { 14 | // support https://www.npmjs.com/package/npm-scripts-info conventions 15 | const pkg = getPackageJSON(ctx) 16 | const rawScripts = pkg.scripts || {} 17 | const scriptsInfo = pkg['scripts-info'] || {} 18 | 19 | const scripts = Object.entries(rawScripts) 20 | .filter(i => !i[0].startsWith('?')) 21 | .map(([key, cmd]) => ({ 22 | key, 23 | cmd, 24 | description: scriptsInfo[key] || rawScripts[`?${key}`] || cmd, 25 | })) 26 | 27 | if (scripts.length === 0 && !ctx?.programmatic) { 28 | console.warn('No scripts found in package.json') 29 | } 30 | 31 | return scripts 32 | } 33 | 34 | runCli(async (agent, args, ctx) => { 35 | const storage = await load() 36 | 37 | // Use --completion to generate completion script and do completion logic 38 | // (No package manager would have an argument named --completion) 39 | if (args[0] === '--completion') { 40 | const compLine = process.env.COMP_LINE 41 | const rawCompCword = process.env.COMP_CWORD 42 | if (compLine !== undefined && rawCompCword !== undefined) { 43 | const compCword = Number.parseInt(rawCompCword, 10) 44 | const compWords = args.slice(1) 45 | // Only complete the second word (nr __here__ ...) 46 | if (compCword === 1) { 47 | const raw = readPackageScripts(ctx) 48 | const fzf = new Fzf(raw, { 49 | selector: item => item.key, 50 | casing: 'case-insensitive', 51 | tiebreakers: [byLengthAsc], 52 | }) 53 | 54 | // compWords will be ['nr'] when the user does not type anything after `nr` so fallback to empty string 55 | const results = fzf.find(compWords[1] || '') 56 | 57 | // eslint-disable-next-line no-console 58 | console.log(results.map(r => r.item.key).join('\n')) 59 | } 60 | } 61 | else { 62 | // eslint-disable-next-line no-console 63 | console.log(rawCompletionScript) 64 | } 65 | return 66 | } 67 | 68 | if (args[0] === '-') { 69 | if (!storage.lastRunCommand) { 70 | if (!ctx?.programmatic) { 71 | console.error('No last command found') 72 | process.exit(1) 73 | } 74 | 75 | throw new Error('No last command found') 76 | } 77 | args[0] = storage.lastRunCommand 78 | } 79 | 80 | if (args.length === 0 && !ctx?.programmatic) { 81 | const raw = readPackageScripts(ctx) 82 | 83 | const terminalColumns = process.stdout?.columns || 80 84 | 85 | const choices: Choice[] = raw 86 | .map(({ key, description }) => ({ 87 | title: key, 88 | value: key, 89 | description: limitText(description, terminalColumns - 15), 90 | })) 91 | 92 | const fzf = new Fzf(raw, { 93 | selector: item => `${item.key} ${item.description}`, 94 | casing: 'case-insensitive', 95 | tiebreakers: [byLengthAsc], 96 | }) 97 | 98 | if (storage.lastRunCommand) { 99 | const last = choices.find(i => i.value === storage.lastRunCommand) 100 | if (last) 101 | choices.unshift(last) 102 | } 103 | 104 | try { 105 | const { fn } = await prompts({ 106 | name: 'fn', 107 | message: 'script to run', 108 | type: 'autocomplete', 109 | choices, 110 | async suggest(input: string, choices: Choice[]) { 111 | if (!input) 112 | return choices 113 | const results = fzf.find(input) 114 | return results.map(r => choices.find(c => c.value === r.item.key)) 115 | }, 116 | }) 117 | if (!fn) 118 | return 119 | args.push(fn) 120 | } 121 | catch { 122 | process.exit(1) 123 | } 124 | } 125 | 126 | if (storage.lastRunCommand !== args[0]) { 127 | storage.lastRunCommand = args[0] 128 | dump() 129 | } 130 | 131 | return parseNr(agent, args) 132 | }) 133 | -------------------------------------------------------------------------------- /src/commands/nun.ts: -------------------------------------------------------------------------------- 1 | import type { Choice, PromptType } from '@posva/prompts' 2 | import process from 'node:process' 3 | import prompts from '@posva/prompts' 4 | import { Fzf } from 'fzf' 5 | import { getPackageJSON } from '../fs' 6 | import { parseNun } from '../parse' 7 | import { runCli } from '../runner' 8 | import { exclude } from '../utils' 9 | 10 | runCli(async (agent, args, ctx) => { 11 | const isInteractive = !args.length && !ctx?.programmatic 12 | 13 | if (isInteractive || args[0] === '-m') { 14 | const pkg = getPackageJSON(ctx) 15 | 16 | const allDependencies = { ...pkg.dependencies, ...pkg.devDependencies } 17 | 18 | const raw = Object.entries(allDependencies) as [string, string][] 19 | 20 | if (!raw.length) { 21 | console.error('No dependencies found') 22 | return 23 | } 24 | 25 | const fzf = new Fzf(raw, { 26 | selector: ([dep, version]) => `${dep} ${version}`, 27 | casing: 'case-insensitive', 28 | }) 29 | 30 | const choices: Choice[] = raw.map(([dependency, version]) => ({ 31 | title: dependency, 32 | value: dependency, 33 | description: version, 34 | })) 35 | 36 | const isMultiple = args[0] === '-m' 37 | 38 | const type: PromptType = isMultiple 39 | ? 'autocompleteMultiselect' 40 | : 'autocomplete' 41 | 42 | if (isMultiple) 43 | args = exclude(args, '-m') 44 | 45 | try { 46 | const { depsToRemove } = await prompts({ 47 | type, 48 | name: 'depsToRemove', 49 | choices, 50 | instructions: false, 51 | message: `remove ${isMultiple ? 'dependencies' : 'dependency'}`, 52 | async suggest(input: string, choices: Choice[]) { 53 | const results = fzf.find(input) 54 | return results.map(r => choices.find(c => c.value === r.item[0])) 55 | }, 56 | }) 57 | 58 | if (!depsToRemove) { 59 | process.exitCode = 1 60 | return 61 | } 62 | 63 | const isSingleDependency = typeof depsToRemove === 'string' 64 | 65 | if (isSingleDependency) 66 | args.push(depsToRemove) 67 | else args.push(...depsToRemove) 68 | } 69 | catch { 70 | process.exit(1) 71 | } 72 | } 73 | 74 | return parseNun(agent, args, ctx) 75 | }) 76 | -------------------------------------------------------------------------------- /src/commands/nup.ts: -------------------------------------------------------------------------------- 1 | import { parseNup } from '../parse' 2 | import { runCli } from '../runner' 3 | 4 | runCli(parseNup) 5 | -------------------------------------------------------------------------------- /src/completion.ts: -------------------------------------------------------------------------------- 1 | // Print completion script 2 | export const rawCompletionScript = ` 3 | ###-begin-nr-completion-### 4 | 5 | if type complete &>/dev/null; then 6 | _nr_completion() { 7 | local words 8 | local cur 9 | local cword 10 | _get_comp_words_by_ref -n =: cur words cword 11 | IFS=$'\\n' 12 | COMPREPLY=($(COMP_CWORD=$cword COMP_LINE=$cur nr --completion \${words[@]})) 13 | } 14 | complete -F _nr_completion nr 15 | fi 16 | 17 | ###-end-nr-completion-### 18 | `.trim() 19 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'package-manager-detector' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import process from 'node:process' 5 | import ini from 'ini' 6 | import { detect } from './detect' 7 | 8 | const customRcPath = process.env.NI_CONFIG_FILE 9 | 10 | const home = process.platform === 'win32' 11 | ? process.env.USERPROFILE 12 | : process.env.HOME 13 | 14 | const defaultRcPath = path.join(home || '~/', '.nirc') 15 | 16 | const rcPath = customRcPath || defaultRcPath 17 | 18 | interface Config { 19 | defaultAgent: Agent | 'prompt' 20 | globalAgent: Agent 21 | } 22 | 23 | const defaultConfig: Config = { 24 | defaultAgent: 'prompt', 25 | globalAgent: 'npm', 26 | } 27 | 28 | let config: Config | undefined 29 | 30 | export async function getConfig(): Promise { 31 | if (!config) { 32 | config = Object.assign( 33 | {}, 34 | defaultConfig, 35 | fs.existsSync(rcPath) 36 | ? ini.parse(fs.readFileSync(rcPath, 'utf-8')) 37 | : null, 38 | ) 39 | 40 | if (process.env.NI_DEFAULT_AGENT) 41 | config.defaultAgent = process.env.NI_DEFAULT_AGENT as Agent 42 | 43 | if (process.env.NI_GLOBAL_AGENT) 44 | config.globalAgent = process.env.NI_GLOBAL_AGENT as Agent 45 | 46 | const agent = await detect({ programmatic: true }) 47 | if (agent) 48 | config.defaultAgent = agent 49 | } 50 | 51 | return config 52 | } 53 | 54 | export async function getDefaultAgent(programmatic?: boolean) { 55 | const { defaultAgent } = await getConfig() 56 | if (defaultAgent === 'prompt' && (programmatic || process.env.CI)) 57 | return 'npm' 58 | return defaultAgent 59 | } 60 | 61 | export async function getGlobalAgent() { 62 | const { globalAgent } = await getConfig() 63 | return globalAgent 64 | } 65 | -------------------------------------------------------------------------------- /src/detect.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import prompts from '@posva/prompts' 3 | import { detect as detectPM } from 'package-manager-detector' 4 | import { INSTALL_PAGE } from 'package-manager-detector/constants' 5 | import terminalLink from 'terminal-link' 6 | import { x } from 'tinyexec' 7 | import { cmdExists } from './utils' 8 | 9 | export interface DetectOptions { 10 | autoInstall?: boolean 11 | programmatic?: boolean 12 | cwd?: string 13 | /** 14 | * Should use Volta when present 15 | * 16 | * @see https://volta.sh/ 17 | * @default true 18 | */ 19 | detectVolta?: boolean 20 | } 21 | 22 | export async function detect({ autoInstall, programmatic, cwd }: DetectOptions = {}) { 23 | const { 24 | name, 25 | agent, 26 | version, 27 | } = await detectPM({ 28 | cwd, 29 | onUnknown: (packageManager) => { 30 | if (!programmatic) { 31 | console.warn('[ni] Unknown packageManager:', packageManager) 32 | } 33 | return undefined 34 | }, 35 | }) || {} 36 | 37 | // auto install 38 | if (name && !cmdExists(name) && !programmatic) { 39 | if (!autoInstall) { 40 | console.warn(`[ni] Detected ${name} but it doesn't seem to be installed.\n`) 41 | 42 | if (process.env.CI) 43 | process.exit(1) 44 | 45 | const link = terminalLink(name, INSTALL_PAGE[name]) 46 | const { tryInstall } = await prompts({ 47 | name: 'tryInstall', 48 | type: 'confirm', 49 | message: `Would you like to globally install ${link}?`, 50 | }) 51 | if (!tryInstall) 52 | process.exit(1) 53 | } 54 | 55 | await x( 56 | 'npm', 57 | ['i', '-g', `${name}${version ? `@${version}` : ''}`], 58 | { 59 | nodeOptions: { 60 | stdio: 'inherit', 61 | cwd, 62 | }, 63 | throwOnError: true, 64 | }, 65 | ) 66 | } 67 | 68 | return agent 69 | } 70 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | export interface EnvironmentOptions { 4 | autoInstall: boolean 5 | } 6 | 7 | const DEFAULT_ENVIRONMENT_OPTIONS: EnvironmentOptions = { 8 | autoInstall: false, 9 | } 10 | 11 | export function getEnvironmentOptions(): EnvironmentOptions { 12 | return { 13 | ...DEFAULT_ENVIRONMENT_OPTIONS, 14 | autoInstall: process.env.NI_AUTO_INSTALL === 'true', 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { Choice } from '@posva/prompts' 2 | import process from 'node:process' 3 | import c from 'ansis' 4 | import { formatPackageWithUrl } from './utils' 5 | 6 | export interface NpmPackage { 7 | name: string 8 | description: string 9 | version: string 10 | keywords: string[] 11 | date: string 12 | links: { 13 | npm: string 14 | homepage: string 15 | repository: string 16 | } 17 | } 18 | 19 | interface NpmRegistryResponse { 20 | objects: { package: NpmPackage }[] 21 | } 22 | 23 | export async function fetchNpmPackages(pattern: string): Promise { 24 | const registryLink = (pattern: string) => 25 | `https://registry.npmjs.com/-/v1/search?text=${pattern}&size=35` 26 | 27 | const terminalColumns = process.stdout?.columns || 80 28 | 29 | try { 30 | const result = await fetch(registryLink(pattern)) 31 | .then(res => res.json()) as NpmRegistryResponse 32 | 33 | return result.objects.map(({ package: pkg }) => ({ 34 | title: formatPackageWithUrl( 35 | `${pkg.name.padEnd(30, ' ')} ${c.blue`v${pkg.version}`}`, 36 | pkg.links.repository ?? pkg.links.npm, 37 | terminalColumns, 38 | ), 39 | value: pkg, 40 | })) 41 | } 42 | catch { 43 | console.error('Error when fetching npm registry') 44 | process.exit(1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/fs.ts: -------------------------------------------------------------------------------- 1 | import type { RunnerContext } from './runner' 2 | import fs from 'node:fs' 3 | import { resolve } from 'node:path' 4 | import process from 'node:process' 5 | 6 | export function getPackageJSON(ctx?: RunnerContext): any { 7 | const cwd = ctx?.cwd ?? process.cwd() 8 | const path = resolve(cwd, 'package.json') 9 | 10 | if (fs.existsSync(path)) { 11 | try { 12 | const raw = fs.readFileSync(path, 'utf-8') 13 | const data = JSON.parse(raw) 14 | return data 15 | } 16 | catch (e) { 17 | if (!ctx?.programmatic) { 18 | console.warn('Failed to parse package.json') 19 | process.exit(1) 20 | } 21 | 22 | throw e 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './detect' 3 | 4 | export * from './parse' 5 | export * from './runner' 6 | export * from './utils' 7 | export * from 'package-manager-detector/commands' 8 | export * from 'package-manager-detector/constants' 9 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Agent, Command, ResolvedCommand } from 'package-manager-detector' 2 | import type { Runner } from './runner' 3 | import { COMMANDS, constructCommand } from '.' 4 | import { exclude } from './utils' 5 | 6 | export class UnsupportedCommand extends Error { 7 | constructor({ agent, command }: { agent: Agent, command: Command }) { 8 | super(`Command "${command}" is not support by agent "${agent}"`) 9 | } 10 | } 11 | 12 | export function getCommand( 13 | agent: Agent, 14 | command: Command, 15 | args: string[] = [], 16 | ): ResolvedCommand { 17 | if (!COMMANDS[agent]) 18 | throw new Error(`Unsupported agent "${agent}"`) 19 | if (!COMMANDS[agent][command]) 20 | throw new UnsupportedCommand({ agent, command }) 21 | 22 | return constructCommand(COMMANDS[agent][command], args)! 23 | } 24 | 25 | export const parseNi = ((agent, args, ctx) => { 26 | // bun use `-d` instead of `-D`, #90 27 | if (agent === 'bun') 28 | args = args.map(i => i === '-D' ? '-d' : i) 29 | 30 | // npm use `--omit=dev` instead of `--production` 31 | if (agent === 'npm') 32 | args = args.map(i => i === '-P' ? '--omit=dev' : i) 33 | 34 | if (args.includes('-P')) 35 | args = args.map(i => i === '-P' ? '--production' : i) 36 | 37 | if (args.includes('-g')) 38 | return getCommand(agent, 'global', exclude(args, '-g')) 39 | 40 | if (args.includes('--frozen-if-present')) { 41 | args = exclude(args, '--frozen-if-present') 42 | return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args) 43 | } 44 | 45 | if (args.includes('--frozen')) 46 | return getCommand(agent, 'frozen', exclude(args, '--frozen')) 47 | 48 | if (args.length === 0 || args.every(i => i.startsWith('-'))) 49 | return getCommand(agent, 'install', args) 50 | 51 | return getCommand(agent, 'add', args) 52 | }) 53 | 54 | export const parseNr = ((agent, args) => { 55 | if (args.length === 0) 56 | args.push('start') 57 | 58 | let hasIfPresent = false 59 | if (args.includes('--if-present')) { 60 | args = exclude(args, '--if-present') 61 | hasIfPresent = true 62 | } 63 | 64 | const cmd = getCommand(agent, 'run', args) 65 | if (!cmd) 66 | return cmd 67 | 68 | if (hasIfPresent) 69 | cmd.args.splice(1, 0, '--if-present') 70 | 71 | return cmd 72 | }) 73 | 74 | export const parseNup = ((agent, args) => { 75 | if (args.includes('-i')) 76 | return getCommand(agent, 'upgrade-interactive', exclude(args, '-i')) 77 | 78 | return getCommand(agent, 'upgrade', args) 79 | }) 80 | 81 | export const parseNun = ((agent, args) => { 82 | if (args.includes('-g')) 83 | return getCommand(agent, 'global_uninstall', exclude(args, '-g')) 84 | return getCommand(agent, 'uninstall', args) 85 | }) 86 | 87 | export const parseNlx = ((agent, args) => { 88 | return getCommand(agent, 'execute', args) 89 | }) 90 | 91 | export const parseNa = ((agent, args) => { 92 | return getCommand(agent, 'agent', args) 93 | }) 94 | 95 | export function serializeCommand(command?: ResolvedCommand) { 96 | if (!command) 97 | return undefined 98 | if (command.args.length === 0) 99 | return command.command 100 | return `${command.command} ${command.args.map(i => i.includes(' ') ? `"${i}"` : i).join(' ')}` 101 | } 102 | -------------------------------------------------------------------------------- /src/runner.ts: -------------------------------------------------------------------------------- 1 | import type { Agent, ResolvedCommand } from 'package-manager-detector' 2 | import type { Options as TinyExecOptions } from 'tinyexec' 3 | import type { DetectOptions } from './detect' 4 | /* eslint-disable no-console */ 5 | import { resolve } from 'node:path' 6 | import process from 'node:process' 7 | import prompts from '@posva/prompts' 8 | import c from 'ansis' 9 | import { AGENTS } from 'package-manager-detector' 10 | import { x } from 'tinyexec' 11 | import { version } from '../package.json' 12 | import { getDefaultAgent, getGlobalAgent } from './config' 13 | import { detect } from './detect' 14 | import { getEnvironmentOptions } from './environment' 15 | import { getCommand, UnsupportedCommand } from './parse' 16 | import { cmdExists, remove } from './utils' 17 | 18 | const DEBUG_SIGN = '?' 19 | 20 | export interface RunnerContext { 21 | programmatic?: boolean 22 | hasLock?: boolean 23 | cwd?: string 24 | } 25 | 26 | export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise | ResolvedCommand | undefined 27 | 28 | export async function runCli(fn: Runner, options: DetectOptions & { args?: string[] } = {}) { 29 | options = { 30 | ...getEnvironmentOptions(), 31 | ...options, 32 | } 33 | const { 34 | args = process.argv.slice(2).filter(Boolean), 35 | } = options 36 | try { 37 | await run(fn, args, options) 38 | } 39 | catch (error) { 40 | if (error instanceof UnsupportedCommand && !options.programmatic) 41 | console.log(c.red(`\u2717 ${error.message}`)) 42 | 43 | if (!options.programmatic) 44 | process.exit(1) 45 | 46 | throw error 47 | } 48 | } 49 | 50 | export async function getCliCommand( 51 | fn: Runner, 52 | args: string[], 53 | options: DetectOptions = {}, 54 | cwd: string = options.cwd ?? process.cwd(), 55 | ) { 56 | const isGlobal = args.includes('-g') 57 | if (isGlobal) 58 | return await fn(await getGlobalAgent(), args) 59 | 60 | let agent = (await detect({ ...options, cwd })) || (await getDefaultAgent(options.programmatic)) 61 | if (agent === 'prompt') { 62 | agent = ( 63 | await prompts({ 64 | name: 'agent', 65 | type: 'select', 66 | message: 'Choose the agent', 67 | choices: AGENTS.filter(i => !i.includes('@')).map(value => ({ title: value, value })), 68 | }) 69 | ).agent 70 | if (!agent) 71 | return 72 | } 73 | 74 | return await fn(agent as Agent, args, { 75 | programmatic: options.programmatic, 76 | hasLock: Boolean(agent), 77 | cwd, 78 | }) 79 | } 80 | 81 | export async function run(fn: Runner, args: string[], options: DetectOptions = {}) { 82 | const { 83 | detectVolta = true, 84 | } = options 85 | 86 | const debug = args.includes(DEBUG_SIGN) 87 | if (debug) 88 | remove(args, DEBUG_SIGN) 89 | 90 | let cwd = options.cwd ?? process.cwd() 91 | if (args[0] === '-C') { 92 | cwd = resolve(cwd, args[1]) 93 | args.splice(0, 2) 94 | } 95 | 96 | if (args.length === 1 && (args[0]?.toLowerCase() === '-v' || args[0] === '--version')) { 97 | const getCmd = (a: Agent) => AGENTS.includes(a) 98 | ? getCommand(a, 'agent', ['-v']) 99 | : { command: a, args: ['-v'] } 100 | const xVersionOptions = { 101 | nodeOptions: { 102 | cwd, 103 | }, 104 | throwOnError: true, 105 | } satisfies Partial 106 | const getV = (a: string) => { 107 | const { command, args } = getCmd(a as Agent) 108 | return x(command, args, xVersionOptions) 109 | .then(e => e.stdout) 110 | .then(e => e.startsWith('v') ? e : `v${e}`) 111 | } 112 | const globalAgentPromise = getGlobalAgent() 113 | const globalAgentVersionPromise = globalAgentPromise.then(getV) 114 | const agentPromise = detect({ ...options, cwd }).then(a => a || '') 115 | const agentVersionPromise = agentPromise.then(a => a && getV(a)) 116 | const nodeVersionPromise = getV('node') 117 | 118 | console.log(`@antfu/ni ${c.cyan`v${version}`}`) 119 | console.log(`node ${c.green(await nodeVersionPromise)}`) 120 | const [agent, agentVersion] = await Promise.all([agentPromise, agentVersionPromise]) 121 | if (agent) 122 | console.log(`${agent.padEnd(10)} ${c.blue(agentVersion)}`) 123 | else 124 | console.log('agent no lock file') 125 | const [globalAgent, globalAgentVersion] = await Promise.all([globalAgentPromise, globalAgentVersionPromise]) 126 | console.log(`${(`${globalAgent} -g`).padEnd(10)} ${c.blue(globalAgentVersion)}`) 127 | return 128 | } 129 | 130 | if (args.length === 1 && ['-h', '--help'].includes(args[0])) { 131 | const dash = c.dim('-') 132 | console.log(c.green.bold('@antfu/ni') + c.dim` use the right package manager v${version}\n`) 133 | console.log(`ni ${dash} install`) 134 | console.log(`nr ${dash} run`) 135 | console.log(`nlx ${dash} execute`) 136 | console.log(`nup ${dash} upgrade`) 137 | console.log(`nun ${dash} uninstall`) 138 | console.log(`nci ${dash} clean install`) 139 | console.log(`na ${dash} agent alias`) 140 | console.log(`ni -v ${dash} show used agent`) 141 | console.log(`ni -i ${dash} interactive package management`) 142 | console.log(c.yellow('\ncheck https://github.com/antfu/ni for more documentation.')) 143 | return 144 | } 145 | 146 | const command = await getCliCommand(fn, args, options, cwd) 147 | 148 | if (!command) 149 | return 150 | 151 | if (detectVolta && cmdExists('volta')) { 152 | command.args = ['run', command.command, ...command.args] 153 | command.command = 'volta' 154 | } 155 | 156 | if (debug) { 157 | const commandStr = [command.command, ...command.args].join(' ') 158 | console.log(commandStr) 159 | return 160 | } 161 | 162 | await x( 163 | command.command, 164 | command.args, 165 | { 166 | nodeOptions: { 167 | stdio: 'inherit', 168 | cwd, 169 | }, 170 | throwOnError: true, 171 | }, 172 | ) 173 | } 174 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fs } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { CLI_TEMP_DIR, writeFileSafe } from './utils' 4 | 5 | export interface Storage { 6 | lastRunCommand?: string 7 | } 8 | 9 | let storage: Storage | undefined 10 | 11 | const storagePath = resolve(CLI_TEMP_DIR, '_storage.json') 12 | 13 | export async function load(fn?: (storage: Storage) => Promise | boolean) { 14 | if (!storage) { 15 | storage = existsSync(storagePath) 16 | ? (JSON.parse(await fs.readFile(storagePath, 'utf-8') || '{}') || {}) 17 | : {} 18 | } 19 | 20 | if (fn) { 21 | if (await fn(storage!)) 22 | await dump() 23 | } 24 | 25 | return storage! 26 | } 27 | 28 | export async function dump() { 29 | if (storage) 30 | await writeFileSafe(storagePath, JSON.stringify(storage)) 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer } from 'node:buffer' 2 | import { existsSync, promises as fs } from 'node:fs' 3 | import os from 'node:os' 4 | import { dirname, join } from 'node:path' 5 | import process from 'node:process' 6 | import c from 'ansis' 7 | import terminalLink from 'terminal-link' 8 | import which from 'which' 9 | 10 | export const CLI_TEMP_DIR = join(os.tmpdir(), 'antfu-ni') 11 | 12 | export function remove(arr: T[], v: T) { 13 | const index = arr.indexOf(v) 14 | if (index >= 0) 15 | arr.splice(index, 1) 16 | 17 | return arr 18 | } 19 | 20 | export function exclude(arr: T[], ...v: T[]) { 21 | return arr.slice().filter(item => !v.includes(item)) 22 | } 23 | 24 | export function cmdExists(cmd: string) { 25 | return which.sync(cmd, { nothrow: true }) !== null 26 | } 27 | 28 | interface TempFile { 29 | path: string 30 | fd: fs.FileHandle 31 | cleanup: () => void 32 | } 33 | 34 | let counter = 0 35 | 36 | async function openTemp(): Promise { 37 | if (!existsSync(CLI_TEMP_DIR)) 38 | await fs.mkdir(CLI_TEMP_DIR, { recursive: true }) 39 | 40 | const competitivePath = join(CLI_TEMP_DIR, `.${process.pid}.${counter}`) 41 | counter += 1 42 | 43 | return fs.open(competitivePath, 'wx') 44 | .then(fd => ({ 45 | fd, 46 | path: competitivePath, 47 | cleanup() { 48 | fd.close().then(() => { 49 | if (existsSync(competitivePath)) 50 | fs.unlink(competitivePath) 51 | }) 52 | }, 53 | })) 54 | .catch((error: any) => { 55 | if (error && error.code === 'EEXIST') 56 | return openTemp() 57 | 58 | else 59 | return undefined 60 | }) 61 | } 62 | 63 | /** 64 | * Write file safely avoiding conflicts 65 | */ 66 | export async function writeFileSafe( 67 | path: string, 68 | data: string | Buffer = '', 69 | ): Promise { 70 | const temp = await openTemp() 71 | 72 | if (temp) { 73 | fs.writeFile(temp.path, data) 74 | .then(() => { 75 | const directory = dirname(path) 76 | if (!existsSync(directory)) 77 | fs.mkdir(directory, { recursive: true }) 78 | 79 | return fs.rename(temp.path, path) 80 | .then(() => true) 81 | .catch(() => false) 82 | }) 83 | .catch(() => false) 84 | .finally(temp.cleanup) 85 | } 86 | 87 | return false 88 | } 89 | 90 | export function limitText(text: string, maxWidth: number) { 91 | if (text.length <= maxWidth) 92 | return text 93 | return `${text.slice(0, maxWidth)}${c.dim('…')}` 94 | } 95 | 96 | export function formatPackageWithUrl(pkg: string, url?: string, limits = 80) { 97 | return url 98 | ? terminalLink( 99 | pkg, 100 | url, 101 | { 102 | fallback: (_, url) => (pkg.length + url.length > limits) 103 | ? pkg 104 | : pkg + c.dim` - ${url}`, 105 | }, 106 | ) 107 | : pkg 108 | } 109 | -------------------------------------------------------------------------------- /taze.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'taze' 2 | 3 | export default defineConfig({ 4 | ignorePaths: [ 5 | 'test/fixtures', 6 | ], 7 | }) 8 | -------------------------------------------------------------------------------- /test/config/.nirc: -------------------------------------------------------------------------------- 1 | defaultAgent=npm 2 | globalAgent=pnpm -------------------------------------------------------------------------------- /test/config/config.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { beforeEach, expect, it, vi } from 'vitest' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | beforeEach(() => { 8 | vi.unstubAllEnvs() 9 | vi.resetModules() 10 | }) 11 | 12 | vi.mock('../../src/detect', () => ({ 13 | detect: vi.fn(), 14 | })) 15 | 16 | it('has correct defaults', async () => { 17 | const { getConfig } = await import('../../src/config') 18 | const config = await getConfig() 19 | 20 | expect(config).toEqual({ 21 | defaultAgent: 'prompt', 22 | globalAgent: 'npm', 23 | }) 24 | }) 25 | 26 | it('loads .nirc', async () => { 27 | vi.stubEnv('NI_CONFIG_FILE', join(__dirname, './.nirc')) 28 | 29 | const { getConfig } = await import('../../src/config') 30 | const config = await getConfig() 31 | 32 | expect(config).toEqual({ 33 | defaultAgent: 'npm', 34 | globalAgent: 'pnpm', 35 | }) 36 | }) 37 | 38 | it('reads environment variable config', async () => { 39 | vi.stubEnv('NI_DEFAULT_AGENT', 'npm') 40 | vi.stubEnv('NI_GLOBAL_AGENT', 'pnpm') 41 | 42 | const { getConfig } = await import('../../src/config') 43 | const config = await getConfig() 44 | 45 | expect(config).toEqual({ 46 | defaultAgent: 'npm', 47 | globalAgent: 'pnpm', 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/fixtures/lockfile/bun/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/bun/bun.lockb -------------------------------------------------------------------------------- /test/fixtures/lockfile/npm/package-lock.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/npm/package-lock.json -------------------------------------------------------------------------------- /test/fixtures/lockfile/pnpm/pnpm-lock.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/pnpm/pnpm-lock.yaml -------------------------------------------------------------------------------- /test/fixtures/lockfile/pnpm@6/pnpm-lock.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/pnpm@6/pnpm-lock.yaml -------------------------------------------------------------------------------- /test/fixtures/lockfile/unknown/future-package-manager.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/lockfile/yarn/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/yarn/yarn.lock -------------------------------------------------------------------------------- /test/fixtures/lockfile/yarn@berry/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu-collective/ni/4d6f6d6aabdfb7226778fb101f9a1b7b7fe873e2/test/fixtures/lockfile/yarn@berry/yarn.lock -------------------------------------------------------------------------------- /test/fixtures/packager/bun/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "bun@0" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "npm@7" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/pnpm-version-range/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "^pnpm@8.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "pnpm@8" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/pnpm@6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "pnpm@6" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/unknown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "future-package-manager" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "yarn@1" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/packager/yarn@berry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "yarn@3" 3 | } 4 | -------------------------------------------------------------------------------- /test/na/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNa, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'bun')) 16 | it('foo', _('foo', 'bun foo')) 17 | it('run test', _('run test', 'bun run test')) 18 | -------------------------------------------------------------------------------- /test/na/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNa, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'npm')) 16 | it('foo', _('foo', 'npm foo')) 17 | it('run test', _('run test', 'npm run test')) 18 | -------------------------------------------------------------------------------- /test/na/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNa, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'pnpm')) 16 | it('foo', _('foo', 'pnpm foo')) 17 | it('run test', _('run test', 'pnpm run test')) 18 | -------------------------------------------------------------------------------- /test/na/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNa, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn')) 16 | it('foo', _('foo', 'yarn foo')) 17 | it('run test', _('run test', 'yarn run test')) 18 | -------------------------------------------------------------------------------- /test/na/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNa, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNa(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn')) 16 | it('foo', _('foo', 'yarn foo')) 17 | it('run test', _('run test', 'yarn run test')) 18 | -------------------------------------------------------------------------------- /test/ng.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { getCommand } from '../src/commands' 3 | 4 | it('wrong agent', () => { 5 | expect(() => { 6 | getCommand('idk' as any, 'install', []) 7 | }).toThrow('Unsupported agent "idk"') 8 | }) 9 | -------------------------------------------------------------------------------- /test/ni/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNi, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'bun install')) 16 | 17 | it('single add', _('axios', 'bun add axios')) 18 | 19 | it('add dev', _('vite -D', 'bun add vite -d')) 20 | 21 | it('multiple', _('eslint @types/node', 'bun add eslint @types/node')) 22 | 23 | it('global', _('eslint -g', 'bun add -g eslint')) 24 | 25 | it('frozen', _('--frozen', 'bun install --frozen-lockfile')) 26 | 27 | it('production', _('-P', 'bun install --production')) 28 | 29 | it('frozen production', _('--frozen -P', 'bun install --frozen-lockfile --production')) 30 | -------------------------------------------------------------------------------- /test/ni/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNi, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'npm i')) 16 | 17 | it('single add', _('axios', 'npm i axios')) 18 | 19 | it('multiple', _('eslint @types/node', 'npm i eslint @types/node')) 20 | 21 | it('-D', _('eslint @types/node -D', 'npm i eslint @types/node -D')) 22 | 23 | it('global', _('eslint -g', 'npm i -g eslint')) 24 | 25 | it('frozen', _('--frozen', 'npm ci')) 26 | 27 | it('production', _('-P', 'npm i --omit=dev')) 28 | 29 | it('frozen production', _('--frozen -P', 'npm ci --omit=dev')) 30 | -------------------------------------------------------------------------------- /test/ni/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNi, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'pnpm i')) 16 | 17 | it('single add', _('axios', 'pnpm add axios')) 18 | 19 | it('multiple', _('eslint @types/node', 'pnpm add eslint @types/node')) 20 | 21 | it('-D', _('-D eslint @types/node', 'pnpm add -D eslint @types/node')) 22 | 23 | it('global', _('eslint -g', 'pnpm add -g eslint')) 24 | 25 | it('frozen', _('--frozen', 'pnpm i --frozen-lockfile')) 26 | 27 | it('forward1', _('--anything', 'pnpm i --anything')) 28 | it('forward2', _('-a', 'pnpm i -a')) 29 | 30 | it('production', _('-P', 'pnpm i --production')) 31 | 32 | it('frozen production', _('--frozen -P', 'pnpm i --frozen-lockfile --production')) 33 | -------------------------------------------------------------------------------- /test/ni/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNi, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn install')) 16 | 17 | it('single add', _('axios', 'yarn add axios')) 18 | 19 | it('multiple', _('eslint @types/node', 'yarn add eslint @types/node')) 20 | 21 | it('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D')) 22 | 23 | it('global', _('eslint ni -g', 'yarn global add eslint ni')) 24 | 25 | it('frozen', _('--frozen', 'yarn install --frozen-lockfile')) 26 | 27 | it('production', _('-P', 'yarn install --production')) 28 | 29 | it('frozen production', _('--frozen -P', 'yarn install --frozen-lockfile --production')) 30 | -------------------------------------------------------------------------------- /test/ni/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNi, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNi(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn install')) 16 | 17 | it('single add', _('axios', 'yarn add axios')) 18 | 19 | it('multiple', _('eslint @types/node', 'yarn add eslint @types/node')) 20 | 21 | it('-D', _('eslint @types/node -D', 'yarn add eslint @types/node -D')) 22 | 23 | it('global', _('eslint ni -g', 'npm i -g eslint ni')) 24 | 25 | it('frozen', _('--frozen', 'yarn install --immutable')) 26 | 27 | it('production', _('-P', 'yarn install --production')) 28 | 29 | it('frozen production', _('--frozen -P', 'yarn install --immutable --production')) 30 | -------------------------------------------------------------------------------- /test/nlx/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNlx, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('esbuild', 'bun x esbuild')) 16 | it('multiple', _('esbuild --version', 'bun x esbuild --version')) 17 | -------------------------------------------------------------------------------- /test/nlx/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNlx, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('esbuild', 'npx esbuild')) 16 | it('multiple', _('esbuild --version', 'npx esbuild --version')) 17 | -------------------------------------------------------------------------------- /test/nlx/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNlx, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('esbuild', 'pnpm dlx esbuild')) 16 | it('multiple', _('esbuild --version', 'pnpm dlx esbuild --version')) 17 | -------------------------------------------------------------------------------- /test/nlx/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNlx, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('esbuild', 'npx esbuild')) 16 | it('multiple', _('esbuild --version', 'npx esbuild --version')) 17 | -------------------------------------------------------------------------------- /test/nlx/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNlx, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNlx(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('esbuild', 'yarn dlx esbuild')) 16 | it('multiple', _('esbuild --version', 'yarn dlx esbuild --version')) 17 | -------------------------------------------------------------------------------- /test/nr/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNr, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'bun run start')) 16 | 17 | it('script', _('dev', 'bun run dev')) 18 | 19 | it('script with arguments', _('build --watch -o', 'bun run build --watch -o')) 20 | 21 | it('colon', _('build:dev', 'bun run build:dev')) 22 | -------------------------------------------------------------------------------- /test/nr/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNr, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'npm run start')) 16 | 17 | it('if-present', _('test --if-present', 'npm run --if-present test')) 18 | 19 | it('script', _('dev', 'npm run dev')) 20 | 21 | it('script with arguments', _('build --watch -o', 'npm run build -- --watch -o')) 22 | 23 | it('colon', _('build:dev', 'npm run build:dev')) 24 | -------------------------------------------------------------------------------- /test/nr/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNr, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'pnpm run start')) 16 | 17 | it('if-present', _('test --if-present', 'pnpm run --if-present test')) 18 | 19 | it('script', _('dev', 'pnpm run dev')) 20 | 21 | it('script with arguments', _('build --watch -o', 'pnpm run build --watch -o')) 22 | 23 | it('colon', _('build:dev', 'pnpm run build:dev')) 24 | -------------------------------------------------------------------------------- /test/nr/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNr, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNr(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn run start')) 16 | 17 | it('if-present', _('test --if-present', 'yarn run --if-present test')) 18 | 19 | it('script', _('dev', 'yarn run dev')) 20 | 21 | it('script with arguments', _('build --watch -o', 'yarn run build --watch -o')) 22 | 23 | it('colon', _('build:dev', 'yarn run build:dev')) 24 | -------------------------------------------------------------------------------- /test/nr/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNr, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNr(agent, arg.split(/\s/g).filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn run start')) 16 | 17 | it('if-present', _('test --if-present', 'yarn run --if-present test')) 18 | 19 | it('script', _('dev', 'yarn run dev')) 20 | 21 | it('script with arguments', _('build --watch -o', 'yarn run build --watch -o')) 22 | 23 | it('colon', _('build:dev', 'yarn run build:dev')) 24 | -------------------------------------------------------------------------------- /test/nun/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNun, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('axios', 'bun remove axios')) 16 | 17 | it('multiple', _('eslint @types/node', 'bun remove eslint @types/node')) 18 | 19 | it('global', _('eslint ni -g', 'bun remove -g eslint ni')) 20 | -------------------------------------------------------------------------------- /test/nun/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNun, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('axios', 'npm uninstall axios')) 16 | 17 | it('multiple', _('eslint @types/node', 'npm uninstall eslint @types/node')) 18 | 19 | it('-D', _('eslint @types/node -D', 'npm uninstall eslint @types/node -D')) 20 | 21 | it('global', _('eslint -g', 'npm uninstall -g eslint')) 22 | -------------------------------------------------------------------------------- /test/nun/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNun, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single add', _('axios', 'pnpm remove axios')) 16 | 17 | it('multiple', _('eslint @types/node', 'pnpm remove eslint @types/node')) 18 | 19 | it('-D', _('-D eslint @types/node', 'pnpm remove -D eslint @types/node')) 20 | 21 | it('global', _('eslint -g', 'pnpm remove --global eslint')) 22 | -------------------------------------------------------------------------------- /test/nun/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNun, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single uninstall', _('axios', 'yarn remove axios')) 16 | 17 | it('multiple', _('eslint @types/node', 'yarn remove eslint @types/node')) 18 | 19 | it('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D')) 20 | 21 | it('global', _('eslint ni -g', 'yarn global remove eslint ni')) 22 | -------------------------------------------------------------------------------- /test/nun/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNun, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNun(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('single add', _('axios', 'yarn remove axios')) 16 | 17 | it('multiple', _('eslint @types/node', 'yarn remove eslint @types/node')) 18 | 19 | it('-D', _('eslint @types/node -D', 'yarn remove eslint @types/node -D')) 20 | 21 | it('global', _('eslint ni -g', 'npm uninstall -g eslint ni')) 22 | -------------------------------------------------------------------------------- /test/nup/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNup, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'bun' 5 | function _(arg: string, expected: string | null) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it.fails('empty', _('', null)) 16 | it.fails('interactive', _('-i', null)) 17 | it.fails('interactive latest', _('-i --latest', null)) 18 | -------------------------------------------------------------------------------- /test/nup/npm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNup, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'npm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'npm update')) 16 | -------------------------------------------------------------------------------- /test/nup/pnpm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNup, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'pnpm' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'pnpm update')) 16 | 17 | it('interactive', _('-i', 'pnpm update -i')) 18 | 19 | it('interactive latest', _('-i --latest', 'pnpm update -i --latest')) 20 | -------------------------------------------------------------------------------- /test/nup/yarn.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNup, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn upgrade')) 16 | 17 | it('interactive', _('-i', 'yarn upgrade-interactive')) 18 | 19 | it('interactive latest', _('-i --latest', 'yarn upgrade-interactive --latest')) 20 | -------------------------------------------------------------------------------- /test/nup/yarn@berry.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { parseNup, serializeCommand } from '../../src/commands' 3 | 4 | const agent = 'yarn@berry' 5 | function _(arg: string, expected: string) { 6 | return async () => { 7 | expect( 8 | serializeCommand(await parseNup(agent, arg.split(' ').filter(Boolean))), 9 | ).toBe( 10 | expected, 11 | ) 12 | } 13 | } 14 | 15 | it('empty', _('', 'yarn up')) 16 | 17 | it('interactive', _('-i', 'yarn up -i')) 18 | -------------------------------------------------------------------------------- /test/programmatic/__snapshots__/detect.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`lockfile > bun 1`] = `"bun"`; 4 | 5 | exports[`lockfile > npm 1`] = `"npm"`; 6 | 7 | exports[`lockfile > pnpm 1`] = `"pnpm"`; 8 | 9 | exports[`lockfile > pnpm@6 1`] = `"pnpm"`; 10 | 11 | exports[`lockfile > unknown 1`] = `undefined`; 12 | 13 | exports[`lockfile > yarn 1`] = `"yarn"`; 14 | 15 | exports[`lockfile > yarn@berry 1`] = `"yarn"`; 16 | 17 | exports[`packager > bun 1`] = `"bun"`; 18 | 19 | exports[`packager > npm 1`] = `"npm"`; 20 | 21 | exports[`packager > pnpm 1`] = `"pnpm"`; 22 | 23 | exports[`packager > pnpm@6 1`] = `"pnpm@6"`; 24 | 25 | exports[`packager > unknown 1`] = `undefined`; 26 | 27 | exports[`packager > yarn 1`] = `"yarn"`; 28 | 29 | exports[`packager > yarn@berry 1`] = `"yarn@berry"`; 30 | -------------------------------------------------------------------------------- /test/programmatic/__snapshots__/runCli.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`debug mode > should return command results in plain text format 1`] = `"npm i @antfu/ni"`; 4 | 5 | exports[`lockfile > bun > na 1`] = `"bun"`; 6 | 7 | exports[`lockfile > bun > na run foo 1`] = `"bun run foo"`; 8 | 9 | exports[`lockfile > bun > ni --frozen 1`] = `"bun install --frozen-lockfile"`; 10 | 11 | exports[`lockfile > bun > ni -g foo 1`] = `"npm i -g foo"`; 12 | 13 | exports[`lockfile > bun > ni 1`] = `"bun install"`; 14 | 15 | exports[`lockfile > bun > ni foo -D 1`] = `"bun add foo -d"`; 16 | 17 | exports[`lockfile > bun > ni foo 1`] = `"bun add foo"`; 18 | 19 | exports[`lockfile > bun > nlx 1`] = `"bun x foo"`; 20 | 21 | exports[`lockfile > bun > nun -g foo 1`] = `"npm uninstall -g foo"`; 22 | 23 | exports[`lockfile > bun > nun foo 1`] = `"bun remove foo"`; 24 | 25 | exports[`lockfile > bun > nup -i 1`] = `"bun update"`; 26 | 27 | exports[`lockfile > bun > nup 1`] = `"bun update"`; 28 | 29 | exports[`lockfile > npm > na 1`] = `"npm"`; 30 | 31 | exports[`lockfile > npm > na run foo 1`] = `"npm run foo"`; 32 | 33 | exports[`lockfile > npm > ni --frozen 1`] = `"npm ci"`; 34 | 35 | exports[`lockfile > npm > ni -g foo 1`] = `"npm i -g foo"`; 36 | 37 | exports[`lockfile > npm > ni 1`] = `"npm i"`; 38 | 39 | exports[`lockfile > npm > ni foo -D 1`] = `"npm i foo -D"`; 40 | 41 | exports[`lockfile > npm > ni foo 1`] = `"npm i foo"`; 42 | 43 | exports[`lockfile > npm > nlx 1`] = `"npx foo"`; 44 | 45 | exports[`lockfile > npm > nun -g foo 1`] = `"npm uninstall -g foo"`; 46 | 47 | exports[`lockfile > npm > nun foo 1`] = `"npm uninstall foo"`; 48 | 49 | exports[`lockfile > npm > nup -i 1`] = `"Command "upgrade-interactive" is not support by agent "npm""`; 50 | 51 | exports[`lockfile > npm > nup 1`] = `"npm update"`; 52 | 53 | exports[`lockfile > pnpm > na 1`] = `"pnpm"`; 54 | 55 | exports[`lockfile > pnpm > na run foo 1`] = `"pnpm run foo"`; 56 | 57 | exports[`lockfile > pnpm > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 58 | 59 | exports[`lockfile > pnpm > ni -g foo 1`] = `"npm i -g foo"`; 60 | 61 | exports[`lockfile > pnpm > ni 1`] = `"pnpm i"`; 62 | 63 | exports[`lockfile > pnpm > ni foo -D 1`] = `"pnpm add foo -D"`; 64 | 65 | exports[`lockfile > pnpm > ni foo 1`] = `"pnpm add foo"`; 66 | 67 | exports[`lockfile > pnpm > nlx 1`] = `"pnpm dlx foo"`; 68 | 69 | exports[`lockfile > pnpm > nun -g foo 1`] = `"npm uninstall -g foo"`; 70 | 71 | exports[`lockfile > pnpm > nun foo 1`] = `"pnpm remove foo"`; 72 | 73 | exports[`lockfile > pnpm > nup -i 1`] = `"pnpm update -i"`; 74 | 75 | exports[`lockfile > pnpm > nup 1`] = `"pnpm update"`; 76 | 77 | exports[`lockfile > pnpm@6 > na 1`] = `"pnpm"`; 78 | 79 | exports[`lockfile > pnpm@6 > na run foo 1`] = `"pnpm run foo"`; 80 | 81 | exports[`lockfile > pnpm@6 > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 82 | 83 | exports[`lockfile > pnpm@6 > ni -g foo 1`] = `"npm i -g foo"`; 84 | 85 | exports[`lockfile > pnpm@6 > ni 1`] = `"pnpm i"`; 86 | 87 | exports[`lockfile > pnpm@6 > ni foo -D 1`] = `"pnpm add foo -D"`; 88 | 89 | exports[`lockfile > pnpm@6 > ni foo 1`] = `"pnpm add foo"`; 90 | 91 | exports[`lockfile > pnpm@6 > nlx 1`] = `"pnpm dlx foo"`; 92 | 93 | exports[`lockfile > pnpm@6 > nun -g foo 1`] = `"npm uninstall -g foo"`; 94 | 95 | exports[`lockfile > pnpm@6 > nun foo 1`] = `"pnpm remove foo"`; 96 | 97 | exports[`lockfile > pnpm@6 > nup -i 1`] = `"pnpm update -i"`; 98 | 99 | exports[`lockfile > pnpm@6 > nup 1`] = `"pnpm update"`; 100 | 101 | exports[`lockfile > unknown > na 1`] = `"pnpm"`; 102 | 103 | exports[`lockfile > unknown > na run foo 1`] = `"pnpm run foo"`; 104 | 105 | exports[`lockfile > unknown > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 106 | 107 | exports[`lockfile > unknown > ni -g foo 1`] = `"npm i -g foo"`; 108 | 109 | exports[`lockfile > unknown > ni 1`] = `"pnpm i"`; 110 | 111 | exports[`lockfile > unknown > ni foo -D 1`] = `"pnpm add foo -D"`; 112 | 113 | exports[`lockfile > unknown > ni foo 1`] = `"pnpm add foo"`; 114 | 115 | exports[`lockfile > unknown > nlx 1`] = `"pnpm dlx foo"`; 116 | 117 | exports[`lockfile > unknown > nun -g foo 1`] = `"npm uninstall -g foo"`; 118 | 119 | exports[`lockfile > unknown > nun foo 1`] = `"pnpm remove foo"`; 120 | 121 | exports[`lockfile > unknown > nup -i 1`] = `"pnpm update -i"`; 122 | 123 | exports[`lockfile > unknown > nup 1`] = `"pnpm update"`; 124 | 125 | exports[`lockfile > yarn > na 1`] = `"yarn"`; 126 | 127 | exports[`lockfile > yarn > na run foo 1`] = `"yarn run foo"`; 128 | 129 | exports[`lockfile > yarn > ni --frozen 1`] = `"yarn install --frozen-lockfile"`; 130 | 131 | exports[`lockfile > yarn > ni -g foo 1`] = `"npm i -g foo"`; 132 | 133 | exports[`lockfile > yarn > ni 1`] = `"yarn install"`; 134 | 135 | exports[`lockfile > yarn > ni foo -D 1`] = `"yarn add foo -D"`; 136 | 137 | exports[`lockfile > yarn > ni foo 1`] = `"yarn add foo"`; 138 | 139 | exports[`lockfile > yarn > nlx 1`] = `"npx foo"`; 140 | 141 | exports[`lockfile > yarn > nun -g foo 1`] = `"npm uninstall -g foo"`; 142 | 143 | exports[`lockfile > yarn > nun foo 1`] = `"yarn remove foo"`; 144 | 145 | exports[`lockfile > yarn > nup -i 1`] = `"yarn upgrade-interactive"`; 146 | 147 | exports[`lockfile > yarn > nup 1`] = `"yarn upgrade"`; 148 | 149 | exports[`lockfile > yarn@berry > na 1`] = `"yarn"`; 150 | 151 | exports[`lockfile > yarn@berry > na run foo 1`] = `"yarn run foo"`; 152 | 153 | exports[`lockfile > yarn@berry > ni --frozen 1`] = `"yarn install --frozen-lockfile"`; 154 | 155 | exports[`lockfile > yarn@berry > ni -g foo 1`] = `"npm i -g foo"`; 156 | 157 | exports[`lockfile > yarn@berry > ni 1`] = `"yarn install"`; 158 | 159 | exports[`lockfile > yarn@berry > ni foo -D 1`] = `"yarn add foo -D"`; 160 | 161 | exports[`lockfile > yarn@berry > ni foo 1`] = `"yarn add foo"`; 162 | 163 | exports[`lockfile > yarn@berry > nlx 1`] = `"npx foo"`; 164 | 165 | exports[`lockfile > yarn@berry > nun -g foo 1`] = `"npm uninstall -g foo"`; 166 | 167 | exports[`lockfile > yarn@berry > nun foo 1`] = `"yarn remove foo"`; 168 | 169 | exports[`lockfile > yarn@berry > nup -i 1`] = `"yarn upgrade-interactive"`; 170 | 171 | exports[`lockfile > yarn@berry > nup 1`] = `"yarn upgrade"`; 172 | 173 | exports[`packager > bun > na 1`] = `"bun"`; 174 | 175 | exports[`packager > bun > na run foo 1`] = `"bun run foo"`; 176 | 177 | exports[`packager > bun > ni --frozen 1`] = `"bun install --frozen-lockfile"`; 178 | 179 | exports[`packager > bun > ni -g foo 1`] = `"npm i -g foo"`; 180 | 181 | exports[`packager > bun > ni 1`] = `"bun install"`; 182 | 183 | exports[`packager > bun > ni foo -D 1`] = `"bun add foo -d"`; 184 | 185 | exports[`packager > bun > ni foo 1`] = `"bun add foo"`; 186 | 187 | exports[`packager > bun > nlx 1`] = `"bun x foo"`; 188 | 189 | exports[`packager > bun > nun -g foo 1`] = `"npm uninstall -g foo"`; 190 | 191 | exports[`packager > bun > nun foo 1`] = `"bun remove foo"`; 192 | 193 | exports[`packager > bun > nup -i 1`] = `"bun update"`; 194 | 195 | exports[`packager > bun > nup 1`] = `"bun update"`; 196 | 197 | exports[`packager > npm > na 1`] = `"npm"`; 198 | 199 | exports[`packager > npm > na run foo 1`] = `"npm run foo"`; 200 | 201 | exports[`packager > npm > ni --frozen 1`] = `"npm ci"`; 202 | 203 | exports[`packager > npm > ni -g foo 1`] = `"npm i -g foo"`; 204 | 205 | exports[`packager > npm > ni 1`] = `"npm i"`; 206 | 207 | exports[`packager > npm > ni foo -D 1`] = `"npm i foo -D"`; 208 | 209 | exports[`packager > npm > ni foo 1`] = `"npm i foo"`; 210 | 211 | exports[`packager > npm > nlx 1`] = `"npx foo"`; 212 | 213 | exports[`packager > npm > nun -g foo 1`] = `"npm uninstall -g foo"`; 214 | 215 | exports[`packager > npm > nun foo 1`] = `"npm uninstall foo"`; 216 | 217 | exports[`packager > npm > nup -i 1`] = `"Command "upgrade-interactive" is not support by agent "npm""`; 218 | 219 | exports[`packager > npm > nup 1`] = `"npm update"`; 220 | 221 | exports[`packager > pnpm > na 1`] = `"pnpm"`; 222 | 223 | exports[`packager > pnpm > na run foo 1`] = `"pnpm run foo"`; 224 | 225 | exports[`packager > pnpm > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 226 | 227 | exports[`packager > pnpm > ni -g foo 1`] = `"npm i -g foo"`; 228 | 229 | exports[`packager > pnpm > ni 1`] = `"pnpm i"`; 230 | 231 | exports[`packager > pnpm > ni foo -D 1`] = `"pnpm add foo -D"`; 232 | 233 | exports[`packager > pnpm > ni foo 1`] = `"pnpm add foo"`; 234 | 235 | exports[`packager > pnpm > nlx 1`] = `"pnpm dlx foo"`; 236 | 237 | exports[`packager > pnpm > nun -g foo 1`] = `"npm uninstall -g foo"`; 238 | 239 | exports[`packager > pnpm > nun foo 1`] = `"pnpm remove foo"`; 240 | 241 | exports[`packager > pnpm > nup -i 1`] = `"pnpm update -i"`; 242 | 243 | exports[`packager > pnpm > nup 1`] = `"pnpm update"`; 244 | 245 | exports[`packager > pnpm@6 > na 1`] = `"pnpm"`; 246 | 247 | exports[`packager > pnpm@6 > na run foo 1`] = `"pnpm run foo"`; 248 | 249 | exports[`packager > pnpm@6 > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 250 | 251 | exports[`packager > pnpm@6 > ni -g foo 1`] = `"npm i -g foo"`; 252 | 253 | exports[`packager > pnpm@6 > ni 1`] = `"pnpm i"`; 254 | 255 | exports[`packager > pnpm@6 > ni foo -D 1`] = `"pnpm add foo -D"`; 256 | 257 | exports[`packager > pnpm@6 > ni foo 1`] = `"pnpm add foo"`; 258 | 259 | exports[`packager > pnpm@6 > nlx 1`] = `"pnpm dlx foo"`; 260 | 261 | exports[`packager > pnpm@6 > nun -g foo 1`] = `"npm uninstall -g foo"`; 262 | 263 | exports[`packager > pnpm@6 > nun foo 1`] = `"pnpm remove foo"`; 264 | 265 | exports[`packager > pnpm@6 > nup -i 1`] = `"pnpm update -i"`; 266 | 267 | exports[`packager > pnpm@6 > nup 1`] = `"pnpm update"`; 268 | 269 | exports[`packager > unknown > na 1`] = `"pnpm"`; 270 | 271 | exports[`packager > unknown > na run foo 1`] = `"pnpm run foo"`; 272 | 273 | exports[`packager > unknown > ni --frozen 1`] = `"pnpm i --frozen-lockfile"`; 274 | 275 | exports[`packager > unknown > ni -g foo 1`] = `"npm i -g foo"`; 276 | 277 | exports[`packager > unknown > ni 1`] = `"pnpm i"`; 278 | 279 | exports[`packager > unknown > ni foo -D 1`] = `"pnpm add foo -D"`; 280 | 281 | exports[`packager > unknown > ni foo 1`] = `"pnpm add foo"`; 282 | 283 | exports[`packager > unknown > nlx 1`] = `"pnpm dlx foo"`; 284 | 285 | exports[`packager > unknown > nun -g foo 1`] = `"npm uninstall -g foo"`; 286 | 287 | exports[`packager > unknown > nun foo 1`] = `"pnpm remove foo"`; 288 | 289 | exports[`packager > unknown > nup -i 1`] = `"pnpm update -i"`; 290 | 291 | exports[`packager > unknown > nup 1`] = `"pnpm update"`; 292 | 293 | exports[`packager > yarn > na 1`] = `"yarn"`; 294 | 295 | exports[`packager > yarn > na run foo 1`] = `"yarn run foo"`; 296 | 297 | exports[`packager > yarn > ni --frozen 1`] = `"yarn install --frozen-lockfile"`; 298 | 299 | exports[`packager > yarn > ni -g foo 1`] = `"npm i -g foo"`; 300 | 301 | exports[`packager > yarn > ni 1`] = `"yarn install"`; 302 | 303 | exports[`packager > yarn > ni foo -D 1`] = `"yarn add foo -D"`; 304 | 305 | exports[`packager > yarn > ni foo 1`] = `"yarn add foo"`; 306 | 307 | exports[`packager > yarn > nlx 1`] = `"npx foo"`; 308 | 309 | exports[`packager > yarn > nun -g foo 1`] = `"npm uninstall -g foo"`; 310 | 311 | exports[`packager > yarn > nun foo 1`] = `"yarn remove foo"`; 312 | 313 | exports[`packager > yarn > nup -i 1`] = `"yarn upgrade-interactive"`; 314 | 315 | exports[`packager > yarn > nup 1`] = `"yarn upgrade"`; 316 | 317 | exports[`packager > yarn@berry > na 1`] = `"yarn"`; 318 | 319 | exports[`packager > yarn@berry > na run foo 1`] = `"yarn run foo"`; 320 | 321 | exports[`packager > yarn@berry > ni --frozen 1`] = `"yarn install --immutable"`; 322 | 323 | exports[`packager > yarn@berry > ni -g foo 1`] = `"npm i -g foo"`; 324 | 325 | exports[`packager > yarn@berry > ni 1`] = `"yarn install"`; 326 | 327 | exports[`packager > yarn@berry > ni foo -D 1`] = `"yarn add foo -D"`; 328 | 329 | exports[`packager > yarn@berry > ni foo 1`] = `"yarn add foo"`; 330 | 331 | exports[`packager > yarn@berry > nlx 1`] = `"yarn dlx foo"`; 332 | 333 | exports[`packager > yarn@berry > nun -g foo 1`] = `"npm uninstall -g foo"`; 334 | 335 | exports[`packager > yarn@berry > nun foo 1`] = `"yarn remove foo"`; 336 | 337 | exports[`packager > yarn@berry > nup -i 1`] = `"yarn up -i"`; 338 | 339 | exports[`packager > yarn@berry > nup 1`] = `"yarn up"`; 340 | -------------------------------------------------------------------------------- /test/programmatic/detect.spec.ts: -------------------------------------------------------------------------------- 1 | import type { MockInstance } from 'vitest' 2 | import fs from 'node:fs/promises' 3 | import { tmpdir } from 'node:os' 4 | import path from 'node:path' 5 | import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' 6 | import { AGENTS, detect } from '../../src' 7 | 8 | let basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance 9 | 10 | function detectTest(fixture: string, agent: string) { 11 | return async () => { 12 | const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-')) 13 | const dir = path.join(__dirname, '..', 'fixtures', fixture, agent) 14 | await fs.cp(dir, cwd, { recursive: true }) 15 | 16 | expect(await detect({ programmatic: true, cwd })).toMatchSnapshot() 17 | } 18 | } 19 | 20 | beforeAll(() => { 21 | basicLog = vi.spyOn(console, 'log') 22 | warnLog = vi.spyOn(console, 'warn') 23 | errorLog = vi.spyOn(console, 'error') 24 | infoLog = vi.spyOn(console, 'info') 25 | }) 26 | 27 | afterAll(() => { 28 | vi.resetAllMocks() 29 | }) 30 | 31 | const agents = [...AGENTS, 'unknown'] 32 | const fixtures = ['lockfile', 'packager'] 33 | const skippedAgents = ['deno'] 34 | 35 | // matrix testing of: fixtures x agents 36 | fixtures.forEach(fixture => describe(fixture, () => agents.forEach((agent) => { 37 | if (skippedAgents.includes(agent)) 38 | return it.skip(`skipped for ${agent}`, () => {}) 39 | 40 | it(agent, detectTest(fixture, agent)) 41 | 42 | it('no logs', () => { 43 | expect(basicLog).not.toHaveBeenCalled() 44 | expect(warnLog).not.toHaveBeenCalled() 45 | expect(errorLog).not.toHaveBeenCalled() 46 | expect(infoLog).not.toHaveBeenCalled() 47 | }) 48 | }))) 49 | -------------------------------------------------------------------------------- /test/programmatic/runCli.spec.ts: -------------------------------------------------------------------------------- 1 | import type { MockInstance } from 'vitest' 2 | import type { Runner } from '../../src' 3 | import fs from 'node:fs/promises' 4 | import { tmpdir } from 'node:os' 5 | import path from 'node:path' 6 | import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' 7 | 8 | import { AGENTS, parseNa, parseNi, parseNlx, parseNun, parseNup, runCli } from '../../src' 9 | 10 | let basicLog: MockInstance, errorLog: MockInstance, warnLog: MockInstance, infoLog: MockInstance 11 | 12 | function runCliTest(fixtureName: string, agent: string, runner: Runner, args: string[]) { 13 | return async () => { 14 | const cwd = await fs.mkdtemp(path.join(tmpdir(), 'ni-')) 15 | const fixture = path.join(__dirname, '..', 'fixtures', fixtureName, agent) 16 | await fs.cp(fixture, cwd, { recursive: true }) 17 | 18 | await runCli( 19 | async (agent, _, ctx) => { 20 | // we override the args to be test specific 21 | return runner(agent, args, ctx) 22 | }, 23 | { 24 | programmatic: true, 25 | cwd, 26 | args, 27 | }, 28 | ).catch((e) => { 29 | // it will always throw if ezspawn is mocked 30 | if (e.command) 31 | expect(e.command).toMatchSnapshot() 32 | else 33 | expect(e.message).toMatchSnapshot() 34 | }) 35 | } 36 | } 37 | 38 | beforeAll(() => { 39 | basicLog = vi.spyOn(console, 'log') 40 | warnLog = vi.spyOn(console, 'warn') 41 | errorLog = vi.spyOn(console, 'error') 42 | infoLog = vi.spyOn(console, 'info') 43 | 44 | vi.mock('tinyexec', async (importOriginal) => { 45 | const mod = await importOriginal() as any 46 | return { 47 | ...mod, 48 | x: (cmd: string, args?: string[]) => { 49 | // break execution flow for easier snapshotting 50 | // eslint-disable-next-line no-throw-literal 51 | throw { command: [cmd, ...(args ?? [])].join(' ') } 52 | }, 53 | } 54 | }) 55 | }) 56 | 57 | afterAll(() => { 58 | vi.resetAllMocks() 59 | }) 60 | 61 | const agents = [...AGENTS, 'unknown'] 62 | const fixtures = ['lockfile', 'packager'] 63 | const skippedAgents = ['deno'] 64 | 65 | // matrix testing of: fixtures x agents x commands 66 | fixtures.forEach(fixture => describe(fixture, () => agents.forEach(agent => describe(agent, () => { 67 | if (skippedAgents.includes(agent)) 68 | return it.skip(`skipped for ${agent}`, () => {}) 69 | 70 | /** na */ 71 | it('na', runCliTest(fixture, agent, parseNa, [])) 72 | it('na run foo', runCliTest(fixture, agent, parseNa, ['run', 'foo'])) 73 | 74 | /** ni */ 75 | it('ni', runCliTest(fixture, agent, parseNi, [])) 76 | it('ni foo', runCliTest(fixture, agent, parseNi, ['foo'])) 77 | it('ni foo -D', runCliTest(fixture, agent, parseNi, ['foo', '-D'])) 78 | it('ni --frozen', runCliTest(fixture, agent, parseNi, ['--frozen'])) 79 | it('ni -g foo', runCliTest(fixture, agent, parseNi, ['-g', 'foo'])) 80 | 81 | /** nlx */ 82 | it('nlx', runCliTest(fixture, agent, parseNlx, ['foo'])) 83 | 84 | /** nup */ 85 | it('nup', runCliTest(fixture, agent, parseNup, [])) 86 | it('nup -i', runCliTest(fixture, agent, parseNup, ['-i'])) 87 | 88 | /** nun */ 89 | it('nun foo', runCliTest(fixture, agent, parseNun, ['foo'])) 90 | it('nun -g foo', runCliTest(fixture, agent, parseNun, ['-g', 'foo'])) 91 | 92 | it('no logs', () => { 93 | expect(basicLog).not.toHaveBeenCalled() 94 | expect(warnLog).not.toHaveBeenCalled() 95 | expect(errorLog).not.toHaveBeenCalled() 96 | expect(infoLog).not.toHaveBeenCalled() 97 | }) 98 | })))) 99 | 100 | // https://github.com/antfu-collective/ni/issues/266 101 | describe('debug mode', () => { 102 | beforeAll(() => basicLog.mockClear()) 103 | 104 | it('ni', runCliTest('lockfile', 'npm', parseNi, ['@antfu/ni', '?'])) 105 | it('should return command results in plain text format', () => { 106 | expect(basicLog).toHaveBeenCalled() 107 | 108 | expect(basicLog.mock.calls[0][0]).toMatchSnapshot() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /test/runner/runCli.test.ts: -------------------------------------------------------------------------------- 1 | import type { Runner } from '../../src' 2 | import { afterEach, describe, expect, it, vi } from 'vitest' 3 | import { runCli } from '../../src' 4 | 5 | // Mock detect to see what options are passed to it 6 | const mocks = vi.hoisted(() => ({ 7 | detectSpy: vi.fn(() => Promise.resolve('npm')), 8 | })) 9 | vi.mock('../../src/detect', () => ({ 10 | detect: mocks.detectSpy, 11 | })) 12 | 13 | const baseRunFn: Runner = async () => { 14 | return undefined 15 | } 16 | 17 | describe('runCli', () => { 18 | afterEach(() => { 19 | vi.clearAllMocks() 20 | vi.unstubAllEnvs() 21 | }) 22 | 23 | it('run without errors', async () => { 24 | const result = await runCli(baseRunFn, {}) 25 | expect(result).toBe(undefined) 26 | }) 27 | 28 | it('handle errors in programmatic mode', async () => { 29 | await expect( 30 | runCli(() => { 31 | throw new Error('test error') 32 | }, { programmatic: true }), 33 | ).rejects.toThrow('test error') 34 | }) 35 | 36 | it('calls detect with the correct options', async () => { 37 | await runCli(baseRunFn) 38 | expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, cwd: expect.any(String) }) 39 | }) 40 | 41 | it('detects environment options', async () => { 42 | vi.stubEnv('NI_AUTO_INSTALL', 'true') 43 | await runCli(baseRunFn) 44 | expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, cwd: expect.any(String) }) 45 | }) 46 | 47 | it('accept options as input', async () => { 48 | await runCli(baseRunFn, { autoInstall: true, programmatic: true }) 49 | expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: true, programmatic: true, cwd: expect.any(String) }) 50 | }) 51 | 52 | it('merges inputs and environment prioritizing inputs', async () => { 53 | vi.stubEnv('NI_AUTO_INSTALL', 'true') 54 | await runCli(baseRunFn, { autoInstall: false, programmatic: true }) 55 | expect(mocks.detectSpy).toHaveBeenCalledWith({ autoInstall: false, programmatic: true, cwd: expect.any(String) }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["esnext"], 5 | "module": "esnext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | // Disable global ni config in test to make the results more predictable 5 | process.env.NI_CONFIG_FILE = 'false' 6 | 7 | export default defineConfig({ 8 | test: { 9 | 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------