@j-o-r/sh
Version:
Execute shell commands on Linux-based systems from javascript
169 lines (154 loc) • 4.47 kB
JavaScript
import SHExecute from './SHExecute.js';
/**
* @typedef {Object} SpawnSyncResponse
* @property {number|null} status - Exit code (null if signal).
* @property {string|null} signal - Terminating signal.
* @property {(string|Buffer|null)[]} output - [stdin, stdout, stderr].
* @property {number} pid - Process ID.
* @property {string|Buffer|null} stdout - Captured stdout.
* @property {string|Buffer|null} stderr - Captured stderr.
*/
/**
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
* @description Stdio config for a stream:
* - `'pipe'`: Pipe to parent.
* - `'ignore'`: Discard.
* - `'inherit'`: From/to parent.
* - `number`: FD.
*/
/**
* @typedef {Array<StdioOption> | StdioOption} StdioOptions
* @description Stdio array [stdin, stdout, stderr] or single value (applied to all).
* @example ['inherit', 'pipe', 'pipe'] // Default: inherit stdin, pipe out/err
*/
/**
* Core options for SH/SHDispatch.
*
* Defaults (merged from global SH):
* - `cwd`: `process.cwd()`
* - `env`: `process.env`
* - `shell`: `'bash'` (string/true → shell; false → no-shell `/usr/bin/env -S`)
* - `stdio`: `['inherit', 'pipe', 'pipe']`
* - `timeout`: `0` (no timeout; rolling on data)
* - `maxBuffer`: `512000` (500 kb (per stream in SHExecute))
*
* Prefix (`.options(undefined, prefix)`) only for shell mode.
*
* @example { timeout: '5s', stdio: 'inherit', shell: false, maxBuffer: 10 * 1024}
*/
const SHOptions = {
cwd: process.cwd(),
env: process.env,
shell: 'bash',
stdio: ['inherit', 'pipe', 'pipe'],
timeout: 0,
};
/**
* Merges user options into predefined defaults (non-destructive).
*
* Only copies defined keys.
*
* @param {typeof SHOptions} predefined - Base options.
* @param {Partial<typeof SHOptions>} options - Overrides.
* @returns {typeof SHOptions} Merged options.
*/
const mergeOptions = (predefined, options) => {
const mergedObj = { ...predefined };
for (const key in options) {
if (options[key] !== undefined) {
mergedObj[key] = options[key];
}
}
return mergedObj;
};
/**
* High-level command dispatcher.
*
* Created by {@link SH`cmd`}; chain `.options()` then `.run()`.
*
* Delegates to {@link SHExecute} for exec/timeout/buffer/kill.
*
* @example
* const dispatch = SH`ls -la`.options({ timeout: '2s' });
* const out = await dispatch.run();
*/
class SHDispatch {
#prefix = '';
#cmd = '';
#options = SHOptions;
/** @type {SHExecute | null} */
#proc = null;
/**
* @param {string} cmd - Command string.
* @param {Partial<typeof SHOptions>} [options] - Initial options.
* @param {string} [prefix] - Shell prefix (e.g., 'set -euo pipefail').
* @throws {Error} Invalid/empty cmd.
*/
constructor(cmd, options = {}, prefix) {
if (typeof cmd !== 'string' || cmd === '') {
throw new Error('Undefined command');
}
this.#cmd = cmd;
this.options(options, prefix);
}
/**
* Updates options/prefix; resets to defaults + user overrides (non-cumulative).
*
* Strings for `stdio` → array fill.
*
* @param {Partial<typeof SHOptions>} [options] - New options.
* @param {string} [prefix] - New prefix.
* @returns {SHDispatch} Self for chaining.
*/
options(options, prefix) {
if (typeof prefix === 'string') {
this.#prefix = prefix;
}
if (!options) {
return this;
}
if (options.stdio && typeof options.stdio === 'string') {
// convert stdio to array
const io = options.stdio;
options.stdio = Array(3).fill(io);
}
this.#options = mergeOptions(SHOptions, options);
return this;
}
/**
* Async run: Captures stdout; rejects on error/timeout.
*
* @param {string} [payload] - Stdin payload.
* @returns {Promise<string>} Stdout.
*/
run(payload) {
this.#proc = new SHExecute(this.#cmd, this.#prefix, this.#options);
return this.#proc.run(payload);
}
/**
* Sync run: Full Node SpawnSyncReturns.
*
* Good for TTY takeovers (e.g., vim).
*
* @param {string} [payload] - Stdin payload.
* @returns {import('child_process').SpawnSyncReturns<Buffer>}
*/
runSync(payload) {
return new SHExecute(this.#cmd, this.#prefix, this.#options).runSync(payload);
}
/**
* Kills running process + children.
*
* @param {number | string} [signal='SIGTERM'] - Signal.
* @returns {Promise<number[]>} Killed PIDs.
*/
async kill(signal = 'SIGTERM') {
let res = [];
if (this.#proc) {
res = await this.#proc.kill(signal);
this.#proc = null;
}
return res;
}
}
export default SHDispatch;