procband 0.3.5 → 0.3.6
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 +79 -15
- package/dist/index.d.mts +43 -5
- package/dist/index.mjs +59 -6
- package/docs/context.md +262 -178
- package/docs/guides/foreground-commands.md +80 -0
- package/docs/guides/readiness.md +104 -0
- package/docs/guides/restarts.md +115 -0
- package/docs/index.md +61 -0
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -1,38 +1,102 @@
|
|
|
1
1
|
# procband
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Supervise subprocesses from TypeScript when a script needs prefixed output,
|
|
4
|
+
> readiness matching, restart policy, and process-tree shutdown without becoming
|
|
5
|
+
> a standalone process manager.
|
|
4
6
|
|
|
5
|
-
`procband`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
`procband` wraps one child process per `supervise()` call. It starts the child
|
|
8
|
+
immediately, prefixes `stdout` and `stderr`, lets your script wait for future
|
|
9
|
+
log lines, and resolves to a final `ProcessResult` when no more restart attempt
|
|
10
|
+
will run.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Install
|
|
11
13
|
|
|
12
14
|
```sh
|
|
13
15
|
pnpm add procband
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
##
|
|
18
|
+
## First Process
|
|
19
|
+
|
|
20
|
+
Use `waitFor()` when the next step depends on output that the child will print
|
|
21
|
+
after supervision starts:
|
|
17
22
|
|
|
18
23
|
```ts
|
|
19
24
|
import process from 'node:process'
|
|
20
25
|
import { supervise } from 'procband'
|
|
21
26
|
|
|
22
27
|
const proc = supervise({
|
|
28
|
+
name: 'worker',
|
|
23
29
|
command: process.execPath,
|
|
24
30
|
args: ['-e', 'console.log("ready")'],
|
|
25
31
|
})
|
|
26
32
|
|
|
27
|
-
await proc.waitFor('ready')
|
|
33
|
+
const ready = await proc.waitFor('ready')
|
|
34
|
+
console.log(`matched ${ready.line}`)
|
|
35
|
+
|
|
28
36
|
const result = await proc
|
|
29
|
-
console.log(result)
|
|
37
|
+
console.log(result.exitCode)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The child output is written to the parent process with a `worker` prefix, the
|
|
41
|
+
readiness wait resolves when a future line contains `ready`, and `await proc`
|
|
42
|
+
returns the terminal result.
|
|
43
|
+
|
|
44
|
+
## Choose the Failure Behavior
|
|
45
|
+
|
|
46
|
+
By default, a failed terminal exit resolves to `ProcessResult`. Use this when a
|
|
47
|
+
supervisor script needs to inspect the outcome:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import process from 'node:process'
|
|
51
|
+
import { supervise } from 'procband'
|
|
52
|
+
|
|
53
|
+
const result = await supervise({
|
|
54
|
+
name: 'job',
|
|
55
|
+
command: process.execPath,
|
|
56
|
+
args: ['-e', 'process.exit(1)'],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
console.error(`job failed with ${result.exitCode}`)
|
|
61
|
+
}
|
|
30
62
|
```
|
|
31
63
|
|
|
32
|
-
|
|
64
|
+
Use `expectSuccess()` for foreground command-runner steps where a failed exit
|
|
65
|
+
should reject:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { ProcessExitError, supervise } from 'procband'
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await supervise({
|
|
72
|
+
name: 'db',
|
|
73
|
+
command: 'pnpm',
|
|
74
|
+
args: ['db', 'ensure'],
|
|
75
|
+
}).expectSuccess()
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof ProcessExitError) {
|
|
78
|
+
console.error(error.command, error.args, error.exitCode)
|
|
79
|
+
}
|
|
80
|
+
throw error
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If your script never awaits the process or calls `wait()`, an unobserved failed
|
|
85
|
+
terminal exit sets the parent `process.exitCode` and starts stopping other live
|
|
86
|
+
`procband` processes in the same parent script.
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
Start with the page that matches the decision in front of you:
|
|
91
|
+
|
|
92
|
+
| Page | Use it when |
|
|
93
|
+
| --------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
94
|
+
| [Docs overview](docs/index.md) | You need the moving parts, boundaries, and next page to read. |
|
|
95
|
+
| [Readiness and matching](docs/guides/readiness.md) | A script must wait for output or react to repeated log lines. |
|
|
96
|
+
| [Restart failed processes](docs/guides/restarts.md) | A child can fail transiently and should be retried with limits. |
|
|
97
|
+
| [Foreground commands](docs/guides/foreground-commands.md) | A script step should reject when a command exits unsuccessfully. |
|
|
98
|
+
| [Concepts and lifecycle](docs/context.md) | You need lifecycle, shutdown, error, and invariant details. |
|
|
33
99
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Sequencing example: [examples/sequenced-processes.ts](examples/sequenced-processes.ts)
|
|
38
|
-
- Exact exported signatures: [dist/index.d.mts](dist/index.d.mts)
|
|
100
|
+
Runnable examples live in [`examples/`](examples/). Exact public signatures are
|
|
101
|
+
generated from the TypeScript source and published through
|
|
102
|
+
[`dist/index.d.mts`](dist/index.d.mts).
|
package/dist/index.d.mts
CHANGED
|
@@ -62,6 +62,12 @@ interface ProcessConfig {
|
|
|
62
62
|
* Defaults to `name`.
|
|
63
63
|
*/
|
|
64
64
|
label?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Whether child output should be prefixed with the process label.
|
|
67
|
+
*
|
|
68
|
+
* Defaults to `true`. Set to `false` for raw child output.
|
|
69
|
+
*/
|
|
70
|
+
prefix?: boolean;
|
|
65
71
|
/**
|
|
66
72
|
* RGB color for this process's `stdout` prefix.
|
|
67
73
|
*
|
|
@@ -221,6 +227,18 @@ interface ProcessResult {
|
|
|
221
227
|
*/
|
|
222
228
|
restartSuppressed: boolean;
|
|
223
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Options for `wait()`.
|
|
232
|
+
*/
|
|
233
|
+
interface WaitOptions {
|
|
234
|
+
/**
|
|
235
|
+
* Reject with `ProcessExitError` when the process exits unsuccessfully.
|
|
236
|
+
*
|
|
237
|
+
* Defaults to `false`, preserving procband's supervision-oriented behavior
|
|
238
|
+
* where terminal failures resolve to `ProcessResult`.
|
|
239
|
+
*/
|
|
240
|
+
rejectOnFailure?: boolean;
|
|
241
|
+
}
|
|
224
242
|
/**
|
|
225
243
|
* A supervised child-process handle.
|
|
226
244
|
*
|
|
@@ -257,11 +275,16 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
|
|
|
257
275
|
/**
|
|
258
276
|
* Wait for the process to become terminal with no further restart pending.
|
|
259
277
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
|
|
278
|
+
* By default this resolves for both success and failure. Pass
|
|
279
|
+
* `rejectOnFailure: true` to reject failed exits with `ProcessExitError`.
|
|
280
|
+
*/
|
|
281
|
+
wait(options?: WaitOptions): Promise<ProcessResult>;
|
|
282
|
+
/**
|
|
283
|
+
* Wait for the process to become terminal and reject if it fails.
|
|
284
|
+
*
|
|
285
|
+
* This is equivalent to `wait({ rejectOnFailure: true })`.
|
|
263
286
|
*/
|
|
264
|
-
|
|
287
|
+
expectSuccess(): Promise<ProcessResult>;
|
|
265
288
|
/**
|
|
266
289
|
* Disable future restarts and terminate the active process tree.
|
|
267
290
|
*
|
|
@@ -273,6 +296,21 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
|
|
|
273
296
|
kill(signal?: KillSignal): boolean;
|
|
274
297
|
}
|
|
275
298
|
//#endregion
|
|
299
|
+
//#region src/errors.d.ts
|
|
300
|
+
/**
|
|
301
|
+
* Error thrown when a process was expected to exit successfully but did not.
|
|
302
|
+
*/
|
|
303
|
+
declare class ProcessExitError extends Error {
|
|
304
|
+
readonly config: ProcessConfig;
|
|
305
|
+
readonly result: ProcessResult;
|
|
306
|
+
readonly command: string;
|
|
307
|
+
readonly args: readonly string[];
|
|
308
|
+
readonly code: number | null;
|
|
309
|
+
readonly exitCode: number;
|
|
310
|
+
readonly signal: NodeJS.Signals | null;
|
|
311
|
+
constructor(config: ProcessConfig, result: ProcessResult);
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
276
314
|
//#region src/process.d.ts
|
|
277
315
|
/**
|
|
278
316
|
* Start supervising a single subprocess.
|
|
@@ -303,4 +341,4 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
|
|
|
303
341
|
*/
|
|
304
342
|
declare function supervise(config: ProcessConfig): ProcbandProcess;
|
|
305
343
|
//#endregion
|
|
306
|
-
export { type MatchCallback, type MatchEvent, type MatchOptions, type ProcbandProcess, type ProcessConfig, type ProcessResult, type RestartPolicy, type RgbColor, type Unsubscribe, type WaitForOptions, supervise };
|
|
344
|
+
export { type MatchCallback, type MatchEvent, type MatchOptions, type ProcbandProcess, type ProcessConfig, ProcessExitError, type ProcessResult, type RestartPolicy, type RgbColor, type Unsubscribe, type WaitForOptions, type WaitOptions, supervise };
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,45 @@ import process from "node:process";
|
|
|
5
5
|
import ansiStyles from "ansi-styles";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import treeKill, { treeKillSync } from "@alloc/tree-kill";
|
|
8
|
+
//#region src/errors.ts
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when a process was expected to exit successfully but did not.
|
|
11
|
+
*/
|
|
12
|
+
var ProcessExitError = class extends Error {
|
|
13
|
+
config;
|
|
14
|
+
result;
|
|
15
|
+
command;
|
|
16
|
+
args;
|
|
17
|
+
code;
|
|
18
|
+
exitCode;
|
|
19
|
+
signal;
|
|
20
|
+
constructor(config, result) {
|
|
21
|
+
super(formatProcessExitMessage(config, result));
|
|
22
|
+
this.name = "ProcessExitError";
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.result = result;
|
|
25
|
+
this.command = config.command;
|
|
26
|
+
this.args = [...config.args ?? []];
|
|
27
|
+
this.code = result.code;
|
|
28
|
+
this.exitCode = result.exitCode;
|
|
29
|
+
this.signal = result.signal;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function formatProcessExitMessage(config, result) {
|
|
33
|
+
const command = formatCommand(config);
|
|
34
|
+
const prefix = `Process "${result.name}" failed: ${command}`;
|
|
35
|
+
if (result.signal) return `${prefix} exited by signal ${result.signal} (exit code ${result.exitCode})`;
|
|
36
|
+
if (result.code != null) return `${prefix} exited with code ${result.code}`;
|
|
37
|
+
return `${prefix} exited unsuccessfully (exit code ${result.exitCode})`;
|
|
38
|
+
}
|
|
39
|
+
function formatCommand(config) {
|
|
40
|
+
return [config.command, ...config.args ?? []].map(formatCommandPart).join(" ");
|
|
41
|
+
}
|
|
42
|
+
function formatCommandPart(value) {
|
|
43
|
+
if (/^[\w./:=@%+,-]+$/.test(value)) return value;
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
8
47
|
//#region src/colors.ts
|
|
9
48
|
const prefixPalette = [
|
|
10
49
|
[
|
|
@@ -81,7 +120,8 @@ function validateProcessColor(color) {
|
|
|
81
120
|
if (red === stderrColor[0] && green === stderrColor[1] && blue === stderrColor[2]) throw new Error("ProcessConfig.color cannot use the reserved stderr color");
|
|
82
121
|
}
|
|
83
122
|
function writePrefixedLine(target, label, color, line, appendNewline) {
|
|
84
|
-
|
|
123
|
+
const prefix = label == null ? "" : formatPrefix(label, color);
|
|
124
|
+
target.write(prefix + line + (appendNewline ? "\n" : ""));
|
|
85
125
|
}
|
|
86
126
|
function formatPrefix(label, color) {
|
|
87
127
|
return `${ansiStyles.color.ansi16m(...color)}[${label}]${ansiStyles.color.close} `;
|
|
@@ -400,6 +440,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
400
440
|
config;
|
|
401
441
|
name;
|
|
402
442
|
label;
|
|
443
|
+
prefix;
|
|
403
444
|
color;
|
|
404
445
|
matches;
|
|
405
446
|
restart;
|
|
@@ -422,6 +463,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
422
463
|
this.config = config;
|
|
423
464
|
this.name = name;
|
|
424
465
|
this.label = config.label ?? name;
|
|
466
|
+
this.prefix = config.name === "" ? false : config.prefix ?? true;
|
|
425
467
|
this.color = resolveProcessColor(config.color);
|
|
426
468
|
this.matches = new MatchRegistry(this.name);
|
|
427
469
|
this.restart = new RestartController(config.restart);
|
|
@@ -480,10 +522,14 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
480
522
|
waitFor(pattern, options) {
|
|
481
523
|
return this.matches.waitFor(pattern, options);
|
|
482
524
|
}
|
|
483
|
-
wait() {
|
|
525
|
+
wait(options) {
|
|
484
526
|
this.markTerminalResultObserved();
|
|
527
|
+
if (options?.rejectOnFailure) return this.finalPromise.then((result) => this.expectSuccessfulResult(result));
|
|
485
528
|
return this.finalPromise;
|
|
486
529
|
}
|
|
530
|
+
expectSuccess() {
|
|
531
|
+
return this.wait({ rejectOnFailure: true });
|
|
532
|
+
}
|
|
487
533
|
then(onfulfilled, onrejected) {
|
|
488
534
|
this.markTerminalResultObserved();
|
|
489
535
|
return this.finalPromise.then(onfulfilled, onrejected);
|
|
@@ -626,7 +672,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
626
672
|
buffer.text = text;
|
|
627
673
|
}
|
|
628
674
|
handleLine(stream, line, appendNewline) {
|
|
629
|
-
writePrefixedLine(stream === "stdout" ? process.stdout : process.stderr, this.label, stream === "stdout" ? this.color : stderrColor, line, appendNewline);
|
|
675
|
+
writePrefixedLine(stream === "stdout" ? process.stdout : process.stderr, this.prefix ? this.label : null, stream === "stdout" ? this.color : stderrColor, line, appendNewline);
|
|
630
676
|
this.matches.emit(stream, line);
|
|
631
677
|
}
|
|
632
678
|
async handleAttemptClose(code, signal) {
|
|
@@ -669,12 +715,16 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
669
715
|
}
|
|
670
716
|
closeMatches() {
|
|
671
717
|
const error = /* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`);
|
|
672
|
-
if (this.matches.hasPendingWait()) writePrefixedLine(process.stderr, this.label, stderrColor, error.message, true);
|
|
718
|
+
if (this.matches.hasPendingWait()) writePrefixedLine(process.stderr, this.prefix ? this.label : null, stderrColor, error.message, true);
|
|
673
719
|
this.matches.close(error);
|
|
674
720
|
}
|
|
675
721
|
markTerminalResultObserved() {
|
|
676
722
|
this.terminalResultObserved = true;
|
|
677
723
|
}
|
|
724
|
+
expectSuccessfulResult(result) {
|
|
725
|
+
if (isFailedResult(result)) throw new ProcessExitError(this.config, result);
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
678
728
|
shouldPropagateFailure(result) {
|
|
679
729
|
return result.exitCode !== 0 && !this.shutdownRequested && !this.terminalResultObserved;
|
|
680
730
|
}
|
|
@@ -734,7 +784,7 @@ let propagatedFailure = false;
|
|
|
734
784
|
function validateProcessConfig(config) {
|
|
735
785
|
if (!config.command) throw new Error("ProcessConfig.command is required");
|
|
736
786
|
validateProcessColor(config.color);
|
|
737
|
-
const name = config.name || inferProcessName(config.command);
|
|
787
|
+
const name = config.name === void 0 || config.name === "" ? inferProcessName(config.command) : config.name;
|
|
738
788
|
if (!name) throw new Error("ProcessConfig.name is required when command does not match /[-\\w]+$/");
|
|
739
789
|
return name;
|
|
740
790
|
}
|
|
@@ -746,6 +796,9 @@ function getResultExitCode(code, signal) {
|
|
|
746
796
|
if (signal) return 128 + (constants.signals[signal] ?? 0);
|
|
747
797
|
return 1;
|
|
748
798
|
}
|
|
799
|
+
function isFailedResult(result) {
|
|
800
|
+
return result.exitCode !== 0 || result.signal != null;
|
|
801
|
+
}
|
|
749
802
|
function propagateFailure(exitCode) {
|
|
750
803
|
if (propagatedFailure) return;
|
|
751
804
|
propagatedFailure = true;
|
|
@@ -756,4 +809,4 @@ function propagateFailure(exitCode) {
|
|
|
756
809
|
});
|
|
757
810
|
}
|
|
758
811
|
//#endregion
|
|
759
|
-
export { supervise };
|
|
812
|
+
export { ProcessExitError, supervise };
|
package/docs/context.md
CHANGED
|
@@ -1,184 +1,268 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
`procband` supervises one
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
# Concepts and Lifecycle
|
|
2
|
+
|
|
3
|
+
> `procband` supervises one child process at a time; this page defines the
|
|
4
|
+
> wrapper lifecycle, restart boundary, shutdown behavior, and failure model that
|
|
5
|
+
> guide pages rely on.
|
|
6
|
+
|
|
7
|
+
## Core Model
|
|
8
|
+
|
|
9
|
+
Each `supervise(config)` call creates one supervised process. The returned
|
|
10
|
+
`ProcbandProcess` is both:
|
|
11
|
+
|
|
12
|
+
| Surface | What it represents |
|
|
13
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| Child-process handle | The current active child attempt. Inherited fields such as `pid`, `stdin`, `stdout`, and `stderr` update across restarts. |
|
|
15
|
+
| Matching surface | Future output lines observed by `match()` and `waitFor()`. |
|
|
16
|
+
| Shutdown surface | `kill()` disables future restarts and stops the current child process tree. |
|
|
17
|
+
| Thenable result | `await proc` is equivalent to `await proc.wait()` and resolves to the terminal `ProcessResult`. |
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import process from 'node:process'
|
|
21
|
+
import { supervise } from 'procband'
|
|
22
|
+
|
|
23
|
+
const proc = supervise({
|
|
24
|
+
name: 'api',
|
|
25
|
+
command: process.execPath,
|
|
26
|
+
args: ['-e', 'console.log("ready")'],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
await proc.waitFor('ready')
|
|
30
|
+
const result = await proc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
After the readiness line is observed, `result` is available only when the
|
|
34
|
+
process is terminal and no restart delay or attempt remains.
|
|
35
|
+
|
|
36
|
+
## Lifecycle
|
|
37
|
+
|
|
38
|
+
1. `supervise(config)` validates the config and spawns the first child
|
|
39
|
+
immediately.
|
|
40
|
+
2. Child `stdout` and `stderr` are decoded as UTF-8 text and split into lines.
|
|
41
|
+
3. Each line is written to the parent `process.stdout` or `process.stderr` with
|
|
42
|
+
a process prefix unless `ProcessConfig.prefix` is `false`. `stderr` prefixes
|
|
43
|
+
always use the reserved red color.
|
|
44
|
+
4. If `ProcessConfig.stderr` is provided, raw child `stderr` bytes are also
|
|
45
|
+
written to that sink.
|
|
46
|
+
5. Matching subscribers receive future lines through `match()` callbacks or
|
|
47
|
+
`waitFor()` promises.
|
|
48
|
+
6. When the child exits, `procband` either finalizes or schedules another child
|
|
49
|
+
attempt according to the restart policy.
|
|
50
|
+
7. `await proc`, `await proc.wait()`, and `await proc.expectSuccess()` settle
|
|
51
|
+
only after the supervised process is terminal.
|
|
52
|
+
|
|
53
|
+
Use this shape when one process must become ready before another starts:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import process from 'node:process'
|
|
57
|
+
import { supervise } from 'procband'
|
|
58
|
+
|
|
59
|
+
const api = supervise({
|
|
60
|
+
name: 'api',
|
|
61
|
+
command: process.execPath,
|
|
62
|
+
args: ['-e', 'setTimeout(() => console.log("ready"), 20)'],
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await api.waitFor('ready')
|
|
66
|
+
|
|
67
|
+
supervise({
|
|
68
|
+
name: 'worker',
|
|
69
|
+
command: process.execPath,
|
|
70
|
+
args: ['-e', 'console.log("watching")'],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The worker starts after the API prints a future line containing `ready`.
|
|
75
|
+
|
|
76
|
+
## Output Prefixes
|
|
77
|
+
|
|
78
|
+
By default, each child output line is written to the parent terminal with the
|
|
79
|
+
process label:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
supervise({
|
|
83
|
+
name: 'server',
|
|
84
|
+
command: process.execPath,
|
|
85
|
+
args: ['-e', 'console.log("ready")'],
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Set `prefix: false` when a child should keep raw `stdout` and `stderr` output:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
supervise({
|
|
93
|
+
name: 'server',
|
|
94
|
+
prefix: false,
|
|
95
|
+
command: process.execPath,
|
|
96
|
+
args: ['-e', 'console.log("ready")'],
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Disabling the prefix changes only parent-visible output and procband
|
|
101
|
+
diagnostics. Matching events and final results still use the process `name`.
|
|
102
|
+
|
|
103
|
+
## Matching
|
|
104
|
+
|
|
105
|
+
Matching is line-based and future-only. A subscription sees lines emitted after
|
|
106
|
+
the subscription is registered; it does not replay earlier output.
|
|
107
|
+
|
|
108
|
+
| Pattern | Behavior |
|
|
109
|
+
| -------- | ---------------------------------------------------------------------------- |
|
|
110
|
+
| String | Matches when the observed line includes the string. |
|
|
111
|
+
| `RegExp` | Runs against the full observed line. `lastIndex` is reset before each match. |
|
|
112
|
+
|
|
113
|
+
Use `waitFor()` for one required line:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const event = await proc.waitFor(/^ready$/, {
|
|
117
|
+
stream: 'stdout',
|
|
118
|
+
timeoutMs: 5000,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
console.log(event.process, event.line)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `match()` for repeated lines:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const unsubscribe = proc.match(
|
|
128
|
+
/^attempt \d+$/,
|
|
129
|
+
(event) => {
|
|
130
|
+
console.log(`observed ${event.line}`)
|
|
131
|
+
},
|
|
132
|
+
{ stream: 'stdout' },
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
unsubscribe()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`waitFor()` rejects when its timeout elapses or the process becomes terminal
|
|
139
|
+
before a matching future line appears. If a `match()` callback throws, only that
|
|
140
|
+
subscription is removed.
|
|
141
|
+
|
|
142
|
+
## Restarts
|
|
143
|
+
|
|
144
|
+
`restart: true` uses the built-in policy:
|
|
145
|
+
|
|
146
|
+
| Field | Default | Meaning |
|
|
147
|
+
| ------------- | -------------- | -------------------------------------------------------- |
|
|
148
|
+
| `when` | `'on-failure'` | Restart only non-zero exits or signal exits. |
|
|
149
|
+
| `delayMs` | `1000` | Wait this long before the next attempt. |
|
|
150
|
+
| `maxFailures` | `3` | Suppress restart after more than this many failed exits. |
|
|
151
|
+
| `windowMs` | `30000` | Count failed exits inside this rolling window. |
|
|
152
|
+
|
|
153
|
+
Pass an explicit policy when the defaults are too slow or too permissive:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const proc = supervise({
|
|
157
|
+
name: 'job',
|
|
158
|
+
command: process.execPath,
|
|
159
|
+
args: ['-e', 'process.exit(1)'],
|
|
160
|
+
restart: {
|
|
161
|
+
delayMs: 25,
|
|
162
|
+
maxFailures: 5,
|
|
163
|
+
windowMs: 1000,
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const result = await proc
|
|
168
|
+
console.log(result.restarts, result.restartSuppressed)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If the child keeps failing inside the configured window, `restartSuppressed`
|
|
172
|
+
becomes `true` and the final failed result is returned.
|
|
173
|
+
|
|
174
|
+
## Shutdown
|
|
175
|
+
|
|
176
|
+
Use `proc.kill()` for deliberate shutdown from your own script:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const stopped = proc.kill('SIGTERM')
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`kill()` disables future restarts and stops the active child process tree. For
|
|
183
|
+
`detached: true` children on Unix-like platforms, shutdown also signals the
|
|
184
|
+
detached child process group so same-group descendants are cleaned up even when
|
|
185
|
+
they are no longer reachable by parent PID.
|
|
186
|
+
|
|
187
|
+
> [!NOTE]
|
|
188
|
+
> `kill(0)` keeps the normal Node.js existence-check behavior. It does not stop
|
|
189
|
+
> supervision or disable restarts.
|
|
190
|
+
|
|
191
|
+
Parent cleanup installs `SIGINT` and `SIGTERM` handlers while live supervised
|
|
192
|
+
processes exist. Set `parentExitSignal` only when children require a specific
|
|
193
|
+
signal during parent-driven cleanup:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
supervise({
|
|
197
|
+
name: 'server',
|
|
198
|
+
command: process.execPath,
|
|
199
|
+
args: ['server.mjs'],
|
|
200
|
+
parentExitSignal: 'SIGHUP',
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`parentExitSignal` does not change the signal used by explicit `proc.kill()`
|
|
205
|
+
calls.
|
|
206
|
+
|
|
207
|
+
## Failure Model
|
|
208
|
+
|
|
209
|
+
`procband` separates terminal process failure from promise rejection:
|
|
210
|
+
|
|
211
|
+
| API | Failed terminal exit |
|
|
212
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
213
|
+
| `await proc` | Resolves to `ProcessResult`. |
|
|
214
|
+
| `proc.wait()` | Resolves to `ProcessResult`. |
|
|
215
|
+
| `proc.wait({ rejectOnFailure: true })` | Rejects with `ProcessExitError`. |
|
|
216
|
+
| `proc.expectSuccess()` | Rejects with `ProcessExitError`. |
|
|
217
|
+
| Unobserved process | Sets parent `process.exitCode` and starts stopping other live supervised processes. |
|
|
218
|
+
|
|
219
|
+
`ProcessExitError` includes the original config, final result, command, args,
|
|
220
|
+
exit code, and signal:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { ProcessExitError } from 'procband'
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await proc.expectSuccess()
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof ProcessExitError) {
|
|
229
|
+
console.error(error.command, error.exitCode, error.signal)
|
|
230
|
+
}
|
|
231
|
+
throw error
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Configuration Boundaries
|
|
236
|
+
|
|
237
|
+
| Field | Boundary |
|
|
238
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `command` | Required shell-free executable or command name passed to `spawn()`. |
|
|
240
|
+
| `name` | Optional stable process identifier. Defaults to the trailing `/[-\w]+$/` match from `command`. |
|
|
241
|
+
| `label` | Optional human-facing output prefix. Defaults to `name`. |
|
|
242
|
+
| `prefix` | Defaults to `true`. Set to `false` to write output and diagnostics without the process label prefix. |
|
|
243
|
+
| `stdin` | Defaults to disconnected. Use `true` for writable `proc.stdin`, or pass a readable stream to pipe automatically. |
|
|
244
|
+
| `stderr` | Optional extra sink for raw child `stderr`; prefixed parent `stderr` output still happens. |
|
|
245
|
+
| `detached` | Passed through to `spawn()` and used during shutdown on Unix-like platforms. |
|
|
246
|
+
| `color` | Optional RGB prefix color for `stdout`; reserved `stderr` red cannot be used. |
|
|
247
|
+
|
|
248
|
+
Invalid config, such as a missing `command`, a command without an inferable
|
|
249
|
+
fallback `name`, or a reserved color, throws synchronously from `supervise()`.
|
|
250
|
+
|
|
251
|
+
## Terminology
|
|
252
|
+
|
|
253
|
+
| Term | Meaning |
|
|
254
|
+
| ------------------- | ------------------------------------------------------------------------------------------------- |
|
|
255
|
+
| Supervised process | A `ProcbandProcess` wrapper plus its current child attempt. |
|
|
256
|
+
| Child attempt | One concrete spawned process instance inside a supervision run. |
|
|
257
|
+
| Terminal | No child is running and no restart will be started. |
|
|
258
|
+
| Restart suppression | Automatic disabling of further restarts after too many failed exits inside the configured window. |
|
|
259
|
+
| Match | A future observed output line that satisfies a string or regex pattern. |
|
|
260
|
+
|
|
261
|
+
## Non-Goals
|
|
178
262
|
|
|
179
263
|
- A standalone CLI
|
|
180
264
|
- Historical log replay
|
|
181
265
|
- Multi-process orchestration in one top-level API
|
|
182
266
|
- Shell command parsing
|
|
183
|
-
-
|
|
267
|
+
- Service-management features such as persistence, cron scheduling, or host
|
|
184
268
|
restarts
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Foreground Commands
|
|
2
|
+
|
|
3
|
+
> Use `expectSuccess()` when a supervised child process represents a script step
|
|
4
|
+
> that must succeed before the parent script can continue.
|
|
5
|
+
|
|
6
|
+
`procband` normally resolves failed exits to `ProcessResult` because background
|
|
7
|
+
supervision often needs to inspect failures instead of throwing immediately.
|
|
8
|
+
Foreground command-runner steps usually want the opposite behavior.
|
|
9
|
+
|
|
10
|
+
## Reject Failed Exits
|
|
11
|
+
|
|
12
|
+
Use `expectSuccess()` when a non-zero exit or signal exit should reject:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import process from 'node:process'
|
|
16
|
+
import { ProcessExitError, supervise } from 'procband'
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await supervise({
|
|
20
|
+
name: 'db',
|
|
21
|
+
command: 'pnpm',
|
|
22
|
+
args: ['db', 'ensure'],
|
|
23
|
+
}).expectSuccess()
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof ProcessExitError) {
|
|
26
|
+
console.error(error.command, error.args, error.exitCode)
|
|
27
|
+
}
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When the command exits successfully, the promise resolves to `ProcessResult`.
|
|
33
|
+
When it fails, `ProcessExitError` exposes the command, args, exit code, signal,
|
|
34
|
+
original config, and terminal result.
|
|
35
|
+
|
|
36
|
+
## Use `wait()` When the Choice Is Dynamic
|
|
37
|
+
|
|
38
|
+
`expectSuccess()` is equivalent to `wait({ rejectOnFailure: true })`. Use
|
|
39
|
+
`wait()` when a caller decides the behavior:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import process from 'node:process'
|
|
43
|
+
import { supervise } from 'procband'
|
|
44
|
+
|
|
45
|
+
const proc = supervise({
|
|
46
|
+
name: 'check',
|
|
47
|
+
command: process.execPath,
|
|
48
|
+
args: ['-e', 'process.exit(1)'],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const result = await proc.wait({ rejectOnFailure: shouldThrow })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If `shouldThrow` is `false`, the failed exit resolves to `ProcessResult`. If it
|
|
55
|
+
is `true`, the same exit rejects with `ProcessExitError`.
|
|
56
|
+
|
|
57
|
+
## Let Background Failures Propagate
|
|
58
|
+
|
|
59
|
+
For background processes that should fail the parent script when nobody handles
|
|
60
|
+
their result, leave the process unobserved:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import process from 'node:process'
|
|
64
|
+
import { supervise } from 'procband'
|
|
65
|
+
|
|
66
|
+
supervise({
|
|
67
|
+
name: 'worker',
|
|
68
|
+
command: process.execPath,
|
|
69
|
+
args: ['-e', 'setTimeout(() => process.exit(1), 60)'],
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If this process reaches a failed terminal state and no code has awaited it or
|
|
74
|
+
called `wait()`, `procband` sets the parent `process.exitCode` and starts
|
|
75
|
+
stopping other live `procband` processes in the same parent script.
|
|
76
|
+
|
|
77
|
+
> [!IMPORTANT]
|
|
78
|
+
> Awaiting `proc` or calling `proc.wait()` marks the terminal result as
|
|
79
|
+
> observed. Once observed, default parent-exit propagation is suppressed for
|
|
80
|
+
> that process.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Readiness and Matching
|
|
2
|
+
|
|
3
|
+
> Use future line matching when a parent script must wait for a child process to
|
|
4
|
+
> announce readiness or react to repeated output without buffering log history.
|
|
5
|
+
|
|
6
|
+
Matching starts when you call `waitFor()` or `match()`. Earlier output is not
|
|
7
|
+
replayed, so create the supervised process and register the wait before the line
|
|
8
|
+
you need can be missed.
|
|
9
|
+
|
|
10
|
+
## Wait for One Line
|
|
11
|
+
|
|
12
|
+
Use `waitFor()` when the next step cannot start until the child prints a known
|
|
13
|
+
line:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import process from 'node:process'
|
|
17
|
+
import { supervise } from 'procband'
|
|
18
|
+
|
|
19
|
+
const api = supervise({
|
|
20
|
+
name: 'api',
|
|
21
|
+
command: process.execPath,
|
|
22
|
+
args: [
|
|
23
|
+
'-e',
|
|
24
|
+
[
|
|
25
|
+
'console.log("booting")',
|
|
26
|
+
'setTimeout(() => console.log("ready"), 20)',
|
|
27
|
+
'setInterval(() => {}, 1000)',
|
|
28
|
+
].join(';'),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const event = await api.waitFor('ready', {
|
|
33
|
+
stream: 'stdout',
|
|
34
|
+
timeoutMs: 5000,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
console.log(`api is ${event.line}`)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The wait resolves with the first future `stdout` line that contains `ready`.
|
|
41
|
+
When `timeoutMs` elapses first, or the process exits before a matching line is
|
|
42
|
+
observed, the wait rejects.
|
|
43
|
+
|
|
44
|
+
## Match Repeated Lines
|
|
45
|
+
|
|
46
|
+
Use `match()` when the child may print the same kind of line many times:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const unsubscribe = api.match(
|
|
50
|
+
/^warn:/,
|
|
51
|
+
(event) => {
|
|
52
|
+
console.log(`observed ${event.stream}: ${event.line}`)
|
|
53
|
+
},
|
|
54
|
+
{ stream: 'stderr' },
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Later, when this script no longer needs warning callbacks:
|
|
58
|
+
unsubscribe()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each subscription is independent. If one callback throws, that subscription is
|
|
62
|
+
removed and other subscriptions keep running.
|
|
63
|
+
|
|
64
|
+
## Choose a Pattern
|
|
65
|
+
|
|
66
|
+
| Pattern | Best for | Result detail |
|
|
67
|
+
| ----------- | ------------------------------------ | -------------------------------------------------- |
|
|
68
|
+
| `'ready'` | Simple substring checks. | `event.match` is `null`. |
|
|
69
|
+
| `/^ready$/` | Exact line shape or captured values. | `event.match` contains the `RegExp.exec()` result. |
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const event = await api.waitFor(/^listening on (\d+)$/)
|
|
73
|
+
const port = Number(event.match?.[1])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The regular expression runs against the full observed line without the trailing
|
|
77
|
+
newline.
|
|
78
|
+
|
|
79
|
+
## Start Another Process After Readiness
|
|
80
|
+
|
|
81
|
+
This pattern keeps sequencing explicit: wait for the first process, then start
|
|
82
|
+
the dependent process.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import process from 'node:process'
|
|
86
|
+
import { supervise } from 'procband'
|
|
87
|
+
|
|
88
|
+
const api = supervise({
|
|
89
|
+
name: 'api',
|
|
90
|
+
command: process.execPath,
|
|
91
|
+
args: ['-e', 'setTimeout(() => console.log("ready"), 20)'],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await api.waitFor('ready')
|
|
95
|
+
|
|
96
|
+
supervise({
|
|
97
|
+
name: 'worker',
|
|
98
|
+
command: process.execPath,
|
|
99
|
+
args: ['-e', 'console.log("watching")'],
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
After the API prints `ready`, the worker starts and its output is prefixed
|
|
104
|
+
separately.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Restart Failed Processes
|
|
2
|
+
|
|
3
|
+
> Add a restart policy when a child process may fail transiently and the parent
|
|
4
|
+
> script should retry before treating the supervision run as terminal.
|
|
5
|
+
|
|
6
|
+
Restarts are disabled by default. Enable them per process with `restart: true`
|
|
7
|
+
or an explicit `RestartPolicy`.
|
|
8
|
+
|
|
9
|
+
## Use the Built-In Policy
|
|
10
|
+
|
|
11
|
+
Use `restart: true` when the defaults are acceptable:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import process from 'node:process'
|
|
15
|
+
import { supervise } from 'procband'
|
|
16
|
+
|
|
17
|
+
const proc = supervise({
|
|
18
|
+
name: 'worker',
|
|
19
|
+
command: process.execPath,
|
|
20
|
+
args: ['-e', 'process.exit(1)'],
|
|
21
|
+
restart: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const result = await proc
|
|
25
|
+
console.log(result.restarts, result.restartSuppressed)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The default policy restarts failed exits after `1000` milliseconds and suppresses
|
|
29
|
+
restart after more than `3` failed exits inside `30000` milliseconds.
|
|
30
|
+
|
|
31
|
+
## Set an Explicit Policy
|
|
32
|
+
|
|
33
|
+
Use an explicit policy when tests, examples, or short-lived scripts need faster
|
|
34
|
+
feedback:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import process from 'node:process'
|
|
38
|
+
import { supervise } from 'procband'
|
|
39
|
+
|
|
40
|
+
const proc = supervise({
|
|
41
|
+
name: 'job',
|
|
42
|
+
command: process.execPath,
|
|
43
|
+
args: ['-e', 'process.exit(1)'],
|
|
44
|
+
restart: {
|
|
45
|
+
when: 'on-failure',
|
|
46
|
+
delayMs: 25,
|
|
47
|
+
maxFailures: 5,
|
|
48
|
+
windowMs: 1000,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With this policy, failed attempts are retried after `25` milliseconds. Once more
|
|
54
|
+
than `5` failures occur inside `1000` milliseconds, `procband` stops retrying
|
|
55
|
+
and resolves the terminal result with `restartSuppressed: true`.
|
|
56
|
+
|
|
57
|
+
## Wait for a Later Attempt
|
|
58
|
+
|
|
59
|
+
`waitFor()` watches future output across restart attempts, so it can wait for a
|
|
60
|
+
line that appears only after earlier attempts fail:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
64
|
+
import process from 'node:process'
|
|
65
|
+
import { tmpdir } from 'node:os'
|
|
66
|
+
import { join } from 'node:path'
|
|
67
|
+
import { supervise } from 'procband'
|
|
68
|
+
|
|
69
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'procband-example-'))
|
|
70
|
+
const attemptFile = join(stateDir, 'attempt.txt')
|
|
71
|
+
writeFileSync(attemptFile, '0')
|
|
72
|
+
|
|
73
|
+
const script = [
|
|
74
|
+
'const fs = await import("node:fs")',
|
|
75
|
+
'const file = process.argv[1]',
|
|
76
|
+
'let attempt = Number(fs.readFileSync(file, "utf8"))',
|
|
77
|
+
'attempt += 1',
|
|
78
|
+
'fs.writeFileSync(file, String(attempt))',
|
|
79
|
+
'console.log(`attempt ${attempt}`)',
|
|
80
|
+
'if (attempt < 3) process.exit(1)',
|
|
81
|
+
'console.log("ready")',
|
|
82
|
+
].join(';')
|
|
83
|
+
|
|
84
|
+
const proc = supervise({
|
|
85
|
+
name: 'job',
|
|
86
|
+
command: process.execPath,
|
|
87
|
+
args: ['-e', script, attemptFile],
|
|
88
|
+
restart: {
|
|
89
|
+
delayMs: 25,
|
|
90
|
+
maxFailures: 5,
|
|
91
|
+
windowMs: 1000,
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
proc.match(/^attempt \d+$/, (event) => {
|
|
96
|
+
console.log(`observed ${event.line}`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await proc.waitFor('ready')
|
|
100
|
+
const result = await proc
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The first two attempts can exit unsuccessfully, the third attempt can print
|
|
104
|
+
`ready`, and the final result includes the number of restart attempts that were
|
|
105
|
+
started.
|
|
106
|
+
|
|
107
|
+
## Pick `when` Deliberately
|
|
108
|
+
|
|
109
|
+
| `when` | Restarts after | Use it when |
|
|
110
|
+
| -------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
|
|
111
|
+
| `'on-failure'` | Non-zero exits and signal exits. | A clean exit means the work is done. |
|
|
112
|
+
| `'on-exit'` | Any exit. | The child is expected to be long-lived and should be relaunched even after code `0`. |
|
|
113
|
+
|
|
114
|
+
`proc.kill()` disables future restarts before stopping the active process tree,
|
|
115
|
+
so deliberate shutdown does not start another attempt.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Documentation Overview
|
|
2
|
+
|
|
3
|
+
> Choose the right `procband` behavior before reaching for an API: readiness
|
|
4
|
+
> matching, failure observation, restart policy, and shutdown each change how a
|
|
5
|
+
> script should supervise its child process.
|
|
6
|
+
|
|
7
|
+
`procband` is for TypeScript scripts that need to run child processes with a
|
|
8
|
+
small amount of supervision. It is not a daemon, service manager, CLI runner, or
|
|
9
|
+
multi-process orchestrator.
|
|
10
|
+
|
|
11
|
+
## What It Adds
|
|
12
|
+
|
|
13
|
+
Each `supervise()` call wraps one child process and adds five behaviors on top
|
|
14
|
+
of Node.js `spawn()`:
|
|
15
|
+
|
|
16
|
+
| Behavior | Use it when |
|
|
17
|
+
| ------------------------------ | -------------------------------------------------------------------------- |
|
|
18
|
+
| Optional prefixed output | Multiple child processes write to the same parent terminal. |
|
|
19
|
+
| Future line matching | The parent script must wait for a readiness line or react to logs. |
|
|
20
|
+
| Optional restarts | A transient failure should start another child attempt. |
|
|
21
|
+
| Process-tree shutdown | Deliberate cleanup should stop descendants as well as the direct child. |
|
|
22
|
+
| Unobserved failure propagation | A background failure should fail the parent script when nobody awaited it. |
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import process from 'node:process'
|
|
26
|
+
import { supervise } from 'procband'
|
|
27
|
+
|
|
28
|
+
const proc = supervise({
|
|
29
|
+
name: 'api',
|
|
30
|
+
command: process.execPath,
|
|
31
|
+
args: ['-e', 'console.log("ready")'],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await proc.waitFor('ready')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
After `supervise()` runs, the child has already started and future output can be
|
|
38
|
+
matched.
|
|
39
|
+
|
|
40
|
+
## First Decisions
|
|
41
|
+
|
|
42
|
+
| Decision | Default | Change it when |
|
|
43
|
+
| ---------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
44
|
+
| Process identity | `name` is inferred from `command`. | The command path does not end in `/[-\w]+$/`, or logs need a stable label. |
|
|
45
|
+
| Output prefix | Child output is labeled. | Pass `prefix: false` when the child should write raw output. |
|
|
46
|
+
| Failure handling | Awaiting resolves to `ProcessResult`. | Use `expectSuccess()` for command-runner steps that should reject. |
|
|
47
|
+
| Matching scope | `waitFor()` and `match()` watch both streams. | Pass `{ stream: 'stdout' }` or `{ stream: 'stderr' }` to narrow matching. |
|
|
48
|
+
| Restart behavior | No restart. | Pass `restart: true` or a policy for transient child failures. |
|
|
49
|
+
| Stdin | Disconnected. | Pass `stdin: true` or a readable stream when the child needs input. |
|
|
50
|
+
|
|
51
|
+
## Where to Go Next
|
|
52
|
+
|
|
53
|
+
| Page | Reader job |
|
|
54
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------- |
|
|
55
|
+
| [Readiness and matching](guides/readiness.md) | Wait for future output or subscribe to repeated matching lines. |
|
|
56
|
+
| [Restart failed processes](guides/restarts.md) | Configure retries with a failure-suppression guard. |
|
|
57
|
+
| [Foreground commands](guides/foreground-commands.md) | Make a supervised command reject on failed exit. |
|
|
58
|
+
| [Concepts and lifecycle](context.md) | Understand terminal state, shutdown, config boundaries, and errors. |
|
|
59
|
+
|
|
60
|
+
Runnable scripts in [`../examples/`](../examples/) cover the same workflows with
|
|
61
|
+
project-realistic child processes that can be checked by `pnpm docs:check`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procband",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Supervise subprocesses from TypeScript: prefix logs, wait for output patterns, and manage lifecycle/restarts.",
|
|
5
|
+
"packageManager": "pnpm@11.5.1",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"author": "Alec Larson",
|
|
7
8
|
"repository": {
|
|
@@ -18,8 +19,19 @@
|
|
|
18
19
|
"types": "./dist/index.d.mts",
|
|
19
20
|
"default": "./dist/index.mjs"
|
|
20
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsdown --sourcemap --watch",
|
|
24
|
+
"build": "tsdown",
|
|
25
|
+
"docs:check": "pnpm build && tsc -p examples --noEmit && node examples/basic-usage.ts && node examples/restart-on-failure.ts",
|
|
26
|
+
"format": "oxfmt .",
|
|
27
|
+
"lint": "oxlint src",
|
|
28
|
+
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"prepublishOnly": "pnpm build"
|
|
31
|
+
},
|
|
21
32
|
"devDependencies": {
|
|
22
33
|
"@types/node": "^25.5.2",
|
|
34
|
+
"lildocs": "^0.1.11",
|
|
23
35
|
"oxfmt": "^0.43.0",
|
|
24
36
|
"oxlint": "^1.58.0",
|
|
25
37
|
"tsdown": "^0.21.7",
|
|
@@ -29,14 +41,5 @@
|
|
|
29
41
|
"dependencies": {
|
|
30
42
|
"@alloc/tree-kill": "^1.4.0",
|
|
31
43
|
"ansi-styles": "^6.2.3"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"dev": "tsdown --sourcemap --watch",
|
|
35
|
-
"build": "tsdown",
|
|
36
|
-
"docs:check": "pnpm build && tsc -p examples --noEmit && node examples/basic-usage.ts && node examples/restart-on-failure.ts",
|
|
37
|
-
"format": "oxfmt .",
|
|
38
|
-
"lint": "oxlint src",
|
|
39
|
-
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
|
40
|
-
"test": "vitest"
|
|
41
44
|
}
|
|
42
|
-
}
|
|
45
|
+
}
|