pkgmgr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # pkgmgr
2
+
3
+ A minimal CLI that forwards commands to whichever package manager you're already using.
4
+
5
+ ## The Problem
6
+
7
+ You're writing a script, a tool, or documentation that needs to run package manager commands. But different users use different package managers (npm, pnpm, yarn, bun), and you don't want to:
8
+
9
+ - Hardcode a specific package manager
10
+ - Ask users to modify your script
11
+ - Set up complex workspace configurations
12
+
13
+ ## What pkgmgr Does
14
+
15
+ pkgmgr detects which package manager invoked the current process and forwards your command to it verbatim. It's a best-effort user-intent propagation tool.
16
+
17
+ ```bash
18
+ pkgmgr install # runs: <detected-pm> install
19
+ pkgmgr add react # runs: <detected-pm> add react
20
+ pkgmgr remove lodash # runs: <detected-pm> remove lodash
21
+ pkgmgr run build # runs: <detected-pm> run build
22
+ pkgmgr exec vitest # runs: <detected-pm> exec vitest
23
+ ```
24
+
25
+ ## Available Binaries
26
+
27
+ | Binary | Fallback |
28
+ |--------|----------|
29
+ | `pkgmgr` | npm |
30
+ | `pkgmgr-bun` | bun |
31
+ | `pkgmgr-pnpm` | pnpm |
32
+ | `pkgmgr-yarn` | yarn |
33
+
34
+ All binaries use the same detection logic. The only difference is which package manager is used when detection fails (no `PKGMGR` env var and no `npm_config_user_agent`).
35
+
36
+ ## Detection Logic
37
+
38
+ pkgmgr determines which package manager to use in this order:
39
+
40
+ 1. **`PKGMGR` environment variable** — If set to a supported value, use it
41
+ 2. **`npm_config_user_agent`** — Parse the first token (e.g., `pnpm/8.0.0` → `pnpm`)
42
+ 3. **Fallback** — Use `npm`
43
+
44
+ This is heuristic detection, not verification. pkgmgr trusts the environment.
45
+
46
+ ## Supported Package Managers
47
+
48
+ - npm
49
+ - pnpm
50
+ - yarn
51
+ - bun
52
+
53
+ ## Overriding Detection
54
+
55
+ Set the `PKGMGR` environment variable to force a specific package manager:
56
+
57
+ ```bash
58
+ PKGMGR=pnpm pkgmgr add zod
59
+ ```
60
+
61
+ ## Usage Examples
62
+
63
+ ```bash
64
+ # Install dependencies (default command if no args)
65
+ pkgmgr
66
+
67
+ # Add a package
68
+ pkgmgr add express
69
+
70
+ # Remove a package
71
+ pkgmgr remove lodash
72
+
73
+ # Run a script
74
+ pkgmgr run test
75
+
76
+ # Run with arguments
77
+ pkgmgr run build -- --watch
78
+
79
+ # Execute a binary
80
+ pkgmgr exec tsc --version
81
+
82
+ # Override detection
83
+ PKGMGR=yarn pkgmgr add react
84
+ ```
85
+
86
+ ## When to Use This
87
+
88
+ **Good fit:**
89
+
90
+ - Scripts that should work regardless of user's package manager choice
91
+ - Documentation that doesn't want to prescribe a specific tool
92
+ - Tools that need to install dependencies as part of setup
93
+
94
+ **Not a good fit:**
95
+
96
+ - CI pipelines where you control the environment (just use your package manager directly)
97
+ - Situations requiring specific package manager features
98
+ - Contexts where command translation between package managers is needed
99
+
100
+ ## Known Limitations
101
+
102
+ - **No command translation** — `pkgmgr add` is passed verbatim; if you're using npm, you may need `pkgmgr install` instead. This tool does not normalize commands between package managers.
103
+ - **Corepack** — pkgmgr does not interact with Corepack. If Corepack intercepts your package manager, that's between you and Corepack.
104
+ - **CI environments** — Detection relies on `npm_config_user_agent`, which may not be set in all CI contexts. Use `PKGMGR` explicitly if needed.
105
+ - **Wrapper scripts** — If your package manager is wrapped or aliased, detection may not work as expected.
106
+ - **Nested installs** — If you're running pkgmgr from within another package manager's lifecycle script, detection reflects the outer package manager.
107
+ - **No lockfile inspection** — pkgmgr does not look at lockfiles to determine package manager. It trusts the runtime environment only.
108
+
109
+ ## Why Not npx?
110
+
111
+ Running `npx pkgmgr add react` defeats the purpose. npx is part of npm, so it sets `npm_config_user_agent` to npm. pkgmgr would detect npm and run `npm add react`—regardless of what package manager you actually use.
112
+
113
+ The same applies to other package runners:
114
+ - `npx pkgmgr ...` → detects npm
115
+ - `pnpm dlx pkgmgr ...` → detects pnpm
116
+ - `yarn dlx pkgmgr ...` → detects yarn
117
+ - `bunx pkgmgr ...` → detects bun
118
+
119
+ pkgmgr is meant to be **installed as a dependency** in your project. When users run your scripts through their package manager (e.g., `pnpm run setup`), pkgmgr detects the invoking package manager from within that context.
120
+
121
+ ## Installation
122
+
123
+ ```bash
124
+ npm install pkgmgr
125
+ # or
126
+ pnpm add pkgmgr
127
+ # or
128
+ yarn add pkgmgr
129
+ # or
130
+ bun add pkgmgr
131
+ ```
132
+
133
+ ## License
134
+
135
+ MIT
package/bin/lib.js ADDED
@@ -0,0 +1,47 @@
1
+ const SUPPORTED = new Set(['npm', 'pnpm', 'yarn', 'bun'])
2
+
3
+ export function detectPackageManager(fallback = 'npm') {
4
+ // 1. Explicit override via environment variable
5
+ const envOverride = process.env.PKGMGR
6
+ if (envOverride && SUPPORTED.has(envOverride)) {
7
+ return envOverride
8
+ }
9
+
10
+ // 2. Infer from npm_config_user_agent (set by package managers when running scripts)
11
+ // Format: "npm/10.2.0 node/v20.10.0 darwin arm64"
12
+ const userAgent = process.env.npm_config_user_agent
13
+ if (userAgent) {
14
+ const firstToken = userAgent.split(' ')[0] // e.g. "npm/10.2.0"
15
+ const name = firstToken?.split('/')[0]
16
+ if (name && SUPPORTED.has(name)) {
17
+ return name
18
+ }
19
+ }
20
+
21
+ // 3. Fall back to specified default
22
+ return fallback
23
+ }
24
+
25
+ export async function run(fallback = 'npm') {
26
+ const pm = detectPackageManager(fallback)
27
+ const args = process.argv.slice(2)
28
+
29
+ // Default to 'install' if no arguments provided
30
+ const command = args.length > 0 ? args : ['install']
31
+
32
+ const { spawn } = await import('node:child_process')
33
+
34
+ const child = spawn(pm, command, {
35
+ stdio: 'inherit',
36
+ shell: process.platform === 'win32',
37
+ })
38
+
39
+ child.on('error', (err) => {
40
+ console.error(`pkgmgr: failed to execute ${pm}: ${err.message}`)
41
+ process.exit(1)
42
+ })
43
+
44
+ child.on('close', (code) => {
45
+ process.exit(code ?? 0)
46
+ })
47
+ }
@@ -0,0 +1,128 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { detectPackageManager } from './lib.js'
4
+
5
+ function setupEnv(overrides = {}) {
6
+ const original = { ...process.env }
7
+
8
+ delete process.env.PKGMGR
9
+ delete process.env.npm_config_user_agent
10
+
11
+ Object.assign(process.env, overrides)
12
+
13
+ return {
14
+ [Symbol.dispose]() {
15
+ process.env = original
16
+ },
17
+ }
18
+ }
19
+
20
+ // PKGMGR environment variable
21
+
22
+ test('uses PKGMGR=npm when set', () => {
23
+ using _env = setupEnv({ PKGMGR: 'npm' })
24
+ assert.strictEqual(detectPackageManager(), 'npm')
25
+ })
26
+
27
+ test('uses PKGMGR=pnpm when set', () => {
28
+ using _env = setupEnv({ PKGMGR: 'pnpm' })
29
+ assert.strictEqual(detectPackageManager(), 'pnpm')
30
+ })
31
+
32
+ test('uses PKGMGR=yarn when set', () => {
33
+ using _env = setupEnv({ PKGMGR: 'yarn' })
34
+ assert.strictEqual(detectPackageManager(), 'yarn')
35
+ })
36
+
37
+ test('uses PKGMGR=bun when set', () => {
38
+ using _env = setupEnv({ PKGMGR: 'bun' })
39
+ assert.strictEqual(detectPackageManager(), 'bun')
40
+ })
41
+
42
+ test('ignores unsupported PKGMGR value and falls back to npm', () => {
43
+ using _env = setupEnv({ PKGMGR: 'unsupported' })
44
+ assert.strictEqual(detectPackageManager(), 'npm')
45
+ })
46
+
47
+ test('PKGMGR takes priority over npm_config_user_agent', () => {
48
+ using _env = setupEnv({
49
+ PKGMGR: 'yarn',
50
+ npm_config_user_agent: 'pnpm/8.0.0 node/v20.0.0',
51
+ })
52
+ assert.strictEqual(detectPackageManager(), 'yarn')
53
+ })
54
+
55
+ // npm_config_user_agent detection
56
+
57
+ test('detects npm from user agent', () => {
58
+ using _env = setupEnv({
59
+ npm_config_user_agent: 'npm/10.2.0 node/v20.10.0 darwin arm64',
60
+ })
61
+ assert.strictEqual(detectPackageManager(), 'npm')
62
+ })
63
+
64
+ test('detects pnpm from user agent', () => {
65
+ using _env = setupEnv({
66
+ npm_config_user_agent: 'pnpm/8.15.0 npm/? node/v20.10.0',
67
+ })
68
+ assert.strictEqual(detectPackageManager(), 'pnpm')
69
+ })
70
+
71
+ test('detects yarn from user agent', () => {
72
+ using _env = setupEnv({
73
+ npm_config_user_agent: 'yarn/4.0.0 npm/? node/v20.10.0',
74
+ })
75
+ assert.strictEqual(detectPackageManager(), 'yarn')
76
+ })
77
+
78
+ test('detects bun from user agent', () => {
79
+ using _env = setupEnv({ npm_config_user_agent: 'bun/1.0.0 node/v20.10.0' })
80
+ assert.strictEqual(detectPackageManager(), 'bun')
81
+ })
82
+
83
+ test('falls back when user agent has unsupported package manager', () => {
84
+ using _env = setupEnv({ npm_config_user_agent: 'unknown/1.0.0 node/v20.0.0' })
85
+ assert.strictEqual(detectPackageManager(), 'npm')
86
+ })
87
+
88
+ test('falls back when user agent is malformed', () => {
89
+ using _env = setupEnv({ npm_config_user_agent: 'malformed-string' })
90
+ assert.strictEqual(detectPackageManager(), 'npm')
91
+ })
92
+
93
+ test('falls back when user agent is empty', () => {
94
+ using _env = setupEnv({ npm_config_user_agent: '' })
95
+ assert.strictEqual(detectPackageManager(), 'npm')
96
+ })
97
+
98
+ // Fallback behavior
99
+
100
+ test('uses npm as default fallback when no env is set', () => {
101
+ using _env = setupEnv()
102
+ assert.strictEqual(detectPackageManager(), 'npm')
103
+ })
104
+
105
+ test('uses pnpm fallback when specified and no env is set', () => {
106
+ using _env = setupEnv()
107
+ assert.strictEqual(detectPackageManager('pnpm'), 'pnpm')
108
+ })
109
+
110
+ test('uses bun fallback when specified and no env is set', () => {
111
+ using _env = setupEnv()
112
+ assert.strictEqual(detectPackageManager('bun'), 'bun')
113
+ })
114
+
115
+ test('uses yarn fallback when specified and no env is set', () => {
116
+ using _env = setupEnv()
117
+ assert.strictEqual(detectPackageManager('yarn'), 'yarn')
118
+ })
119
+
120
+ test('custom fallback is ignored when PKGMGR is set', () => {
121
+ using _env = setupEnv({ PKGMGR: 'npm' })
122
+ assert.strictEqual(detectPackageManager('pnpm'), 'npm')
123
+ })
124
+
125
+ test('custom fallback is ignored when user agent is present', () => {
126
+ using _env = setupEnv({ npm_config_user_agent: 'yarn/4.0.0 node/v20.0.0' })
127
+ assert.strictEqual(detectPackageManager('pnpm'), 'yarn')
128
+ })
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './lib.js'
3
+ run('bun')
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './lib.js'
3
+ run('pnpm')
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './lib.js'
3
+ run('yarn')
package/bin/pkgmgr.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './lib.js'
3
+ run('npm')
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "pkgmgr",
3
+ "version": "1.0.0",
4
+ "description": "Forward commands to the package manager you're already using",
5
+ "bin": {
6
+ "pkgmgr": "./bin/pkgmgr.js",
7
+ "pkgmgr-bun": "./bin/pkgmgr-bun.js",
8
+ "pkgmgr-pnpm": "./bin/pkgmgr-pnpm.js",
9
+ "pkgmgr-yarn": "./bin/pkgmgr-yarn.js"
10
+ },
11
+ "files": [
12
+ "bin"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "test": "node --test bin/lib.test.js"
19
+ },
20
+ "keywords": [
21
+ "npm",
22
+ "pnpm",
23
+ "yarn",
24
+ "bun",
25
+ "package-manager",
26
+ "cli"
27
+ ],
28
+ "author": "Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com/)",
29
+ "license": "MIT",
30
+ "type": "module",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/kentcdodds/pkgmgr.git"
34
+ }
35
+ }