type-a-bin 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SynthLuvr
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.
package/README.md ADDED
@@ -0,0 +1,680 @@
1
+ # Type-A-Bin
2
+
3
+ > Mock any executable binary — for testing scripts that shell out.
4
+
5
+ [![License:
6
+ MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ Have a script or CLI that executes shell commands (`git`, `gh`,
9
+ `docker`, `kubectl`…)? Want to test that code without actually invoking
10
+ those commands — no network calls, no side effects, no real
11
+ dependencies? **Type-A-Bin** lets you mock any executable binary by
12
+ injecting a mock script into your `PATH`.
13
+
14
+ - **Intercept any command.** The moment your code shells out, the mock
15
+ takes over.
16
+ - **Total control over output and exit codes.** Return whatever stdout,
17
+ stderr, and exit status your tests need.
18
+ - **Conditional mocking.** Mock only the subcommands you care about and
19
+ let everything else fall through to the real binary.
20
+ - **Any interpreter.** Bash, Node, Python, Perl — if it has a shebang,
21
+ you can use it.
22
+ - **Cross-platform.** The same API works on Linux and Windows.
23
+ - **TypeScript-first.** Full type definitions and overloaded signatures.
24
+ - **Zero dependencies at runtime.** Pure Node.js standard library.
25
+
26
+ Type-A-Bin is a Node.js alternative to the npm packages
27
+ [mock-bin](https://github.com/stevemao/mock-bin) and
28
+ [mock-a-bin](https://github.com/levibostian/mock-a-bin). It began as a
29
+ Deno-to-Node.js migration of `mock-a-bin` and has since grown into a
30
+ fully typed, dependency-free library with richer features.
31
+
32
+ ------------------------------------------------------------------------
33
+
34
+ ## Table of Contents
35
+
36
+ - [Why does this exist?](#why-does-this-exist)
37
+ - [How it works](#how-it-works)
38
+ - [Installation](#installation)
39
+ - [Quick start](#quick-start)
40
+ - [Usage](#usage)
41
+ - [1. Output shorthand](#1-output-shorthand)
42
+ - [2. Full script](#2-full-script)
43
+ - [3. Script file](#3-script-file)
44
+ - [Conditional mocking](#conditional-mocking)
45
+ - [Pattern-based mocking](#pattern-based-mocking)
46
+ - [Script-based mocking with
47
+ `mock-a-bin-run-original`](#script-based-mocking-with-mock-a-bin-run-original)
48
+ - [Controlling exit codes](#controlling-exit-codes)
49
+ - [Passing environment variables](#passing-environment-variables)
50
+ - [Mocking multiple commands at
51
+ once](#mocking-multiple-commands-at-once)
52
+ - [Mocking with any interpreter](#mocking-with-any-interpreter)
53
+ - [Using with a test runner](#using-with-a-test-runner)
54
+ - [API reference](#api-reference)
55
+ - [`mockBin(...)`](#mockbin)
56
+ - [Types](#types)
57
+ - [Packages](#packages)
58
+ - [Prerequisites](#prerequisites)
59
+ - [Scripts](#scripts)
60
+ - [How it works under the hood](#how-it-works-under-the-hood)
61
+ - [Windows support](#windows-support)
62
+ - [Comparison with other tools](#comparison-with-other-tools)
63
+ - [Contributing](#contributing)
64
+ - [License](#license)
65
+
66
+ ------------------------------------------------------------------------
67
+
68
+ ## Why does this exist?
69
+
70
+ Testing code that shells out is hard. When a function calls
71
+ `execSync("gh pr list")`, your test depends on the real GitHub CLI, a
72
+ network connection, a logged-in account, and a real repository. That is
73
+ slow, fragile, and impossible to reproduce deterministically.
74
+
75
+ There are a few common workarounds, each with drawbacks:
76
+
77
+ | Approach | Problem |
78
+ |-----------------------|----------------------------------------------------------|
79
+ | Stub `child_process` | Couples tests to the module boundary; misses edge cases. |
80
+ | `nock` / HTTP mocking | Only works for HTTP, not arbitrary binaries. |
81
+ | Real binaries in CI | Slow, requires secrets, produces flaky tests. |
82
+ | **Type-A-Bin** | Mocks *any* binary at the `PATH` level — transparently. |
83
+
84
+ Type-A-Bin works by creating a temporary executable with the same name
85
+ as the target binary and prepending its directory to `PATH`. When your
86
+ code runs `gh`, the shell resolves it to your mock instead of the real
87
+ `gh`. Your application code doesn’t change at all — no dependency
88
+ injection, no wrappers, no monkey-patching. This is especially powerful
89
+ for testing CLIs and scripts that invoke other CLIs.
90
+
91
+ ## How it works
92
+
93
+ Your test process.env.PATH
94
+ ────────── ────────────────────────────────────────
95
+ mockBin("gh") /tmp/mock-bin-xK9/ ← prepended (mock)
96
+ │ /usr/local/bin ← original (real gh)
97
+ ▼ /usr/bin
98
+ process calls ────────────────────────────────────────
99
+ "gh pr list" The shell finds the mock FIRST, runs it.
100
+
101
+ Every `mockBin()` call:
102
+
103
+ 1. Creates a fresh temp directory.
104
+ 2. Writes an executable mock script named after your binary into it.
105
+ 3. Prepends that directory to `process.env.PATH` so the mock shadows
106
+ the real binary.
107
+ 4. Returns a **cleanup function** that restores the original `PATH` and
108
+ deletes the temp directory.
109
+
110
+ ## Installation
111
+
112
+ ``` bash
113
+ pnpm add -D type-a-bin
114
+ ```
115
+
116
+ Or with npm/yarn:
117
+
118
+ ``` bash
119
+ npm install -D type-a-bin
120
+ yarn add -D type-a-bin
121
+ ```
122
+
123
+ > You can also install pre-built packages straight from GitHub via the
124
+ > `dist` branch, which is rebuilt automatically on every merge to
125
+ > `main`:
126
+ >
127
+ > ``` bash
128
+ > pnpm add -D github:SynthLuvr/type-a-bin#dist
129
+ > ```
130
+
131
+ ## Quick start
132
+
133
+ ``` ts
134
+ import { execFileSync } from "node:child_process";
135
+ import { mockBin } from "type-a-bin";
136
+
137
+ // Replace the 'gh' command with a mock that prints custom output
138
+ const cleanup = await mockBin("gh", "bash", 'echo "mocked output"');
139
+
140
+ // Any call to 'gh' now executes the mock script
141
+ const output = execFileSync("gh", ["pr", "list"], { encoding: "utf-8" });
142
+ console.log(output); // "mocked output\n"
143
+
144
+ cleanup(); // Restore the original PATH
145
+ ```
146
+
147
+ ## Usage
148
+
149
+ `mockBin` offers three calling conventions. Choose the one that fits how
150
+ much control you need.
151
+
152
+ ### 1. Output shorthand
153
+
154
+ When you only need the mock to print static text, skip the interpreter
155
+ and pass the output string directly. The mock uses `bash` and `echo`s
156
+ the value:
157
+
158
+ ``` ts
159
+ const cleanup = await mockBin("git", "Everything is up to date");
160
+ // $ git status → "Everything is up to date\n"
161
+ cleanup();
162
+ ```
163
+
164
+ This is the simplest form — perfect for quick stubs where you just need
165
+ a predictable string back.
166
+
167
+ ### 2. Full script
168
+
169
+ For dynamic behaviour — conditional logic, arguments, exit codes — pass
170
+ an interpreter (`shebang`) and arbitrary script `code`:
171
+
172
+ ``` ts
173
+ const cleanup = await mockBin(
174
+ "gh",
175
+ "bash",
176
+ 'echo "pr: $1 $2"',
177
+ );
178
+
179
+ // $ gh pr list → "pr: pr list\n"
180
+ cleanup();
181
+ ```
182
+
183
+ The `shebang` argument accepts either a bare interpreter name or a full
184
+ shebang line:
185
+
186
+ ``` ts
187
+ // Bare interpreter — wrapped automatically in `#!/usr/bin/env …`
188
+ mockBin("git", "bash", 'echo "hi"');
189
+
190
+ // Full shebang line — used as-is
191
+ mockBin("git", "#!/usr/bin/env bash", 'echo "hi"');
192
+
193
+ // Absolute path
194
+ mockBin("git", "#!/bin/bash", 'echo "hi"');
195
+ ```
196
+
197
+ The mock receives all arguments the real binary would have received. In
198
+ a Bash script, access them via `$1`, `$2`, `"$@"`, etc.:
199
+
200
+ ``` ts
201
+ await mockBin(
202
+ "docker",
203
+ "bash",
204
+ `
205
+ echo "Building image: $1"
206
+ echo "Args received: $@"
207
+ `,
208
+ );
209
+ ```
210
+
211
+ ### 3. Script file
212
+
213
+ For larger or more complex mocks, point `mockBin` at a script file on
214
+ disk instead of inlining the code:
215
+
216
+ ``` ts
217
+ const cleanup = await mockBin("dragon", "node --import tsx", {
218
+ file: "./src/tests/hoard-script.ts",
219
+ });
220
+ cleanup();
221
+ ```
222
+
223
+ **Why a file?** When you inline script code as a string, the mock binary
224
+ is written to an *extensionless* temp file. Some tooling relies on file
225
+ extensions to decide what to do — most notably `node --import tsx`,
226
+ which only transforms `.ts`/`.tsx` files. By passing `{ file }`, the
227
+ original file keeps its real extension on disk, so extension-aware
228
+ loaders work correctly. Internally, Type-A-Bin writes a tiny `/bin/sh`
229
+ wrapper that `exec`s your file through the given interpreter, forwarding
230
+ all arguments.
231
+
232
+ This works with any language and any file extension:
233
+
234
+ ``` ts
235
+ // A Python mock
236
+ await mockBin("mycli", "python3", {
237
+ file: "./tests/mocks/mycli.py",
238
+ });
239
+ ```
240
+
241
+ ### Conditional mocking
242
+
243
+ Often you want to mock only *some* invocations of a command and let
244
+ others run normally — for example, mock `git status` but use the real
245
+ `git log`. Type-A-Bin provides two complementary approaches.
246
+
247
+ #### Pattern-based mocking
248
+
249
+ Use a regex `pattern` to match the full command string. Only matching
250
+ commands are mocked; everything else passes through to the real binary
251
+ automatically:
252
+
253
+ ``` ts
254
+ const cleanup = await mockBin(
255
+ {
256
+ binName: "gh",
257
+ pattern: "^gh pr (list|view)", // matches "gh pr list" and "gh pr view"
258
+ },
259
+ "bash",
260
+ 'echo "mocked PR command"',
261
+ );
262
+
263
+ // These are mocked:
264
+ // $ gh pr list → "mocked PR command\n"
265
+ // $ gh pr view 123 → "mocked PR command\n"
266
+
267
+ // Everything else hits the real gh:
268
+ // $ gh auth status → (real output)
269
+ cleanup();
270
+ ```
271
+
272
+ The pattern is matched against the full command including the binary
273
+ name and all arguments (e.g. `gh pr list`). An empty pattern (`""`)
274
+ mocks **every** invocation, just like passing no pattern at all — so you
275
+ can toggle the behaviour dynamically.
276
+
277
+ Pattern-based mocking composes with all three calling conventions,
278
+ including the output shorthand:
279
+
280
+ ``` ts
281
+ const cleanup = await mockBin(
282
+ { binName: "git", pattern: "^git status" },
283
+ "mocked status",
284
+ );
285
+ ```
286
+
287
+ > **How patterns work internally:** the generated mock script builds the
288
+ > full command string (`"${binName} $*"`) and tests it with `grep -qE`.
289
+ > If it matches, the mock code runs; otherwise the real binary is
290
+ > invoked directly via `exec`.
291
+
292
+ #### Script-based mocking with `mock-a-bin-run-original`
293
+
294
+ For finer-grained, programmatic control, write the logic yourself in the
295
+ mock script and call the `mock-a-bin-run-original` helper to delegate
296
+ back to the real binary when you want to:
297
+
298
+ ``` ts
299
+ const cleanup = await mockBin(
300
+ "git",
301
+ "bash",
302
+ `
303
+ if [ "$1" = "status" ]; then
304
+ echo "Everything is clean!"
305
+ else
306
+ mock-a-bin-run-original "$@"
307
+ fi
308
+ `,
309
+ );
310
+
311
+ // $ git status → "Everything is clean!\n" (mocked)
312
+ // $ git log --oneline → (real git output) (passed through)
313
+ cleanup();
314
+ ```
315
+
316
+ Every `mockBin()` call creates a `mock-a-bin-run-original` executable in
317
+ the same temp directory (which is on the `PATH`). This helper restores
318
+ the original `PATH`, locates the real binary, and executes it with all
319
+ forwarded arguments. It works with **any** interpreter — bash, Node,
320
+ Python, etc.:
321
+
322
+ ``` ts
323
+ // Conditional mocking from a Node mock
324
+ const cleanup = await mockBin("git", "node", `
325
+ const { spawnSync } = require("child_process");
326
+ if (process.argv[2] === "status") {
327
+ console.log("mocked from node");
328
+ } else {
329
+ const result = spawnSync("mock-a-bin-run-original", process.argv.slice(2), {
330
+ stdio: "inherit",
331
+ });
332
+ process.exit(result.status || 0);
333
+ }
334
+ `);
335
+ cleanup();
336
+ ```
337
+
338
+ If the original binary doesn’t exist on the system, the helper prints an
339
+ error to stderr and exits with code `127` (the conventional “command not
340
+ found” exit status).
341
+
342
+ ### Controlling exit codes
343
+
344
+ Your mock script controls the exit code exactly like any normal script.
345
+ In Bash, use `exit`:
346
+
347
+ ``` ts
348
+ const cleanup = await mockBin("git", "bash", "exit 1");
349
+
350
+ const result = spawnSync("git", { encoding: "utf-8" });
351
+ console.log(result.status); // 1
352
+
353
+ cleanup();
354
+ ```
355
+
356
+ This lets you simulate failures, permission errors, timeouts — anything
357
+ your application needs to handle.
358
+
359
+ ### Passing environment variables
360
+
361
+ Environment variables from the calling process are passed through to
362
+ both the mock and the original binary. This is useful when you want your
363
+ test to inject configuration via `env`:
364
+
365
+ ``` ts
366
+ const cleanup = await mockBin("env", "bash", 'mock-a-bin-run-original "$@"');
367
+
368
+ const result = spawnSync("env", {
369
+ encoding: "utf-8",
370
+ env: {
371
+ ...process.env,
372
+ CUSTOM_TEST_VAR: "my-custom-value",
373
+ },
374
+ });
375
+
376
+ console.log(result.stdout); // includes "CUSTOM_TEST_VAR=my-custom-value"
377
+ cleanup();
378
+ ```
379
+
380
+ ### Mocking multiple commands at once
381
+
382
+ Call `mockBin` as many times as you need. Each call is independent and
383
+ returns its own cleanup function:
384
+
385
+ ``` ts
386
+ const cleanupGit = await mockBin("git", "bash", 'echo "mocked git"');
387
+ const cleanupGh = await mockBin("gh", "bash", 'echo "mocked gh"');
388
+
389
+ // $ git status → "mocked git\n"
390
+ // $ gh pr list → "mocked gh\n"
391
+
392
+ cleanupGit();
393
+ cleanupGh();
394
+ ```
395
+
396
+ ### Mocking with any interpreter
397
+
398
+ Because mocks are real executable scripts with shebangs, you can use any
399
+ language available on your system:
400
+
401
+ ``` ts
402
+ // Node.js
403
+ await mockBin("mytool", "node", 'console.log("from node")');
404
+
405
+ // Python
406
+ await mockBin("mytool", "python3", 'print("from python")');
407
+
408
+ // Perl
409
+ await mockBin("mytool", "perl", 'print "from perl\n";');
410
+ ```
411
+
412
+ ## Using with a test runner
413
+
414
+ Type-A-Bin is test-runner agnostic — it only touches `process.env.PATH`.
415
+ The recommended pattern is to set up mocks in a `beforeEach`/`beforeAll`
416
+ and clean up in `afterEach`/`afterAll` so each test starts with a fresh
417
+ `PATH`. Here’s an example using [Vitest](https://vitest.dev):
418
+
419
+ ``` ts
420
+ import { execFileSync } from "node:child_process";
421
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
422
+ import { mockBin } from "type-a-bin";
423
+
424
+ describe("my CLI", () => {
425
+ let cleanup: () => void;
426
+
427
+ beforeEach(async () => {
428
+ cleanup = await mockBin("git", "bash", 'echo "On branch main"');
429
+ });
430
+
431
+ afterEach(() => {
432
+ cleanup();
433
+ });
434
+
435
+ it("reads the current branch", () => {
436
+ const output = execFileSync("git", ["branch", "--show-current"], {
437
+ encoding: "utf-8",
438
+ });
439
+ expect(output.trim()).toBe("On branch main");
440
+ });
441
+ });
442
+ ```
443
+
444
+ The same approach works with Jest, Mocha, Node’s built-in test runner,
445
+ or any other framework. The key is to always call the cleanup function
446
+ to restore the original `PATH` — otherwise mocks leak into subsequent
447
+ tests.
448
+
449
+ ## API reference
450
+
451
+ ### `mockBin`
452
+
453
+ Creates a mock executable and prepends it to `PATH`. Returns an async
454
+ **cleanup function** that restores the original `PATH` and removes the
455
+ temp directory.
456
+
457
+ The function is overloaded with three signatures:
458
+
459
+ #### Output shorthand
460
+
461
+ ``` ts
462
+ mockBin(binNameOrConfig, output): Promise<MockBinCleanup>
463
+ ```
464
+
465
+ | Parameter | Type | Description |
466
+ |-------------------|---------------------------|-----------------------------------------------------|
467
+ | `binNameOrConfig` | `string \| MockBinConfig` | Binary name, or a config with `binName` + `pattern` |
468
+ | `output` | `string` | The text the mock echoes (via `bash`) |
469
+
470
+ Mocks the binary so that every invocation prints `output`. The fastest
471
+ way to stub a command that just needs to return a string.
472
+
473
+ #### Full script
474
+
475
+ ``` ts
476
+ mockBin(binNameOrConfig, shebang, code): Promise<MockBinCleanup>
477
+ ```
478
+
479
+ | Parameter | Type | Description |
480
+ |-------------------|---------------------------|-----------------------------------------------------|
481
+ | `binNameOrConfig` | `string \| MockBinConfig` | Binary name, or a config with `binName` + `pattern` |
482
+ | `shebang` | `string` | Interpreter (e.g. `"bash"`, `"node"`) |
483
+ | `code` | `string` | The script body that runs when the mock is invoked |
484
+
485
+ Gives full control. The `shebang` accepts a bare interpreter name
486
+ (wrapped in `#!/usr/bin/env …` automatically) or a full shebang line.
487
+
488
+ #### Script file
489
+
490
+ ``` ts
491
+ mockBin(binNameOrConfig, shebang, script): Promise<MockBinCleanup>
492
+ ```
493
+
494
+ | Parameter | Type | Description |
495
+ |-------------------|---------------------------|-----------------------------------------------------|
496
+ | `binNameOrConfig` | `string \| MockBinConfig` | Binary name, or a config with `binName` + `pattern` |
497
+ | `shebang` | `string` | Interpreter used to run the file |
498
+ | `script` | `MockBinScriptFile` | `{ file: string }` pointing at a script on disk |
499
+
500
+ Runs a script file through the given interpreter, keeping the file’s
501
+ original extension so extension-aware loaders (e.g. `node --import tsx`)
502
+ work. Throws if the file does not exist.
503
+
504
+ ### Types
505
+
506
+ ``` ts
507
+ /** A cleanup function that restores the original PATH. */
508
+ type MockBinCleanup = () => void;
509
+
510
+ interface MockBinConfig {
511
+ /** The name of the binary to mock (e.g. "gh", "git"). */
512
+ binName: string;
513
+ /** Optional regex pattern. Only commands matching it are mocked. */
514
+ pattern?: string;
515
+ }
516
+
517
+ interface MockBinScriptFile {
518
+ /**
519
+ * Script executed when the mock runs. The file keeps its real
520
+ * extension, so extension-aware loaders (e.g. `node --import tsx`)
521
+ * parse it — embedding the source inline fails because the mock
522
+ * binary is written to an extensionless temp file.
523
+ */
524
+ file: string;
525
+ }
526
+ ```
527
+
528
+ ## Packages
529
+
530
+ This repository is a [pnpm](https://pnpm.io) workspace. The `type-a-bin`
531
+ library lives at the root; additional packages live under `packages/`:
532
+
533
+ - [`packages/bin-test`](packages/bin-test/) — a dragon CLI demo that
534
+ wraps a fictional `dragon` binary and tests it by mocking the binary
535
+ with `mockBin`. A great reference for how to consume the library in a
536
+ real package.
537
+
538
+ ## Prerequisites
539
+
540
+ - [Node.js](https://nodejs.org) 26 and [pnpm](https://pnpm.io) (enforced
541
+ via `engines` in `package.json`)
542
+ - [pandoc](https://pandoc.org) ≥ 3.1 — only needed for Markdown
543
+ formatting/linting (`pnpm lint:md` / `pnpm format:md`), not for using
544
+ the library
545
+ - On Windows: [Git for Windows](https://gitforwindows.org/) — its bash
546
+ powers bash-interpreter mocks (node-interpreter mocks need nothing
547
+ beyond Node itself)
548
+
549
+ ## Scripts
550
+
551
+ Run from the repository root:
552
+
553
+ | Script | Description |
554
+ |-------------------|-------------------------------------------------------|
555
+ | `pnpm build` | Build the library (and workspace packages) to `dist/` |
556
+ | `pnpm test` | Build, run unit tests, then run workspace tests |
557
+ | `pnpm lint` | Run all linters (Biome, oxlint, ast-grep, pandoc) |
558
+ | `pnpm format` | Run all formatters with auto-fix |
559
+ | `pnpm test:watch` | Run unit tests in watch mode |
560
+
561
+ ## How it works under the hood
562
+
563
+ When you call `mockBin(...)` (on Linux/macOS — Windows follows the same
564
+ shape, see [Windows support](#windows-support)):
565
+
566
+ 1. **Temp directory.** A fresh directory is created under the OS temp
567
+ folder (e.g. `/tmp/mock-bin-xxxx/`).
568
+
569
+ 2. **Mock script.** An executable file named after your binary is
570
+ written into the temp dir. Its content depends on the calling
571
+ convention:
572
+
573
+ - **Output shorthand** → a `bash` script that echoes the string.
574
+ - **Full script** → the interpreter shebang + your code.
575
+ - **Script file** → a `/bin/sh` wrapper that `exec`s your file
576
+ through the interpreter, forwarding arguments.
577
+
578
+ 3. **`mock-a-bin-run-original` helper.** A second executable is written
579
+ alongside the mock. It restores the original `PATH`, finds the real
580
+ binary with `command -v`, and `exec`s it with all forwarded
581
+ arguments. This is what powers [script-based conditional
582
+ mocking](#script-based-mocking-with-mock-a-bin-run-original).
583
+
584
+ 4. **Pattern wrapper** *(only when a `pattern` is given)*. The mock
585
+ script is wrapped so that it builds the full command string, tests
586
+ it against the regex with `grep -qE`, and either runs the mock code
587
+ or `exec`s the real binary directly.
588
+
589
+ 5. **PATH manipulation.** The temp directory is prepended to
590
+ `process.env.PATH`, so the mock shadows the real binary. The
591
+ original `PATH` is saved.
592
+
593
+ 6. **Cleanup.** The returned function restores `process.env.PATH` to
594
+ its original value (or deletes it if it was unset) and recursively
595
+ removes the temp directory. Calling cleanup more than once is safe.
596
+
597
+ Type-A-Bin handles the platform-specific path separator (`:` on Unix,
598
+ `;` on Windows) automatically.
599
+
600
+ ## Windows support
601
+
602
+ The same `mockBin` API works on Windows. Because Windows cannot execute
603
+ extensionless `#!` scripts — and Node refuses to spawn `.cmd`/`.bat`
604
+ shims without a shell — the implementation swaps the mechanism, not the
605
+ contract:
606
+
607
+ - Each mock binary is a **hard link of `node.exe`** named `<bin>.exe`
608
+ (copied if the temp directory is on another volume), prepended to
609
+ `PATH` exactly as on Linux.
610
+ - A small **preload** is registered through `NODE_OPTIONS --import` in
611
+ every spawned process. It detects that the process was started as a
612
+ mock shim (its first CLI argument is not a real file), then swaps the
613
+ entry for your mock script — argv, stdin, stdout, stderr, and exit
614
+ codes all pass through.
615
+ - Node-interpreter mocks (and `node --import tsx` script files) run
616
+ **in-process** through loader hooks; other interpreters (`bash`,
617
+ `python`, …) are resolved from `PATH` and spawned with your script and
618
+ the original arguments.
619
+ - The `mock-a-bin-run-original` helper, `pattern` conditionals, output
620
+ shorthand, script files, and cleanup contract all behave as on Linux.
621
+ Cleanup additionally restores `NODE_OPTIONS` and the internal mock
622
+ registry.
623
+
624
+ Requirements and behaviour notes:
625
+
626
+ - **Bash mocks need Git for Windows.** Bash-like interpreters prefer a
627
+ native `bash.exe` (Git Bash) over WSL’s launcher, which cannot run
628
+ Windows-path scripts; well-known Git install locations are probed as a
629
+ fallback.
630
+ - **The first CLI argument must be positional.** A shim *is* `node.exe`,
631
+ so a leading flag (`gh --version`) is consumed by Node’s own option
632
+ parser before the mock can intercept it. Prefer the subcommand form
633
+ (`gh version`), or wrap the flag behind a positional subcommand.
634
+ - **Pass-through needs a real `.exe`.** `mock-a-bin-run-original` and
635
+ pattern fall-through locate and spawn the original binary directly;
636
+ `.cmd`/`.bat`-only binaries (e.g. `npm.cmd`) cannot be spawned by Node
637
+ without a shell.
638
+ - **The output shorthand expands `$1`–`$9`, `$*`, and `$@`** from the
639
+ command line (matching bash `echo` for positional parameters); other
640
+ shell substitutions are printed literally.
641
+ - **Stacked mocks clean up last-in, first-out.** Like `PATH`, the mock
642
+ registry and `NODE_OPTIONS` are snapshot-restored, so call the cleanup
643
+ functions in reverse order of the `mockBin` calls for a full restore.
644
+
645
+ `NODE_OPTIONS` is process environment, not global state: it only affects
646
+ processes spawned while mocks are active, and the preload is inert for
647
+ any process that is not a mock shim.
648
+
649
+ ## Comparison with other tools
650
+
651
+ | Feature | Type-A-Bin | [mock-bin](https://github.com/stevemao/mock-bin) | [mock-a-bin](https://github.com/levibostian/mock-a-bin) |
652
+ |------------------------------------|:----------:|:------------------------------------------------:|:-------------------------------------------------------:|
653
+ | Mocks any binary via `PATH` | ✅ | ✅ | ✅ |
654
+ | Runtime dependencies | 0 | several | several (Deno) |
655
+ | TypeScript types & overloads | ✅ | ❌ | partial |
656
+ | Output shorthand | ✅ | ❌ | ❌ |
657
+ | Script-file mode (keeps extension) | ✅ | ❌ | ❌ |
658
+ | Pattern-based conditional mocking | ✅ | ❌ | ❌ |
659
+ | `run-original` pass-through | ✅ | ❌ | ✅ |
660
+ | Cleanup function | ✅ | ✅ | ✅ |
661
+ | Runtime | Node.js | Node.js | Deno |
662
+
663
+ ## Contributing
664
+
665
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
666
+ the development setup, the coding conventions enforced by the toolchain,
667
+ and how to submit changes.
668
+
669
+ ## License
670
+
671
+ [MIT](LICENSE) © SynthLuvr
672
+
673
+ ## Special Thanks
674
+
675
+ This project began as a Deno-to-Node.js migration of
676
+ [mock-a-bin](https://github.com/levibostian/mock-a-bin) by [Levi
677
+ Bostian](https://github.com/levibostian), and it is also a Node.js
678
+ alternative to the npm package
679
+ [mock-bin](https://github.com/stevemao/mock-bin) by [Steve
680
+ Mao](https://github.com/stevemao). Both projects inspired Type-A-Bin.
@@ -0,0 +1 @@
1
+ export { type MockBinCleanup, type MockBinConfig, type MockBinScriptFile, mockBin, } from "./mock-bin.js";