@happytoolin/alur 0.0.1

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,440 @@
1
+ # alur
2
+
3
+ ![alur og banner](.github/og-image.png)
4
+
5
+ [![CI](https://github.com/happytoolin/alur/actions/workflows/ci.yml/badge.svg)](https://github.com/happytoolin/alur/actions/workflows/ci.yml)
6
+ [![License: GPLv3](https://img.shields.io/badge/License-GPLv3-4F46E5.svg)](https://www.gnu.org/licenses/gpl-3.0)
7
+ [![npm](https://img.shields.io/npm/v/alur?logo=npm&logoColor=white)](https://www.npmjs.com/package/alur)
8
+ ![npm](https://img.shields.io/badge/npm-supported-CB3837?logo=npm&logoColor=white)
9
+ ![yarn](https://img.shields.io/badge/yarn-supported-2C8EBB?logo=yarn&logoColor=white)
10
+ ![pnpm](https://img.shields.io/badge/pnpm-supported-F69220?logo=pnpm&logoColor=white)
11
+ ![bun](https://img.shields.io/badge/bun-supported-111111?logo=bun&logoColor=white)
12
+ ![deno](https://img.shields.io/badge/deno-supported-000000?logo=deno&logoColor=white)
13
+
14
+ Fast package manager routing for `npm`, `yarn`, `pnpm`, `bun`, and `deno`.
15
+
16
+ `alur` is inspired by Antfu's [`ni`](https://github.com/antfu-collective/ni#readme), but packaged as a single multicall binary with extra shell setup for a `node` shim.
17
+
18
+ `alur` is still beta software and may have bugs.
19
+ The supported interface is the CLI; the Rust crate modules are internal and do not carry a stable API guarantee.
20
+
21
+ One install gives you:
22
+
23
+ - `alur`
24
+ - `ni`, `nr`, `nlx`, `nun`, `nci`, `np`, `ns`
25
+ - `node` shim via `alur init <shell>` (managed launcher)
26
+
27
+ ## Install
28
+
29
+ ### npm (global)
30
+
31
+ ```bash
32
+ npm install -g alur
33
+ alur --version
34
+ ```
35
+
36
+ This installs `alur` and the `ni`-family aliases (`ni`, `nr`, `nlx`, `nun`, `nci`, `np`, `ns`) onto your global npm bin path.
37
+ The `node` shim is only enabled through `alur init <shell>`.
38
+ Under the hood, the npm postinstall downloads the matching native `alur` binary from the GitHub release.
39
+
40
+ ### Homebrew
41
+
42
+ ```bash
43
+ brew tap happytoolin/happytap
44
+ brew install alur
45
+ alur --version
46
+ ```
47
+
48
+ ### Script install (macOS / Linux)
49
+
50
+ ```bash
51
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/happytoolin/alur/releases/latest/download/alur-installer.sh | sh
52
+ ```
53
+
54
+ To pin a specific version:
55
+
56
+ ```bash
57
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/happytoolin/alur/releases/download/v0.0.1/alur-installer.sh | sh
58
+ ```
59
+
60
+ ### Script install (PowerShell)
61
+
62
+ ```powershell
63
+ powershell -ExecutionPolicy Bypass -c "irm https://github.com/happytoolin/alur/releases/latest/download/alur-installer.ps1 | iex"
64
+ ```
65
+
66
+ ### CI / automation
67
+
68
+ ```bash
69
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/happytoolin/alur/releases/download/v0.0.1/alur-installer.sh | sh
70
+ echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> "$GITHUB_ENV"
71
+ ```
72
+
73
+ Use the versioned release URL to pin. Use `releases/latest/download` to track the latest release.
74
+
75
+ ## Enable the `node` shim
76
+
77
+ Once `alur` is installed, run `alur init` for your shell to enable the `node` shim.
78
+ This creates a managed `node` launcher (a symlink on Unix, copied executable on Windows) and outputs a PATH setup line for your shell config.
79
+
80
+ Add the output to the **end** of your shell rc file (after nvm / mise / asdf / fnm / volta init):
81
+
82
+ **zsh** (`~/.zshrc`):
83
+
84
+ ```bash
85
+ eval "$(alur init zsh)"
86
+ ```
87
+
88
+ **bash** (`~/.bashrc`):
89
+
90
+ ```bash
91
+ eval "$(alur init bash)"
92
+ ```
93
+
94
+ **fish** (`~/.config/fish/config.fish`):
95
+
96
+ ```fish
97
+ alur init fish | source
98
+ ```
99
+
100
+ **PowerShell** (`$PROFILE`):
101
+
102
+ ```powershell
103
+ Invoke-Expression (& alur init powershell)
104
+ ```
105
+
106
+ **Nushell** (`~/.config/nushell/config.nu`):
107
+
108
+ ```nu
109
+ alur init nushell | save --force ~/.config/nushell/alur.nu
110
+ source ~/.config/nushell/alur.nu
111
+ ```
112
+
113
+ Once added, restart your shell. `node` will route known npm verbs through alur
114
+ (`node install vite` → `ni vite`) and pass everything else through to the real Node.js.
115
+
116
+ ## Commands
117
+
118
+ ### Canonical `alur` commands
119
+
120
+ ```bash
121
+ alur install vite
122
+ alur uninstall lodash
123
+ alur run dev
124
+ alur exec vitest
125
+ alur ci
126
+ alur parallel "pnpm dev" "pnpm test"
127
+ alur sequential "pnpm lint" "pnpm test"
128
+ ```
129
+
130
+ ### `ni`
131
+
132
+ Install dependencies or add new ones.
133
+
134
+ ```bash
135
+ ni
136
+ ni vite
137
+ ni -D vitest
138
+ ni -g eslint
139
+ ni --frozen
140
+ ni --frozen-if-present
141
+ ```
142
+
143
+ ### `nr`
144
+
145
+ Run package scripts.
146
+
147
+ ```bash
148
+ nr
149
+ nr dev
150
+ nr build
151
+ nr test -- --watch
152
+ nr --if-present lint
153
+ ```
154
+
155
+ ### `nlx`
156
+
157
+ Execute binaries without adding them permanently to your project.
158
+
159
+ ```bash
160
+ nlx vitest
161
+ nlx eslint .
162
+ nlx create-vite@latest
163
+ ```
164
+
165
+ ### `nun`
166
+
167
+ Uninstall dependencies.
168
+
169
+ ```bash
170
+ nun lodash
171
+ nun react react-dom
172
+ nun -g typescript
173
+ ```
174
+
175
+ ### `nci`
176
+
177
+ Run a clean install. If a lockfile exists, `alur` uses the package-manager-specific frozen install command.
178
+
179
+ ```bash
180
+ nci
181
+ ```
182
+
183
+ ### `np` / `ns`
184
+
185
+ Run shell commands in parallel or sequentially.
186
+
187
+ ```bash
188
+ np "pnpm dev" "pnpm test"
189
+ ns "pnpm lint" "pnpm test"
190
+ ```
191
+
192
+ ### `node`
193
+
194
+ `alur` can also act as a package-manager-aware `node` shim.
195
+ Enable it by adding `alur init <shell>` to your shell config first.
196
+
197
+ ```bash
198
+ node install vite
199
+ node uninstall lodash
200
+ node run dev
201
+ node exec vitest
202
+ node ci
203
+ node p "echo one" "echo two"
204
+ ```
205
+
206
+ Regular Node.js usage still passes through:
207
+
208
+ ```bash
209
+ node script.js
210
+ node -v
211
+ node -- --trace-warnings
212
+ ```
213
+
214
+ ### Utilities
215
+
216
+ ```bash
217
+ alur help ni
218
+ alur completion zsh
219
+ alur init bash
220
+ alur doctor
221
+ ```
222
+
223
+ ## Global Flags
224
+
225
+ These work across `alur` and the multicall aliases:
226
+
227
+ ```bash
228
+ --print-command
229
+ --explain
230
+ -C <dir>
231
+ -v --version
232
+ -h --help
233
+ ```
234
+
235
+ Use `--` to forward flags to the underlying package manager or script:
236
+
237
+ ```bash
238
+ alur install -- --help
239
+ nr test -- --watch
240
+ ```
241
+
242
+ ## Configuration
243
+
244
+ Config file:
245
+
246
+ - `$XDG_CONFIG_HOME/alur/config.toml`
247
+ - macOS default: `~/Library/Application Support/alur/config.toml`
248
+ - Windows default: `%APPDATA%\alur\config.toml`
249
+
250
+ Supported keys:
251
+
252
+ ```toml
253
+ default_package_manager = "pnpm"
254
+ global_package_manager = "npm"
255
+ fast_mode = true
256
+ ```
257
+
258
+ Environment overrides:
259
+
260
+ - `ALUR_CONFIG_FILE`
261
+ - `ALUR_DEFAULT_PACKAGE_MANAGER`
262
+ - `ALUR_GLOBAL_PACKAGE_MANAGER`
263
+ - `ALUR_FAST_MODE`
264
+
265
+ ## How It Works
266
+
267
+ `alur` detects the package manager from:
268
+
269
+ 1. `packageManager` in `package.json`
270
+ 2. lockfiles such as `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `yarn.lock`, `package-lock.json`, `bun.lockb`, or `deno.lock`
271
+ 3. `devEngines.packageManager` in `package.json`
272
+ 4. install metadata such as `.pnp.cjs`, `node_modules/.pnpm`, or `node_modules/.package-lock.json`
273
+ 5. config defaults if detection is unavailable
274
+
275
+ Then it maps the command family to the right underlying command:
276
+
277
+ - `ni` -> install or add
278
+ - `nr` -> run or task
279
+ - `nlx` -> `npx` / `pnpm dlx` / `yarn dlx` / `bun x`
280
+ - `nun` -> uninstall or remove
281
+ - `nci` -> frozen install when lockfiles exist
282
+ - `np` / `ns` -> parallel or sequential shell commands
283
+
284
+ ## Troubleshooting
285
+
286
+ ### PowerShell `ni` alias conflict
287
+
288
+ PowerShell ships with a built-in `ni` alias for `New-Item`.
289
+
290
+ If that conflicts with `alur`, remove or override it in your profile before loading `alur`:
291
+
292
+ ```powershell
293
+ Remove-Item Alias:ni -ErrorAction SilentlyContinue
294
+ Invoke-Expression (& alur init powershell)
295
+ ```
296
+
297
+ ### Check what `alur` resolved
298
+
299
+ ```bash
300
+ ni vite --print-command
301
+ nr dev --explain
302
+ alur doctor
303
+ ```
304
+
305
+ ## Benchmarking
306
+
307
+ The active benchmark suite lives in [`benchmark/`](benchmark/).
308
+
309
+ If you use [`just`](https://github.com/casey/just), the common local commands are wrapped in [`justfile`](justfile):
310
+
311
+ ```bash
312
+ just build-release
313
+ just test
314
+ just test-fast
315
+ just ci
316
+ just bench
317
+ ```
318
+
319
+ Run the default local benchmark with:
320
+
321
+ ```bash
322
+ npm ci
323
+ npm run bench
324
+ just bench
325
+ ```
326
+
327
+ Pass options through either entrypoint:
328
+
329
+ ```bash
330
+ npm run bench -- --track=compare
331
+ npm run bench -- --track=fast
332
+ npm run bench -- --track=runtime
333
+ npm run bench -- --track=direct
334
+ just bench --track=direct --runs=3 --warmups=1 --no-build
335
+ ```
336
+
337
+ Run the full release-style matrix with:
338
+
339
+ ```bash
340
+ npm run bench -- --track=all --runs=500 --warmups=50
341
+ ```
342
+
343
+ Generate flamegraphs with:
344
+
345
+ ```bash
346
+ ./benchmark/profile.sh
347
+ ```
348
+
349
+ Tracked benchmark docs:
350
+
351
+ - current snapshot: [`benchmark/LATEST.md`](benchmark/LATEST.md)
352
+ - lightweight history: [`benchmark/HISTORY.md`](benchmark/HISTORY.md)
353
+ - fast-mode compatibility: [`docs/fast-compat.md`](docs/fast-compat.md)
354
+
355
+ ### Representative Results
356
+
357
+ All numbers below were measured on macOS (Apple Silicon) with the release binary, using `hyperfine` with 10 warmups and 100 measured runs per case. See [`benchmark/LATEST.md`](benchmark/LATEST.md) for the raw tracked snapshot.
358
+
359
+ **Headline:** `alur --fast` is **7.4x faster** than running package managers directly, and **4.6x faster** than `alur` in its own PM fallback mode.
360
+
361
+ #### 1. Fast mode vs PM mode (inside alur)
362
+
363
+ Fast mode bypasses the package manager CLI entirely and runs scripts / local bins natively.
364
+
365
+ | Case | PM mode | Fast mode | Speedup |
366
+ | ------------------------ | ------: | --------: | --------: |
367
+ | `nr noop (npm)` | 246 ms | 37 ms | **6.6x** |
368
+ | `nr noop (pnpm)` | 799 ms | 49 ms | **16.4x** |
369
+ | `nr noop (yarn)` | 348 ms | 38 ms | **9.3x** |
370
+ | `node run noop (pnpm)` | 956 ms | 34 ms | **28.4x** |
371
+ | `nlx hello --flag (npm)` | 288 ms | 17 ms | **17.0x** |
372
+ | `nr noop (bun)` | 70 ms | 37 ms | **1.9x** |
373
+ | `nr noop (deno)` | 80 ms | 35 ms | **2.2x** |
374
+
375
+ _Geometric mean across all package managers: **4.6x**._
376
+
377
+ pnpm and yarn see the biggest wins because their CLIs carry the most startup overhead. Bun and Deno are already fast, so the margin is smaller (but still consistently ahead).
378
+
379
+ #### 2. alur fast vs direct package-manager usage
380
+
381
+ This is the real-world comparison: what users actually type today versus using `alur`.
382
+
383
+ | Case | Direct PM | alur --fast | Speedup |
384
+ | ------------------------ | --------: | ---------: | --------: |
385
+ | `npm run noop` | 320 ms | 53 ms | **6.1x** |
386
+ | `pnpm run noop` | 749 ms | 41 ms | **18.2x** |
387
+ | `yarn run noop` | 443 ms | 34 ms | **13.0x** |
388
+ | `npx hello --flag` | 300 ms | 4.8 ms | **62.0x** |
389
+ | `pnpm exec hello --flag` | 733 ms | 8.9 ms | **82.8x** |
390
+ | `bun run noop` | 79 ms | 34 ms | **2.4x** |
391
+ | `deno task noop` | 50 ms | 34 ms | **1.5x** |
392
+
393
+ _Geometric mean: **7.4x**._
394
+
395
+ Local bin execution is the standout feature: `npx` and `pnpm exec` spend hundreds of milliseconds resolving, validating, and bootstrapping before they even start your binary. `alur` resolves the bin once and runs it directly.
396
+
397
+ #### 3. alur vs Antfu's `ni`
398
+
399
+ For startup/version checks, `alur` is faster:
400
+
401
+ | Case | antfu/ni | alur | Speedup |
402
+ | -------------- | -------: | ----: | -------: |
403
+ | `ni --version` | 149 ms | 92 ms | **1.6x** |
404
+
405
+ _Current compare track keeps only version startup because `alur` no longer carries legacy `?` command-printing compatibility._
406
+
407
+ #### 4. Runtime comparison vs Bun and Deno
408
+
409
+ Even against native runtime task execution, `alur` holds its own:
410
+
411
+ | Case | alur | bun | deno |
412
+ | ------------ | ----: | -----: | ----: |
413
+ | `task noop` | 33 ms | 78 ms | 49 ms |
414
+ | `task hooks` | 90 ms | 210 ms | 77 ms |
415
+
416
+ `alur` is **2.3x faster than bun** for task execution and slightly faster than Deno for simple scripts.
417
+
418
+ ### Methodology
419
+
420
+ The benchmark suite lives in [`benchmark/`](benchmark/) and uses `hyperfine` to time the release binary. It covers five angles:
421
+
422
+ - **`direct`** — normal package-manager commands (`npm run`, `pnpm exec`, etc.) vs `alur --fast`
423
+ - **`fast`** — `alur` PM mode vs `alur` fast mode (isolates the native-execution win)
424
+ - **`compare`** — `alur` vs `@antfu/ni` on startup/version overhead
425
+ - **`runtime`** — `alur` vs `bun` vs `deno` on actual task execution time
426
+ - **`fixtures`** — real project fixtures from `tests/fixtures/` across all detection categories
427
+
428
+ Run the full matrix locally:
429
+
430
+ ```bash
431
+ npm run bench -- --track=all --runs=100 --warmups=10
432
+ ```
433
+
434
+ Or generate flamegraphs:
435
+
436
+ ```bash
437
+ ./benchmark/profile.sh
438
+ ```
439
+
440
+ Tracked snapshots are kept in [`benchmark/LATEST.md`](benchmark/LATEST.md).
@@ -0,0 +1,212 @@
1
+ const { createWriteStream, existsSync, mkdirSync, mkdtemp } = require("fs");
2
+ const { join, sep } = require("path");
3
+ const { spawnSync } = require("child_process");
4
+ const { tmpdir } = require("os");
5
+
6
+ const axios = require("axios");
7
+ const rimraf = require("rimraf");
8
+ const tmpDir = tmpdir();
9
+
10
+ const error = (msg) => {
11
+ console.error(msg);
12
+ process.exit(1);
13
+ };
14
+
15
+ class Package {
16
+ constructor(platform, name, url, filename, zipExt, binaries) {
17
+ let errors = [];
18
+ if (typeof url !== "string") {
19
+ errors.push("url must be a string");
20
+ } else {
21
+ try {
22
+ new URL(url);
23
+ } catch (e) {
24
+ errors.push(e);
25
+ }
26
+ }
27
+ if (name && typeof name !== "string") {
28
+ errors.push("package name must be a string");
29
+ }
30
+ if (!name) {
31
+ errors.push("You must specify the name of your package");
32
+ }
33
+ if (binaries && typeof binaries !== "object") {
34
+ errors.push("binaries must be a string => string map");
35
+ }
36
+ if (!binaries) {
37
+ errors.push("You must specify the binaries in the package");
38
+ }
39
+
40
+ if (errors.length > 0) {
41
+ let errorMsg =
42
+ "One or more of the parameters you passed to the Binary constructor are invalid:\n";
43
+ errors.forEach((error) => {
44
+ errorMsg += error;
45
+ });
46
+ errorMsg +=
47
+ '\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
48
+ error(errorMsg);
49
+ }
50
+
51
+ this.platform = platform;
52
+ this.url = url;
53
+ this.name = name;
54
+ this.filename = filename;
55
+ this.zipExt = zipExt;
56
+ this.installDirectory = join(__dirname, "node_modules", ".bin_real");
57
+ this.binaries = binaries;
58
+
59
+ if (!existsSync(this.installDirectory)) {
60
+ mkdirSync(this.installDirectory, { recursive: true });
61
+ }
62
+ }
63
+
64
+ exists() {
65
+ for (const binaryName in this.binaries) {
66
+ const binRelPath = this.binaries[binaryName];
67
+ const binPath = join(this.installDirectory, binRelPath);
68
+ if (!existsSync(binPath)) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ install(fetchOptions, suppressLogs = false) {
76
+ if (this.exists()) {
77
+ if (!suppressLogs) {
78
+ console.error(
79
+ `${this.name} is already installed, skipping installation.`,
80
+ );
81
+ }
82
+ return Promise.resolve();
83
+ }
84
+
85
+ if (existsSync(this.installDirectory)) {
86
+ rimraf.sync(this.installDirectory);
87
+ }
88
+
89
+ mkdirSync(this.installDirectory, { recursive: true });
90
+
91
+ if (!suppressLogs) {
92
+ console.error(`Downloading release from ${this.url}`);
93
+ }
94
+
95
+ return axios({ ...fetchOptions, url: this.url, responseType: "stream" })
96
+ .then((res) => {
97
+ return new Promise((resolve, reject) => {
98
+ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
99
+ let tempFile = join(directory, this.filename);
100
+ const sink = res.data.pipe(createWriteStream(tempFile));
101
+ sink.on("error", (err) => reject(err));
102
+ sink.on("close", () => {
103
+ if (/\.tar\.*/.test(this.zipExt)) {
104
+ const result = spawnSync("tar", [
105
+ "xf",
106
+ tempFile,
107
+ // The tarballs are stored with a leading directory
108
+ // component; we strip one component in the
109
+ // shell installers too.
110
+ "--strip-components",
111
+ "1",
112
+ "-C",
113
+ this.installDirectory,
114
+ ]);
115
+ if (result.status == 0) {
116
+ resolve();
117
+ } else if (result.error) {
118
+ reject(result.error);
119
+ } else {
120
+ reject(
121
+ new Error(
122
+ `An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
123
+ ),
124
+ );
125
+ }
126
+ } else if (this.zipExt == ".zip") {
127
+ let result;
128
+ if (this.platform.artifactName.includes("windows")) {
129
+ // Windows does not have "unzip" by default on many installations, instead
130
+ // we use Expand-Archive from powershell
131
+ result = spawnSync("powershell.exe", [
132
+ "-NoProfile",
133
+ "-NonInteractive",
134
+ "-Command",
135
+ `& {
136
+ param([string]$LiteralPath, [string]$DestinationPath)
137
+ Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
138
+ }`,
139
+ tempFile,
140
+ this.installDirectory,
141
+ ]);
142
+ } else {
143
+ result = spawnSync("unzip", [
144
+ "-q",
145
+ tempFile,
146
+ "-d",
147
+ this.installDirectory,
148
+ ]);
149
+ }
150
+
151
+ if (result.status == 0) {
152
+ resolve();
153
+ } else if (result.error) {
154
+ reject(result.error);
155
+ } else {
156
+ reject(
157
+ new Error(
158
+ `An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
159
+ ),
160
+ );
161
+ }
162
+ } else {
163
+ reject(
164
+ new Error(`Unrecognized file extension: ${this.zipExt}`),
165
+ );
166
+ }
167
+ });
168
+ });
169
+ });
170
+ })
171
+ .then(() => {
172
+ if (!suppressLogs) {
173
+ console.error(`${this.name} has been installed!`);
174
+ }
175
+ })
176
+ .catch((e) => {
177
+ error(`Error fetching release: ${e.message}`);
178
+ });
179
+ }
180
+
181
+ run(binaryName, fetchOptions) {
182
+ const promise = !this.exists()
183
+ ? this.install(fetchOptions, true)
184
+ : Promise.resolve();
185
+
186
+ promise
187
+ .then(() => {
188
+ const [, , ...args] = process.argv;
189
+
190
+ const options = { cwd: process.cwd(), stdio: "inherit" };
191
+
192
+ const binRelPath = this.binaries[binaryName];
193
+ if (!binRelPath) {
194
+ error(`${binaryName} is not a known binary in ${this.name}`);
195
+ }
196
+ const binPath = join(this.installDirectory, binRelPath);
197
+ const result = spawnSync(binPath, args, options);
198
+
199
+ if (result.error) {
200
+ error(result.error);
201
+ }
202
+
203
+ process.exit(result.status);
204
+ })
205
+ .catch((e) => {
206
+ error(e.message);
207
+ process.exit(1);
208
+ });
209
+ }
210
+ }
211
+
212
+ module.exports.Package = Package;