# @j-o-r/sh &nbsp; [![npm](https://img.shields.io/npm/v/@j-o-r/sh?logo=npm)](https://www.npmjs.com/package/@j-o-r/sh) [![License](https://img.shields.io/npm/license/@j-o-r/sh)](https://codeberg.org/duin/sh/src/branch/main/LICENSE) [![Codeberg](https://img.shields.io/badge/Codeberg-main-blue?logo=codeberg)](https://codeberg.org/duin/sh)

Execute shell commands from JavaScript on Linux.

## Introduction

`@j-o-r/sh` is a lightweight Node.js module for running shell commands with a clean, zx-inspired API. It fixes namespace pollution, adds **rolling timeouts** (reset on stdout/stderr), **no-shell mode** (`/usr/bin/env -S`), process tree kills, buffer limits (1MB default), and a built-in **Test** framework + **AsyncTracker** for leaks.

**Key Features**:
- Template tag `SH`cmd`` → `SHDispatch` for `.options().run()`.
- Global options: `SH.timeout = '5s'; SH.cwd = '/tmp';`.
- Sync/async `run()`/`runSync()`; stdin payload; detached mode.
- Utils: `sleep`, `retry`, `expBackoff`, `cd`, `parseArgs`, `userIn`, `readIn`.
- Testing: `new Test().add('name', () => assert(...)).run()`.
- Async leak detection: `AsyncTracker` via `async_hooks`.
- Safe interpolation; `bashEscape`; full JSDoc.

No runtime deps. ESM-only (ES2020+).

## Quick Install

```bash
npm i @j-o-r/sh
```

## Usage

### Basics

```js
import { SH, cd, sleep } from '@j-o-r/sh';

cd('/tmp');
const out = await SH`ls -la`.run();
console.log(out); // Captured stdout (trimmed)
```

### Chaining & Options

```js
SH`curl -s ip.js.org`
  .options({ timeout: '2s', shell: false })  // No-shell: /usr/bin/env -S
  .run()
  .catch(e => console.error(e.message));     // "Command failed with code 1: ..."
```

### Parallel & Context

```js
import { within } from '@j-o-r/sh';

const results = await within(async () => Promise.all([
  SH`sleep 1; echo ok`.run(),
  sleep('500ms'),
  SH`uname`.run()
]));
```

### Retry & Backoff

```js
import { retry, expBackoff } from '@j-o-r/sh';

try {
  const res = await retry(3, expBackoff('10s'), () =>
    SH`curl -s unreachable`.run()
  );
} catch (e) {
  // Last error
}
```

### Interactive / Stdin

```js
import { userIn, readIn } from '@j-o-r/sh';

// User prompt (abortable)
const { input, abort } = userIn('Password: ');
const pw = await input;

// Piped stdin
const out = await SH`grep secret`.run(await readIn());
```

### Vim / TTY (Sync)

```js
SH`vim`.options({ stdio: 'inherit' }).runSync();
```

## Testing

Full-featured tester with async support, error reporting, unresolved Promise detection.

```js
import { Test, assert, jsType } from '@j-o-r/sh';

const t = new Test(true);  // quiet: no console
t.add('sync assert', () => assert.strictEqual(1 + 1, 2));
t.add('async', async () => {
  await sleep('100ms');
  assert.strictEqual(jsType([]), 'Array');
});
const report = await t.run();
console.log(report);  // { tests: 2, executed: 2, duration: 150, errors: 0 }

t.unresolved();  // Logs leaks if any
```

## Advanced

- **SSH Example** (interactive; use keys/sshpass for automation):
  ```js
  // TTY/inherit for prompts
  SH`ssh user@host`.options({ stdio: 'inherit' }).runSync();
  // Or sshpass: SH`sshpass -p pw ssh user@host`.run()
  ```
  See `scenarios/` for full demos.

- **CLI Args**: `parseArgs()` → `{ port: '8080', _: ['file'] }`.
- **Global Defaults**: Set global options on the `SH` object to apply to all subsequent commands. These can be overridden per-command via `.options()`. Examples:
  - `SH.timeout = '5s';` – Default timeout for all runs.
  - `SH.cwd = '/tmp';` – Default working directory.
  - `SH.shell = false;` – Disable shell mode (uses `/usr/bin/env -S`).
  - `SH.maxBuffer = 1 * 1024 * 1024;` – Override default buffer limit (500kb) to 1MB per stream (stdout/stderr). Buffering captures output up to this limit; excess is truncated with markers.
- **Kill Tree**: `dispatch.kill('SIGKILL')` → children via `pgrep -P`.

## API

Full JSDoc in `lib/*.js`. Key exports:

| Utility | Description |
|---------|-------------|
| `SH`cmd`` | Template → {@link SHDispatch} |
| `cd(dir)` | `process.chdir()` |
| `sleep('1s')` | Promise delay |
| `retry(3, '1s', fn)` | Retry w/ delay/gen |
| `userIn(prompt)` | `{ input: Promise, abort() }` |
| `Test` | Test runner |
| `AsyncTracker` | Async leak detector |
| `parseArgs(argv)` | CLI parser |
| `bashEscape(str)` | Shell-safe string |

See [types/index.d.ts](types/index.d.ts) for TS defs.

## Development

```bash
npm run types    # Generate types/
npm test         # Run scenarios/sh.js
npm run release  # Pack for publish
npm run publish  # npm publish
```

Repo: [Codeberg](https://codeberg.org/duin/sh) | Issues: [Codeberg Issues](https://codeberg.org/duin/sh/issues)

## License

Apache-2.0 © [Jorrit Duin](mailto:jorrit.duin+sh@gmail.com)
