procband 0.3.1 → 0.3.3

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/dist/index.d.mts CHANGED
@@ -7,7 +7,7 @@ import { Readable, Writable } from "node:stream";
7
7
  */
8
8
  type Signals = NodeJS.Signals;
9
9
  /**
10
- * Signal values accepted by `kill()` and `stop()`.
10
+ * Signal values accepted by `kill()`.
11
11
  */
12
12
  type KillSignal = number | Signals;
13
13
  /**
@@ -47,6 +47,12 @@ interface ProcessConfig {
47
47
  * Environment variables for the child process.
48
48
  */
49
49
  env?: Record<string, string | undefined>;
50
+ /**
51
+ * Whether to spawn the child in a detached process group/session.
52
+ *
53
+ * This is passed through to Node.js `spawn()` unchanged.
54
+ */
55
+ detached?: boolean;
50
56
  /**
51
57
  * Human-facing label used in prefixed output.
52
58
  *
@@ -81,7 +87,7 @@ interface ProcessConfig {
81
87
  * When provided, `procband` uses this signal for parent-driven cleanup on
82
88
  * `SIGINT`, `SIGTERM`, and `exit` instead of mirroring the parent signal or
83
89
  * relying on the platform default. This only affects parent-driven cleanup;
84
- * `proc.stop()` and `proc.kill()` still use their own explicit signals.
90
+ * `proc.kill()` still uses its own explicit signal argument.
85
91
  */
86
92
  parentExitSignal?: KillSignal;
87
93
  /**
@@ -180,23 +186,6 @@ type MatchCallback = (event: MatchEvent) => void;
180
186
  * Stops a callback subscription created by `match()`.
181
187
  */
182
188
  type Unsubscribe = () => void;
183
- /**
184
- * Options for `stop()`.
185
- */
186
- interface StopOptions {
187
- /**
188
- * Signal used for the initial process-tree shutdown attempt.
189
- *
190
- * Defaults to `"SIGTERM"`.
191
- */
192
- signal?: KillSignal;
193
- /**
194
- * Delay before escalating to `SIGKILL`.
195
- *
196
- * Defaults to `5000`.
197
- */
198
- killAfterMs?: number;
199
- }
200
189
  /**
201
190
  * Final state for a supervised process after all restart attempts are done.
202
191
  */
@@ -273,11 +262,11 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
273
262
  /**
274
263
  * Disable future restarts and terminate the active process tree.
275
264
  *
276
- * `stop()` targets the current child process and any descendants it spawned.
277
- * It first uses the configured `signal`, then escalates to `SIGKILL` after
278
- * `killAfterMs` if the tree is still alive.
265
+ * For supervised processes, `kill()` targets the current child process and
266
+ * any descendants it spawned instead of only the direct child.
267
+ * `kill(0)` preserves the normal `ChildProcess` existence-check behavior.
279
268
  */
280
- stop(options?: StopOptions): Promise<void>;
269
+ kill(signal?: KillSignal): boolean;
281
270
  }
282
271
  //#endregion
283
272
  //#region src/process.d.ts
@@ -310,4 +299,4 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
310
299
  */
311
300
  declare function supervise(config: ProcessConfig): ProcbandProcess;
312
301
  //#endregion
313
- export { type MatchCallback, type MatchEvent, type MatchOptions, type ProcbandProcess, type ProcessConfig, type ProcessResult, type RestartPolicy, type RgbColor, type StopOptions, type Unsubscribe, type WaitForOptions, supervise };
302
+ export { type MatchCallback, type MatchEvent, type MatchOptions, type ProcbandProcess, type ProcessConfig, type ProcessResult, type RestartPolicy, type RgbColor, type Unsubscribe, type WaitForOptions, supervise };
package/dist/index.mjs CHANGED
@@ -111,7 +111,7 @@ var MatchRegistry = class {
111
111
  }
112
112
  waitFor(pattern, options) {
113
113
  if (this.closedError) return Promise.reject(this.closedError);
114
- return new Promise((resolve, reject) => {
114
+ const promise = new Promise((resolve, reject) => {
115
115
  const subscription = {
116
116
  active: true,
117
117
  kind: "promise",
@@ -125,6 +125,9 @@ var MatchRegistry = class {
125
125
  this.unsubscribe(subscription);
126
126
  reject(error);
127
127
  },
128
+ suppressUnhandledRejection: () => {
129
+ promise.catch(() => {});
130
+ },
128
131
  timer: null
129
132
  };
130
133
  if (options?.timeoutMs != null) subscription.timer = setTimeout(() => {
@@ -132,6 +135,7 @@ var MatchRegistry = class {
132
135
  }, options.timeoutMs);
133
136
  this.subscriptions.add(subscription);
134
137
  });
138
+ return promise;
135
139
  }
136
140
  emit(stream, line) {
137
141
  if (this.closedError) return;
@@ -155,11 +159,17 @@ var MatchRegistry = class {
155
159
  subscription.resolve(event);
156
160
  }
157
161
  }
162
+ hasPendingWait() {
163
+ for (const subscription of this.subscriptions) if (subscription.kind === "promise") return true;
164
+ return false;
165
+ }
158
166
  close(error) {
159
167
  if (this.closedError) return;
160
168
  this.closedError = error;
161
- for (const subscription of [...this.subscriptions]) if (subscription.kind === "promise") subscription.reject(error);
162
- else this.unsubscribe(subscription);
169
+ for (const subscription of [...this.subscriptions]) if (subscription.kind === "promise") {
170
+ subscription.suppressUnhandledRejection();
171
+ subscription.reject(error);
172
+ } else this.unsubscribe(subscription);
163
173
  }
164
174
  unsubscribe(subscription) {
165
175
  if (!subscription.active) return;
@@ -387,7 +397,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
387
397
  stderrSink;
388
398
  stderrSinkActive = true;
389
399
  stderrSinkCleanup = null;
390
- stopPromise = null;
400
+ shutdownPromise = null;
391
401
  shutdownRequested = false;
392
402
  restartDisabled = false;
393
403
  finalized = false;
@@ -466,9 +476,12 @@ var ProcbandProcessImpl = class extends EventEmitter {
466
476
  return this.finalPromise.then(onfulfilled, onrejected);
467
477
  }
468
478
  kill(signal) {
469
- const child = this.currentChild;
470
- if (!child) return false;
471
- return child.kill(signal);
479
+ if (signal === 0) return this.currentChild?.kill(0) ?? false;
480
+ if (!this.attempt || this.finalized) return false;
481
+ this.beginShutdown(signal ?? "SIGTERM", 5e3).catch((error) => {
482
+ this.emit("error", error);
483
+ });
484
+ return true;
472
485
  }
473
486
  ref() {
474
487
  this.currentChild?.ref();
@@ -481,26 +494,24 @@ var ProcbandProcessImpl = class extends EventEmitter {
481
494
  disconnect() {
482
495
  this.currentChild?.disconnect();
483
496
  }
484
- async stop(options) {
485
- if (this.stopPromise) return this.stopPromise;
497
+ prepareShutdown() {
486
498
  this.shutdownRequested = true;
487
499
  this.restartDisabled = true;
488
500
  this.restart.cancelDelay();
489
- this.stopPromise = this.stopActiveTree(options?.signal ?? "SIGTERM", options?.killAfterMs ?? 5e3);
490
- return this.stopPromise;
501
+ }
502
+ beginShutdown(signal, killAfterMs) {
503
+ this.prepareShutdown();
504
+ if (this.shutdownPromise) return this.shutdownPromise;
505
+ this.shutdownPromise = this.stopActiveTree(signal, killAfterMs);
506
+ return this.shutdownPromise;
491
507
  }
492
508
  cleanupFromExit() {
493
- this.shutdownRequested = true;
494
- this.restartDisabled = true;
495
- this.restart.cancelDelay();
509
+ this.prepareShutdown();
496
510
  const child = this.currentChild;
497
511
  if (child) killTreeBestEffort(child, this.getParentCleanupSignal());
498
512
  }
499
513
  async cleanupFromSignal(signal) {
500
- this.shutdownRequested = true;
501
- this.restartDisabled = true;
502
- this.restart.cancelDelay();
503
- await this.stopActiveTree(this.getParentCleanupSignal(signal), 1e3);
514
+ await this.beginShutdown(this.getParentCleanupSignal(signal), 1e3);
504
515
  }
505
516
  async stopActiveTree(signal, killAfterMs) {
506
517
  const attempt = this.attempt;
@@ -517,6 +528,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
517
528
  this.generation += 1;
518
529
  const child = spawn(this.config.command, this.config.args ?? [], {
519
530
  cwd: this.config.cwd,
531
+ detached: this.config.detached,
520
532
  env: this.config.env,
521
533
  stdio: [
522
534
  this.config.stdin ? "pipe" : "ignore",
@@ -637,11 +649,16 @@ var ProcbandProcessImpl = class extends EventEmitter {
637
649
  liveProcesses.delete(this);
638
650
  if (liveProcesses.size === 0) propagatedFailure = false;
639
651
  if (this.shouldPropagateFailure(result)) propagateFailure(result.exitCode);
640
- this.matches.close(/* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`));
652
+ this.closeMatches();
641
653
  this.unbindStderrSink();
642
654
  unregisterCleanupTarget(this);
643
655
  this.finalResolve(result);
644
656
  }
657
+ closeMatches() {
658
+ const error = /* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`);
659
+ if (this.matches.hasPendingWait()) writePrefixedLine(process.stderr, this.label, stderrColor, error.message, true);
660
+ this.matches.close(error);
661
+ }
645
662
  markTerminalResultObserved() {
646
663
  this.terminalResultObserved = true;
647
664
  }
@@ -720,7 +737,7 @@ function propagateFailure(exitCode) {
720
737
  if (propagatedFailure) return;
721
738
  propagatedFailure = true;
722
739
  if (process.exitCode == null || process.exitCode === 0) process.exitCode = exitCode;
723
- for (const proc of liveProcesses) proc.stop().catch(() => {});
740
+ for (const proc of liveProcesses) proc.kill();
724
741
  }
725
742
  //#endregion
726
743
  export { supervise };
package/docs/context.md CHANGED
@@ -36,8 +36,8 @@ Supervision adds five behaviors on top of raw `spawn()`:
36
36
 
37
37
  - `ProcessConfig`
38
38
  Declares one supervised subprocess plus its label, color, restart policy,
39
- optional stdin behavior, optional parent-exit signal override, and optional
40
- raw `stderr` tee.
39
+ optional stdin behavior, optional detached spawn mode, optional
40
+ parent-exit signal override, and optional raw `stderr` tee.
41
41
  - `ProcbandProcess`
42
42
  The live wrapper returned by `supervise()`. It is a `ChildProcess`-compatible
43
43
  handle, a matching surface, a shutdown surface, and a thenable final result.
@@ -73,7 +73,7 @@ Supervision adds five behaviors on top of raw `spawn()`:
73
73
  - React to repeated matching output:
74
74
  `proc.match(pattern, callback, options)`
75
75
  - Stop the process and its descendants:
76
- `proc.stop()`
76
+ `proc.kill()`
77
77
  - Inspect final exit state:
78
78
  `await proc` or `await proc.wait()`
79
79
  - Take ownership of a process failure:
@@ -95,7 +95,7 @@ Supervision adds five behaviors on top of raw `spawn()`:
95
95
 
96
96
  # Recommended Patterns
97
97
 
98
- - Use `proc.stop()` for deliberate shutdown initiated by your own script.
98
+ - Use `proc.kill()` for deliberate shutdown initiated by your own script.
99
99
  - Reserve `parentExitSignal` for children that expect a specific signal from
100
100
  their supervisor during parent-driven cleanup.
101
101
  - Await `proc` or call `proc.wait()` when your script intends to own failure
@@ -103,11 +103,10 @@ Supervision adds five behaviors on top of raw `spawn()`:
103
103
 
104
104
  # Patterns to Avoid
105
105
 
106
- - Do not treat `parentExitSignal` as a replacement for `StopOptions.signal`.
107
- The former only changes parent-driven cleanup; the latter controls
108
- explicit `proc.stop()` calls.
109
- - Do not rely on `kill()` for full shutdown when descendants may still be
110
- running. `kill()` only targets the current direct child.
106
+ - Do not treat `parentExitSignal` as a replacement for the signal passed to
107
+ `proc.kill(signal)`. The former only changes parent-driven cleanup.
108
+ - Do not expect `kill(0)` to stop supervision. `kill(0)` remains an existence
109
+ check for the current child attempt.
111
110
  - Do not expect historical log replay from `match()` or `waitFor()`. Matching
112
111
  is future-only by design.
113
112
 
@@ -131,12 +130,12 @@ Supervision adds five behaviors on top of raw `spawn()`:
131
130
  - The wrapper survives restarts, but inherited `pid`, `stdin`, `stdout`,
132
131
  `stderr`, and related `ChildProcess` fields always refer to the current active
133
132
  child attempt. `stdin` is `null` unless `ProcessConfig.stdin` is enabled.
134
- - `kill()` only signals the current direct child. `stop()` disables restart and
135
- kills the full process tree.
133
+ - `kill()` disables future restarts and kills the full process tree, except for
134
+ `kill(0)`, which only checks whether the current child attempt exists.
136
135
  - Parent cleanup installs both `SIGINT` and `SIGTERM` handlers while any live
137
136
  supervised process exists. Set `ProcessConfig.parentExitSignal` to override
138
137
  which signal is sent to the child tree during parent-driven cleanup. This
139
- does not change the signal used by explicit `proc.stop()` calls.
138
+ does not change the signal used by explicit `proc.kill()` calls.
140
139
  - `stderr` prefixes always use the reserved red, even when a custom process
141
140
  color is configured.
142
141
  - `ProcessConfig.name` is optional. When omitted, it falls back to the
@@ -153,7 +152,7 @@ Supervision adds five behaviors on top of raw `spawn()`:
153
152
  supervision.
154
153
  - Unobserved terminal failures do not reject promises. They set the parent
155
154
  `process.exitCode` and start stopping sibling `procband` processes.
156
- - `stop()` may reject if tree-kill fails with a non-`ESRCH` error.
155
+ - `kill()` emits `error` if tree-kill fails with a non-`ESRCH` error.
157
156
 
158
157
  # Terminology
159
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "procband",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Supervise subprocesses from TypeScript: prefix logs, wait for output patterns, and manage lifecycle/restarts.",
5
5
  "license": "MIT",
6
6
  "author": "Alec Larson",