@zokugun/exec-lite 0.2.0 → 0.3.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 CHANGED
@@ -126,6 +126,47 @@ Spawns a child process and returns a `Subprocess` instance. The process starts i
126
126
  - `exec` does not throw. Errors surface through the `fails` flag of the `Result`.
127
127
  - On Windows, batch files (`.bat`, `.cmd`) are wrapped in `cmd.exe` with verbatim argument handling.
128
128
 
129
+ #### `createCommand`
130
+
131
+ ```typescript
132
+ import { createCommand } from '@zokugun/exec-lite';
133
+
134
+ const git = createCommand('git', [], { resultMode: 'merged' });
135
+
136
+ await git(['status'], { cwd: repository });
137
+ await git.withOptions({ cwd: repository })(['status']);
138
+ await git.toCwdFirst()(repository, 'status');
139
+ await git.toFlatArgs()('status');
140
+ ```
141
+
142
+ ```typescript
143
+ type Command = {
144
+ (args?: readonly string[] | null, options?: Partial<Options>): Subprocess;
145
+ toCwdFirst(): (cwd: string, ...args: string[]) => Subprocess;
146
+ toFlatArgs(): (...args: string[]) => Subprocess;
147
+ withOptions(options: Partial<Options>): Command;
148
+ }
149
+ ```
150
+
151
+ Creates a reusable command with optional default arguments and options.
152
+
153
+ ### Merged Result Mode
154
+
155
+ Set `resultMode: 'merged'` to make the awaited subprocess resolve directly to a string containing combined stdout and stderr, rather than the default `Result` object:
156
+
157
+ ```typescript
158
+ const task = exec('git', ['status'], { resultMode: 'merged' });
159
+ const output = await task;
160
+
161
+ console.log(output);
162
+
163
+ if(task.failed) {
164
+ console.error(`Command failed with exit code: ${task.exitCode}`);
165
+ }
166
+ ```
167
+
168
+ The subprocess does not reject on a non-zero exit. Check `.failed` and, when needed, `.exitCode` or `.error` for process status and errors.
169
+
129
170
  ---
130
171
 
131
172
  ### Classes
@@ -165,10 +206,13 @@ The class behind every subprocess returned by `exec`. You rarely need to constru
165
206
 
166
207
  #### `ExecResult`
167
208
 
168
- A discriminated union wrapping success or failure:
209
+ A structured success or failure result by default. In merged mode, this resolves directly to a string:
169
210
 
170
211
  ```typescript
171
- type ExecResult = Result<ExecSuccess, ExecFailure>;
212
+ type ResultMode = 'result' | 'merged';
213
+ type ExecResult<M extends ResultMode = 'result'> = M extends 'merged'
214
+ ? string
215
+ : Result<ExecSuccess, ExecFailure>;
172
216
  ```
173
217
 
174
218
  ---
@@ -179,10 +223,12 @@ Returned when the process exits with code 0 and no error occurred.
179
223
 
180
224
  ```typescript
181
225
  type ExecSuccess = {
182
- all: string; // combined stdout + stderr
226
+ args: readonly string[];
183
227
  command: string;
228
+ cwd: string;
184
229
  durationMs: number;
185
230
  exitCode: number;
231
+ merged: string; // combined stdout + stderr
186
232
  stdout: string;
187
233
  stderr: string;
188
234
  };
@@ -196,12 +242,14 @@ Returned when the process errors, exits non-zero, or gets killed.
196
242
 
197
243
  ```typescript
198
244
  type ExecFailure = {
199
- all?: string;
245
+ args: readonly string[];
200
246
  canceled: boolean; // true if aborted via signal
201
247
  command: string;
248
+ cwd?: string;
202
249
  durationMs: number;
203
250
  error?: Error;
204
251
  exitCode?: number;
252
+ merged?: string;
205
253
  signalCode?: string;
206
254
  stderr?: string;
207
255
  stdout?: string;
@@ -221,24 +269,24 @@ The shape returned by `exec()`. It is:
221
269
  ```typescript
222
270
  type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & { ... };
223
271
  ```
224
-
225
272
  ---
226
273
 
227
274
  #### `Options`
228
275
 
229
276
  Extended `SpawnOptions` with extra fields:
230
277
 
231
- | Field | Type | Default | Description |
232
- | ------------------- | ------------------------------------- | -------------------- | ------------------------------------- |
233
- | `cwd` | `string` | `process.cwd()` | Working directory |
234
- | `env` | `Record<string, string>` | inherited + computed | Environment variables |
235
- | `localDir` | `string` | `cwd` | Directory to resolve local paths from |
236
- | `preferLocal` | `boolean` | `false` | Prepends `node_modules/.bin` to PATH |
237
- | `signal` | `AbortSignal` | none | Abort signal for cancellation |
238
- | `stdin` | `string \| Subprocess \| { process }` | `pipe` | Input source |
239
- | `stdio` | | none | Standard I/O configurations. |
240
- | `stdout` / `stderr` | `StdioOption` | none | Stream configuration |
241
- | `timeout` | `number` | none | Auto-abort after N milliseconds |
278
+ | Field | Type | Default | Description |
279
+ | ------------------- | ------------------------------------- | -------------------- | ---------------------------------------------------- |
280
+ | `cwd` | `string` | `process.cwd()` | Working directory |
281
+ | `env` | `Record<string, string>` | inherited + computed | Environment variables |
282
+ | `localDir` | `string` | `cwd` | Directory to resolve local paths from |
283
+ | `preferLocal` | `boolean` | `false` | Prepends `node_modules/.bin` to PATH |
284
+ | `resultMode` | `'result' \| 'merged'` | `'result'` | Resolve to a structured result or merged output text |
285
+ | `signal` | `AbortSignal` | none | Abort signal for cancellation |
286
+ | `stdin` | `string \| Subprocess \| { process }` | `'pipe'` | Input source |
287
+ | `stdio` | | none | Standard I/O configurations. |
288
+ | `stdout` / `stderr` | `StdioOption` | none | Stream configuration |
289
+ | `timeout` | `number` | none | Auto-abort after N milliseconds |
242
290
 
243
291
  Contributions
244
292
  -------------
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCommand = createCommand;
4
+ const exec_js_1 = require("./exec.cjs");
5
+ function createCommand(command, args = [], options = {}) {
6
+ const create = (options) => {
7
+ const run = ((runArgs, runOptions) => {
8
+ return (0, exec_js_1.exec)(command, [...(args ?? []), ...(runArgs ?? [])], {
9
+ ...options,
10
+ ...runOptions,
11
+ });
12
+ });
13
+ run.withOptions = (overrides) => create({ ...options, ...overrides });
14
+ run.toCwdFirst = () => ((cwd, ...runArgs) => run(runArgs, { cwd }));
15
+ run.toFlatArgs = () => ((...runArgs) => run(runArgs));
16
+ return run;
17
+ };
18
+ return create(options);
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command, Options, ResultMode, Subprocess } from './types.cjs';
2
+ declare function createCommand<M extends ResultMode = 'result'>(command: string, args?: readonly string[] | null, options?: Partial<Options<M>>): Command<M>;
3
+ export { type Command, type Options, type ResultMode, type Subprocess, createCommand, };
@@ -16,6 +16,7 @@ class ExecProcess {
16
16
  _aborted = false;
17
17
  _args;
18
18
  _command;
19
+ _cwd;
19
20
  _failed = false;
20
21
  _options;
21
22
  _process;
@@ -75,6 +76,7 @@ class ExecProcess {
75
76
  this._start = process.hrtime.bigint();
76
77
  this.reset();
77
78
  const normalizedOptions = (0, normalizeOptions_js_1.normalizeOptions)(this._options);
79
+ this._cwd = normalizedOptions.cwd;
78
80
  if (normalizedOptions.signal) {
79
81
  this._signal = normalizedOptions.signal;
80
82
  }
@@ -138,7 +140,12 @@ class ExecProcess {
138
140
  } // }}}
139
141
  // eslint-disable-next-line ts/promise-function-async
140
142
  then(onfulfilled, onrejected) {
141
- return this.toResult().then(onfulfilled, onrejected);
143
+ return this.toResult().then((result) => {
144
+ if (onfulfilled) {
145
+ return onfulfilled(result);
146
+ }
147
+ return result;
148
+ }, onrejected);
142
149
  } // }}}
143
150
  onClose() {
144
151
  if (this._resolveClose) {
@@ -165,26 +172,31 @@ class ExecProcess {
165
172
  } // }}}
166
173
  toFailure(error) {
167
174
  this._failed = true;
175
+ if (this._options.resultMode === 'merged') {
176
+ return (0, xtry_1.stringifyError)(error);
177
+ }
168
178
  const result = {
169
- all: '',
179
+ args: this._args,
170
180
  canceled: this._signal?.aborted ?? false,
171
181
  command: this._command,
182
+ cwd: this._cwd,
172
183
  durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
173
184
  error,
174
185
  exitCode: this.exitCode,
186
+ merged: '',
175
187
  signalCode: this.signalCode,
176
188
  };
177
- return result;
189
+ return (0, xtry_1.err)(result);
178
190
  } // }}}
179
191
  async toResult() {
180
192
  if (this._thrownError) {
181
- return (0, xtry_1.err)(this.toFailure(this._thrownError));
193
+ return this.toFailure(this._thrownError);
182
194
  }
183
195
  const task = this._process;
184
196
  if (!task) {
185
- return (0, xtry_1.err)(this.toFailure(new Error('No process was started')));
197
+ return this.toFailure(new Error('No process was started'));
186
198
  }
187
- const context = { all: '' };
199
+ const context = { merged: '' };
188
200
  const [stdout, stderr] = await Promise.all([
189
201
  this._streamOut ? (0, readStream_js_1.readStream)(this._streamOut, context) : '',
190
202
  this._streamErr ? (0, readStream_js_1.readStream)(this._streamErr, context) : '',
@@ -196,13 +208,21 @@ class ExecProcess {
196
208
  }
197
209
  task.removeAllListeners();
198
210
  if (this._thrownError) {
199
- return (0, xtry_1.err)(this.toFailure(this._thrownError));
211
+ return this.toFailure(this._thrownError);
212
+ }
213
+ if (this._options.resultMode === 'merged') {
214
+ if ((this.exitCode !== 0 && this.exitCode !== undefined) || this.signalCode !== undefined) {
215
+ this._failed = true;
216
+ }
217
+ return context.merged;
200
218
  }
201
219
  const result = {
202
- all: context.all,
220
+ args: this._args,
203
221
  command: this._command,
222
+ cwd: this._cwd,
204
223
  durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
205
224
  exitCode: this.exitCode,
225
+ merged: context.merged,
206
226
  stderr,
207
227
  stdout,
208
228
  };
@@ -1,10 +1,11 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
  import type { Readable } from 'node:stream';
3
- import type { ExecFailure, ExecResult, KillSignal, Options, Subprocess } from './types.cjs';
4
- export declare class ExecProcess implements Subprocess {
3
+ import type { ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, ResultMode, Subprocess } from './types.cjs';
4
+ declare class ExecProcess implements Subprocess<ResultMode> {
5
5
  protected _aborted: boolean;
6
6
  protected _args: readonly string[];
7
7
  protected _command: string;
8
+ protected _cwd: string | undefined;
8
9
  protected _failed: boolean;
9
10
  protected _options: Partial<Options>;
10
11
  protected _process?: ChildProcess;
@@ -25,13 +26,16 @@ export declare class ExecProcess implements Subprocess {
25
26
  get process(): ChildProcess | undefined;
26
27
  get signalCode(): string | undefined;
27
28
  kill(signal?: KillSignal): boolean;
28
- pipe(command: string, args?: readonly string[], options?: Partial<Options>): Subprocess;
29
+ pipe<N extends ResultMode = 'result'>(command: string, args?: readonly string[], options?: Partial<Options> & {
30
+ resultMode?: N;
31
+ }): Subprocess<N>;
29
32
  spawn(): void;
30
33
  [Symbol.asyncIterator](): AsyncIterator<string>;
31
- then<TResult1 = ExecResult, TResult2 = never>(onfulfilled?: ((value: ExecResult) => PromiseLike<TResult1> | TResult1) | null, onrejected?: ((reason: unknown) => PromiseLike<TResult2> | TResult2) | null): Promise<TResult1 | TResult2>;
34
+ then<TResult1 = ExecResult<ResultMode>, TResult2 = never>(onfulfilled?: ((value: ExecResult<ResultMode>) => PromiseLike<TResult1> | TResult1) | null, onrejected?: ((reason: unknown) => PromiseLike<TResult2> | TResult2) | null): Promise<TResult1 | TResult2>;
32
35
  protected onClose(): void;
33
36
  protected onError(error: Error): void;
34
37
  protected reset(): void;
35
- protected toFailure(error: Error): ExecFailure;
36
- protected toResult(): Promise<ExecResult>;
38
+ protected toFailure(error: Error): ExecResult<ResultMode>;
39
+ protected toResult(): Promise<ExecResult<ResultMode>>;
37
40
  }
41
+ export { type ExecFailure, type ExecResult, type ExecSuccess, type KillSignal, type Options, type ResultMode, type Subprocess, ExecProcess, };
@@ -1,2 +1,3 @@
1
- import type { Options, Subprocess } from './types.cjs';
2
- export declare function exec(command: string, args?: readonly string[] | null, options?: Partial<Options>): Subprocess;
1
+ import type { Options, ResultMode, Subprocess } from './types.cjs';
2
+ declare function exec<M extends ResultMode = 'result'>(command: string, args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
3
+ export { type Options, type ResultMode, type Subprocess, exec, };
package/lib/cjs/index.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.exec = exports.ExecProcess = void 0;
3
+ exports.exec = exports.ExecProcess = exports.createCommand = void 0;
4
+ var create_command_js_1 = require("./create-command.cjs");
5
+ Object.defineProperty(exports, "createCommand", { enumerable: true, get: function () { return create_command_js_1.createCommand; } });
4
6
  var exec_process_js_1 = require("./exec-process.cjs");
5
7
  Object.defineProperty(exports, "ExecProcess", { enumerable: true, get: function () { return exec_process_js_1.ExecProcess; } });
6
8
  var exec_js_1 = require("./exec.cjs");
@@ -1,3 +1,4 @@
1
+ export type { Command, ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, ResultMode, StdinOption, StdioOption, Subprocess, } from './types.cjs';
2
+ export { createCommand } from './create-command.cjs';
1
3
  export { ExecProcess } from './exec-process.cjs';
2
4
  export { exec } from './exec.cjs';
3
- export { ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, StdinOption, StdioOption, Subprocess, } from './types.cjs';
@@ -1,37 +1,49 @@
1
1
  import type { ChildProcess, SpawnOptions as ProcessSpawnOptions } from 'node:child_process';
2
2
  import type { Result } from '@zokugun/xtry';
3
+ export type Command<M extends ResultMode = 'result'> = {
4
+ (args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
5
+ toCwdFirst(): (cwd: string, ...args: string[]) => Subprocess<M>;
6
+ toFlatArgs(): (...args: string[]) => Subprocess<M>;
7
+ withOptions(options: Partial<Options<M>>): Command<M>;
8
+ };
3
9
  export type ExecFailure = {
4
- all?: string;
10
+ args: readonly string[];
5
11
  canceled: boolean;
12
+ cwd?: string;
6
13
  command: string;
7
14
  durationMs: number;
8
15
  error?: Error;
9
16
  exitCode?: number;
17
+ merged?: string;
10
18
  pipedFrom?: ExecResult;
11
19
  signalCode?: string;
12
20
  stderr?: string;
13
21
  stdout?: string;
14
22
  };
15
- export type ExecResult = Result<ExecSuccess, ExecFailure>;
23
+ export type ResultMode = 'merged' | 'result';
24
+ export type ExecResult<M extends ResultMode = 'result'> = M extends 'merged' ? string : Result<ExecSuccess, ExecFailure>;
16
25
  export type ExecSuccess = {
17
- all: string;
26
+ args: readonly string[];
18
27
  command: string;
28
+ cwd: string;
19
29
  durationMs: number;
20
30
  error?: undefined;
21
31
  exitCode: number;
32
+ merged: string;
22
33
  pipedFrom?: ExecResult;
23
34
  stderr: string;
24
35
  stdout: string;
25
36
  };
26
37
  export type KillSignal = Parameters<ChildProcess['kill']>[0];
27
- export type Options = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
38
+ export type Options<M extends ResultMode = ResultMode> = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
28
39
  cwd?: string;
29
40
  env: Readonly<Partial<Record<string, string>>>;
30
41
  localDir: string;
31
42
  preferLocal: boolean;
43
+ resultMode: M;
32
44
  signal: AbortSignal;
33
45
  stderr: StdioOption;
34
- stdin: ExecResult | StdinOption | Subprocess;
46
+ stdin: ExecResult | StdinOption | Subprocess<ResultMode>;
35
47
  stdio: readonly [StdinOption, ...readonly StdioOption[]] | SpawnOptions['stdio'];
36
48
  stdout: StdioOption;
37
49
  }>>;
@@ -42,7 +54,7 @@ export type StdinOption = StdioOption | {
42
54
  readonly string?: string;
43
55
  };
44
56
  export type StdioOption = Readonly<Exclude<SpawnOptions['stdio'], undefined>[number]>;
45
- export type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & {
57
+ export type Subprocess<M extends ResultMode = 'result'> = AsyncIterable<string> & PromiseLike<ExecResult<M>> & {
46
58
  get aborted(): boolean;
47
59
  get error(): Error | undefined;
48
60
  get exitCode(): number | undefined;
@@ -50,7 +62,9 @@ export type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & {
50
62
  kill: (signal?: KillSignal) => boolean;
51
63
  get killed(): boolean;
52
64
  get pid(): number | undefined;
53
- pipe: (command: string, args?: readonly string[], options?: Partial<Options>) => Subprocess;
65
+ pipe: <N extends ResultMode = 'result'>(command: string, args?: readonly string[], options?: Partial<Options> & {
66
+ resultMode?: N;
67
+ }) => Subprocess<N>;
54
68
  get process(): ChildProcess | undefined;
55
69
  get signalCode(): string | undefined;
56
70
  };
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.normalizeOptions = normalizeOptions;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const computeEnv_js_1 = require("./env/computeEnv.cjs");
9
- const combineSignals_js_1 = require("../combineSignals.cjs");
9
+ const combineSignals_js_1 = require("./combineSignals.cjs");
10
10
  function normalizeOptions(options) {
11
- const { cwd, env, preferLocal, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
11
+ const { cwd, env, preferLocal, resultMode: _resultMode, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
12
12
  const spawnOptions = restOptions;
13
13
  spawnOptions.cwd = node_path_1.default.resolve(cwd ?? process.cwd());
14
14
  spawnOptions.env = (0, computeEnv_js_1.computeEnv)(env, preferLocal ?? false, options.localDir ?? spawnOptions.cwd);
@@ -6,7 +6,7 @@ async function readStream(stream, context) {
6
6
  try {
7
7
  for await (const chunk of stream) {
8
8
  output += String(chunk);
9
- context.all += String(chunk);
9
+ context.merged += String(chunk);
10
10
  }
11
11
  }
12
12
  catch {
@@ -1,5 +1,5 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  export type StreamContext = {
3
- all: string;
3
+ merged: string;
4
4
  };
5
5
  export declare function readStream(stream: Readable, context: StreamContext): Promise<string>;
@@ -0,0 +1,3 @@
1
+ import type { Command, Options, ResultMode, Subprocess } from './types.mjs';
2
+ declare function createCommand<M extends ResultMode = 'result'>(command: string, args?: readonly string[] | null, options?: Partial<Options<M>>): Command<M>;
3
+ export { type Command, type Options, type ResultMode, type Subprocess, createCommand, };
@@ -0,0 +1,17 @@
1
+ import { exec } from './exec.mjs';
2
+ function createCommand(command, args = [], options = {}) {
3
+ const create = (options) => {
4
+ const run = ((runArgs, runOptions) => {
5
+ return exec(command, [...(args ?? []), ...(runArgs ?? [])], {
6
+ ...options,
7
+ ...runOptions,
8
+ });
9
+ });
10
+ run.withOptions = (overrides) => create({ ...options, ...overrides });
11
+ run.toCwdFirst = () => ((cwd, ...runArgs) => run(runArgs, { cwd }));
12
+ run.toFlatArgs = () => ((...runArgs) => run(runArgs));
13
+ return run;
14
+ };
15
+ return create(options);
16
+ }
17
+ export { createCommand, };
@@ -1,10 +1,11 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
  import type { Readable } from 'node:stream';
3
- import type { ExecFailure, ExecResult, KillSignal, Options, Subprocess } from './types.mjs';
4
- export declare class ExecProcess implements Subprocess {
3
+ import type { ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, ResultMode, Subprocess } from './types.mjs';
4
+ declare class ExecProcess implements Subprocess<ResultMode> {
5
5
  protected _aborted: boolean;
6
6
  protected _args: readonly string[];
7
7
  protected _command: string;
8
+ protected _cwd: string | undefined;
8
9
  protected _failed: boolean;
9
10
  protected _options: Partial<Options>;
10
11
  protected _process?: ChildProcess;
@@ -25,13 +26,16 @@ export declare class ExecProcess implements Subprocess {
25
26
  get process(): ChildProcess | undefined;
26
27
  get signalCode(): string | undefined;
27
28
  kill(signal?: KillSignal): boolean;
28
- pipe(command: string, args?: readonly string[], options?: Partial<Options>): Subprocess;
29
+ pipe<N extends ResultMode = 'result'>(command: string, args?: readonly string[], options?: Partial<Options> & {
30
+ resultMode?: N;
31
+ }): Subprocess<N>;
29
32
  spawn(): void;
30
33
  [Symbol.asyncIterator](): AsyncIterator<string>;
31
- then<TResult1 = ExecResult, TResult2 = never>(onfulfilled?: ((value: ExecResult) => PromiseLike<TResult1> | TResult1) | null, onrejected?: ((reason: unknown) => PromiseLike<TResult2> | TResult2) | null): Promise<TResult1 | TResult2>;
34
+ then<TResult1 = ExecResult<ResultMode>, TResult2 = never>(onfulfilled?: ((value: ExecResult<ResultMode>) => PromiseLike<TResult1> | TResult1) | null, onrejected?: ((reason: unknown) => PromiseLike<TResult2> | TResult2) | null): Promise<TResult1 | TResult2>;
32
35
  protected onClose(): void;
33
36
  protected onError(error: Error): void;
34
37
  protected reset(): void;
35
- protected toFailure(error: Error): ExecFailure;
36
- protected toResult(): Promise<ExecResult>;
38
+ protected toFailure(error: Error): ExecResult<ResultMode>;
39
+ protected toResult(): Promise<ExecResult<ResultMode>>;
37
40
  }
41
+ export { type ExecFailure, type ExecResult, type ExecSuccess, type KillSignal, type Options, type ResultMode, type Subprocess, ExecProcess, };
@@ -1,15 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import readline from 'node:readline';
3
- import { err, ok } from '@zokugun/xtry';
3
+ import { err, ok, stringifyError } from '@zokugun/xtry';
4
4
  import { exec } from './exec.mjs';
5
5
  import { normalizeCommand } from './utils/command/normalizeCommand.mjs';
6
6
  import { normalizeOptions } from './utils/normalizeOptions.mjs';
7
7
  import { combineStreams } from './utils/stream/combineStreams.mjs';
8
8
  import { readStream } from './utils/stream/readStream.mjs';
9
- export class ExecProcess {
9
+ class ExecProcess {
10
10
  _aborted = false;
11
11
  _args;
12
12
  _command;
13
+ _cwd;
13
14
  _failed = false;
14
15
  _options;
15
16
  _process;
@@ -69,6 +70,7 @@ export class ExecProcess {
69
70
  this._start = process.hrtime.bigint();
70
71
  this.reset();
71
72
  const normalizedOptions = normalizeOptions(this._options);
73
+ this._cwd = normalizedOptions.cwd;
72
74
  if (normalizedOptions.signal) {
73
75
  this._signal = normalizedOptions.signal;
74
76
  }
@@ -132,7 +134,12 @@ export class ExecProcess {
132
134
  } // }}}
133
135
  // eslint-disable-next-line ts/promise-function-async
134
136
  then(onfulfilled, onrejected) {
135
- return this.toResult().then(onfulfilled, onrejected);
137
+ return this.toResult().then((result) => {
138
+ if (onfulfilled) {
139
+ return onfulfilled(result);
140
+ }
141
+ return result;
142
+ }, onrejected);
136
143
  } // }}}
137
144
  onClose() {
138
145
  if (this._resolveClose) {
@@ -159,26 +166,31 @@ export class ExecProcess {
159
166
  } // }}}
160
167
  toFailure(error) {
161
168
  this._failed = true;
169
+ if (this._options.resultMode === 'merged') {
170
+ return stringifyError(error);
171
+ }
162
172
  const result = {
163
- all: '',
173
+ args: this._args,
164
174
  canceled: this._signal?.aborted ?? false,
165
175
  command: this._command,
176
+ cwd: this._cwd,
166
177
  durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
167
178
  error,
168
179
  exitCode: this.exitCode,
180
+ merged: '',
169
181
  signalCode: this.signalCode,
170
182
  };
171
- return result;
183
+ return err(result);
172
184
  } // }}}
173
185
  async toResult() {
174
186
  if (this._thrownError) {
175
- return err(this.toFailure(this._thrownError));
187
+ return this.toFailure(this._thrownError);
176
188
  }
177
189
  const task = this._process;
178
190
  if (!task) {
179
- return err(this.toFailure(new Error('No process was started')));
191
+ return this.toFailure(new Error('No process was started'));
180
192
  }
181
- const context = { all: '' };
193
+ const context = { merged: '' };
182
194
  const [stdout, stderr] = await Promise.all([
183
195
  this._streamOut ? readStream(this._streamOut, context) : '',
184
196
  this._streamErr ? readStream(this._streamErr, context) : '',
@@ -190,13 +202,21 @@ export class ExecProcess {
190
202
  }
191
203
  task.removeAllListeners();
192
204
  if (this._thrownError) {
193
- return err(this.toFailure(this._thrownError));
205
+ return this.toFailure(this._thrownError);
206
+ }
207
+ if (this._options.resultMode === 'merged') {
208
+ if ((this.exitCode !== 0 && this.exitCode !== undefined) || this.signalCode !== undefined) {
209
+ this._failed = true;
210
+ }
211
+ return context.merged;
194
212
  }
195
213
  const result = {
196
- all: context.all,
214
+ args: this._args,
197
215
  command: this._command,
216
+ cwd: this._cwd,
198
217
  durationMs: Number(process.hrtime.bigint() - this._start) / 1e6,
199
218
  exitCode: this.exitCode,
219
+ merged: context.merged,
200
220
  stderr,
201
221
  stdout,
202
222
  };
@@ -212,3 +232,4 @@ export class ExecProcess {
212
232
  return ok(result);
213
233
  } // }}}
214
234
  }
235
+ export { ExecProcess, };
@@ -1,2 +1,3 @@
1
- import type { Options, Subprocess } from './types.mjs';
2
- export declare function exec(command: string, args?: readonly string[] | null, options?: Partial<Options>): Subprocess;
1
+ import type { Options, ResultMode, Subprocess } from './types.mjs';
2
+ declare function exec<M extends ResultMode = 'result'>(command: string, args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
3
+ export { type Options, type ResultMode, type Subprocess, exec, };
package/lib/esm/exec.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ExecProcess } from './exec-process.mjs';
2
- export function exec(command, args, options) {
2
+ function exec(command, args, options) {
3
3
  const subprocess = new ExecProcess(command, args, options);
4
4
  subprocess.spawn();
5
5
  return subprocess;
6
6
  }
7
+ export { exec, };
@@ -1,3 +1,4 @@
1
+ export type { Command, ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, ResultMode, StdinOption, StdioOption, Subprocess, } from './types.mjs';
2
+ export { createCommand } from './create-command.mjs';
1
3
  export { ExecProcess } from './exec-process.mjs';
2
4
  export { exec } from './exec.mjs';
3
- export { ExecFailure, ExecResult, ExecSuccess, KillSignal, Options, StdinOption, StdioOption, Subprocess, } from './types.mjs';
package/lib/esm/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
+ export { createCommand } from './create-command.mjs';
1
2
  export { ExecProcess } from './exec-process.mjs';
2
3
  export { exec } from './exec.mjs';
@@ -1,37 +1,49 @@
1
1
  import type { ChildProcess, SpawnOptions as ProcessSpawnOptions } from 'node:child_process';
2
2
  import type { Result } from '@zokugun/xtry';
3
+ export type Command<M extends ResultMode = 'result'> = {
4
+ (args?: readonly string[] | null, options?: Partial<Options<M>>): Subprocess<M>;
5
+ toCwdFirst(): (cwd: string, ...args: string[]) => Subprocess<M>;
6
+ toFlatArgs(): (...args: string[]) => Subprocess<M>;
7
+ withOptions(options: Partial<Options<M>>): Command<M>;
8
+ };
3
9
  export type ExecFailure = {
4
- all?: string;
10
+ args: readonly string[];
5
11
  canceled: boolean;
12
+ cwd?: string;
6
13
  command: string;
7
14
  durationMs: number;
8
15
  error?: Error;
9
16
  exitCode?: number;
17
+ merged?: string;
10
18
  pipedFrom?: ExecResult;
11
19
  signalCode?: string;
12
20
  stderr?: string;
13
21
  stdout?: string;
14
22
  };
15
- export type ExecResult = Result<ExecSuccess, ExecFailure>;
23
+ export type ResultMode = 'merged' | 'result';
24
+ export type ExecResult<M extends ResultMode = 'result'> = M extends 'merged' ? string : Result<ExecSuccess, ExecFailure>;
16
25
  export type ExecSuccess = {
17
- all: string;
26
+ args: readonly string[];
18
27
  command: string;
28
+ cwd: string;
19
29
  durationMs: number;
20
30
  error?: undefined;
21
31
  exitCode: number;
32
+ merged: string;
22
33
  pipedFrom?: ExecResult;
23
34
  stderr: string;
24
35
  stdout: string;
25
36
  };
26
37
  export type KillSignal = Parameters<ChildProcess['kill']>[0];
27
- export type Options = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
38
+ export type Options<M extends ResultMode = ResultMode> = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
28
39
  cwd?: string;
29
40
  env: Readonly<Partial<Record<string, string>>>;
30
41
  localDir: string;
31
42
  preferLocal: boolean;
43
+ resultMode: M;
32
44
  signal: AbortSignal;
33
45
  stderr: StdioOption;
34
- stdin: ExecResult | StdinOption | Subprocess;
46
+ stdin: ExecResult | StdinOption | Subprocess<ResultMode>;
35
47
  stdio: readonly [StdinOption, ...readonly StdioOption[]] | SpawnOptions['stdio'];
36
48
  stdout: StdioOption;
37
49
  }>>;
@@ -42,7 +54,7 @@ export type StdinOption = StdioOption | {
42
54
  readonly string?: string;
43
55
  };
44
56
  export type StdioOption = Readonly<Exclude<SpawnOptions['stdio'], undefined>[number]>;
45
- export type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & {
57
+ export type Subprocess<M extends ResultMode = 'result'> = AsyncIterable<string> & PromiseLike<ExecResult<M>> & {
46
58
  get aborted(): boolean;
47
59
  get error(): Error | undefined;
48
60
  get exitCode(): number | undefined;
@@ -50,7 +62,9 @@ export type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & {
50
62
  kill: (signal?: KillSignal) => boolean;
51
63
  get killed(): boolean;
52
64
  get pid(): number | undefined;
53
- pipe: (command: string, args?: readonly string[], options?: Partial<Options>) => Subprocess;
65
+ pipe: <N extends ResultMode = 'result'>(command: string, args?: readonly string[], options?: Partial<Options> & {
66
+ resultMode?: N;
67
+ }) => Subprocess<N>;
54
68
  get process(): ChildProcess | undefined;
55
69
  get signalCode(): string | undefined;
56
70
  };
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { computeEnv } from './env/computeEnv.mjs';
3
- import { combineSignals } from '../combineSignals.mjs';
3
+ import { combineSignals } from './combineSignals.mjs';
4
4
  export function normalizeOptions(options) {
5
- const { cwd, env, preferLocal, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
5
+ const { cwd, env, preferLocal, resultMode: _resultMode, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
6
6
  const spawnOptions = restOptions;
7
7
  spawnOptions.cwd = path.resolve(cwd ?? process.cwd());
8
8
  spawnOptions.env = computeEnv(env, preferLocal ?? false, options.localDir ?? spawnOptions.cwd);
@@ -1,5 +1,5 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  export type StreamContext = {
3
- all: string;
3
+ merged: string;
4
4
  };
5
5
  export declare function readStream(stream: Readable, context: StreamContext): Promise<string>;
@@ -3,7 +3,7 @@ export async function readStream(stream, context) {
3
3
  try {
4
4
  for await (const chunk of stream) {
5
5
  output += String(chunk);
6
- context.all += String(chunk);
6
+ context.merged += String(chunk);
7
7
  }
8
8
  }
9
9
  catch {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zokugun/exec-lite",
3
3
  "description": "A lightweight tool for running shell commands and reading their output easily.",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "author": {
6
6
  "name": "Baptiste Augrain",
7
7
  "email": "daiyam@zokugun.org"
@@ -20,6 +20,18 @@
20
20
  ".": {
21
21
  "import": "./lib/esm/index.mjs",
22
22
  "require": "./lib/cjs/index.cjs"
23
+ },
24
+ "./command-line": {
25
+ "import": "./lib/esm/command-line.mjs",
26
+ "require": "./lib/cjs/command-line.cjs"
27
+ },
28
+ "./exec-process": {
29
+ "import": "./lib/esm/exec-process.mjs",
30
+ "require": "./lib/cjs/exec-process.cjs"
31
+ },
32
+ "./exec": {
33
+ "import": "./lib/esm/exec.mjs",
34
+ "require": "./lib/cjs/exec.cjs"
23
35
  }
24
36
  },
25
37
  "main": "lib/cjs/index.cjs",
@@ -28,6 +40,15 @@
28
40
  "*": {
29
41
  ".": [
30
42
  "./lib/cjs/index.d.cts"
43
+ ],
44
+ "command-line": [
45
+ "./lib/cjs/command-line.d.cts"
46
+ ],
47
+ "exec-process": [
48
+ "./lib/cjs/exec-process.d.cts"
49
+ ],
50
+ "exec": [
51
+ "./lib/cjs/exec.d.cts"
31
52
  ]
32
53
  }
33
54
  },
@@ -47,6 +68,7 @@
47
68
  "lint:package": "fixpack || true",
48
69
  "prepack": "npm run build",
49
70
  "prepare": "husky; npm run lint:package",
71
+ "release": "bump-n-ship release --all --pause",
50
72
  "test": "vitest run --reporter tree",
51
73
  "test:coverage": "vitest run --reporter tree --coverage --coverage.reporter text",
52
74
  "test:ui": "vitest --ui --coverage",
@@ -67,6 +89,7 @@
67
89
  "@types/node": "20.19.43",
68
90
  "@vitest/coverage-v8": "4.1.12",
69
91
  "@vitest/ui": "4.1.12",
92
+ "@zokugun/bump-n-ship": "0.1.1",
70
93
  "@zokugun/eslint-config": "0.2.1",
71
94
  "@zokugun/eslint-config-data": "0.2.1",
72
95
  "@zokugun/eslint-config-glossary": "0.2.0",
@@ -77,7 +100,7 @@
77
100
  "@zokugun/eslint-config-test": "0.2.0",
78
101
  "@zokugun/eslint-config-ts": "0.3.2",
79
102
  "@zokugun/fs-extra-plus": "0.7.0",
80
- "@zokugun/tsc-leda": "0.9.4",
103
+ "@zokugun/tsc-leda": "0.9.5",
81
104
  "eslint": "10.13.0",
82
105
  "fixpack": "4.0.0",
83
106
  "get-node": "15.0.4",