procband 0.0.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # procband
2
+
3
+ ## Purpose
4
+
5
+ `procband` supervises subprocesses from TypeScript. It prefixes child output,
6
+ matches future log lines, waits for terminal exit, optionally restarts failed
7
+ processes, tree-kills descendants during shutdown, and propagates unobserved
8
+ terminal failures to the parent process.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ pnpm add procband
14
+ ```
15
+
16
+ ## Quick Example
17
+
18
+ ```ts
19
+ import process from 'node:process'
20
+ import { supervise } from 'procband'
21
+
22
+ const proc = supervise({
23
+ name: 'api',
24
+ command: process.execPath,
25
+ args: ['-e', 'console.log("ready")'],
26
+ })
27
+
28
+ await proc.waitFor('ready')
29
+ const result = await proc
30
+ console.log(result)
31
+ ```
32
+
33
+ ## Documentation Map
34
+
35
+ - Concepts and lifecycle: [docs/context.md](docs/context.md)
36
+ - Minimal usage example: [examples/basic-usage.ts](examples/basic-usage.ts)
37
+ - Restart example: [examples/restart-on-failure.ts](examples/restart-on-failure.ts)
38
+ - Sequencing example: [examples/sequenced-processes.ts](examples/sequenced-processes.ts)
39
+ - Exact exported signatures: [dist/index.d.mts](dist/index.d.mts)
@@ -0,0 +1,294 @@
1
+ import { ChildProcess } from "node:child_process";
2
+ import { Writable } from "node:stream";
3
+
4
+ //#region src/types.d.ts
5
+ /**
6
+ * Accepted Node.js signal names.
7
+ */
8
+ type Signals = NodeJS.Signals;
9
+ /**
10
+ * Signal values accepted by `kill()` and `stop()`.
11
+ */
12
+ type KillSignal = number | Signals;
13
+ /**
14
+ * A line matcher used by `match()` and `waitFor()`.
15
+ *
16
+ * String patterns use substring matching. `RegExp` patterns are executed
17
+ * against the full observed line.
18
+ */
19
+ type MatchPattern = string | RegExp;
20
+ /**
21
+ * The output streams that can participate in line matching.
22
+ */
23
+ type MatchStream = 'stdout' | 'stderr' | 'both';
24
+ /**
25
+ * Configuration for a single supervised subprocess.
26
+ */
27
+ interface ProcessConfig {
28
+ /**
29
+ * Stable identifier used in prefixed output and emitted match events.
30
+ */
31
+ name: string;
32
+ /**
33
+ * Executable or shell-free command name passed to `spawn()`.
34
+ */
35
+ command: string;
36
+ /**
37
+ * Positional arguments passed to the child process.
38
+ */
39
+ args?: string[];
40
+ /**
41
+ * Working directory for the child process.
42
+ */
43
+ cwd?: string;
44
+ /**
45
+ * Environment variables for the child process.
46
+ */
47
+ env?: Record<string, string | undefined>;
48
+ /**
49
+ * Human-facing label used in prefixed output.
50
+ *
51
+ * Defaults to `name`.
52
+ */
53
+ label?: string;
54
+ /**
55
+ * RGB color for this process's `stdout` prefix.
56
+ *
57
+ * When omitted, `procband` assigns the next color from its default palette.
58
+ * The reserved `stderr` red cannot be used here.
59
+ */
60
+ color?: RgbColor;
61
+ /**
62
+ * Optional extra sink for raw child `stderr` bytes.
63
+ *
64
+ * This is additive. It does not replace the normal prefixed writes to
65
+ * `process.stderr`.
66
+ */
67
+ stderr?: Writable;
68
+ /**
69
+ * Automatic restart behavior for terminal child exits.
70
+ *
71
+ * Use `true` for the built-in defaults or provide an explicit policy.
72
+ */
73
+ restart?: boolean | RestartPolicy;
74
+ }
75
+ /**
76
+ * A 24-bit RGB color tuple.
77
+ */
78
+ type RgbColor = readonly [red: number, green: number, blue: number];
79
+ /**
80
+ * Controls whether and how a supervised process restarts after exit.
81
+ */
82
+ interface RestartPolicy {
83
+ /**
84
+ * Which exit outcomes trigger a restart.
85
+ *
86
+ * Defaults to `"on-failure"`.
87
+ */
88
+ when?: 'on-failure' | 'on-exit';
89
+ /**
90
+ * Delay before spawning the next attempt.
91
+ *
92
+ * Defaults to `1000`.
93
+ */
94
+ delayMs?: number;
95
+ /**
96
+ * Maximum failed exits allowed inside `windowMs` before restart is
97
+ * suppressed.
98
+ *
99
+ * Defaults to `3`.
100
+ */
101
+ maxFailures?: number;
102
+ /**
103
+ * Rolling time window used for failed-exit suppression.
104
+ *
105
+ * Defaults to `30000`.
106
+ */
107
+ windowMs?: number;
108
+ }
109
+ /**
110
+ * Common options for line-matching APIs.
111
+ */
112
+ interface MatchOptions {
113
+ /**
114
+ * Which output stream to inspect.
115
+ *
116
+ * Defaults to `"both"`.
117
+ */
118
+ stream?: MatchStream;
119
+ }
120
+ /**
121
+ * Options for `waitFor()`.
122
+ */
123
+ interface WaitForOptions extends MatchOptions {
124
+ /**
125
+ * Maximum time to wait for a future match before rejecting.
126
+ */
127
+ timeoutMs?: number;
128
+ }
129
+ /**
130
+ * A matched output line observed from a supervised process.
131
+ */
132
+ interface MatchEvent {
133
+ /**
134
+ * The `ProcessConfig.name` associated with the process.
135
+ */
136
+ process: string;
137
+ /**
138
+ * Which child output stream produced the line.
139
+ */
140
+ stream: 'stdout' | 'stderr';
141
+ /**
142
+ * The matched line without its trailing newline.
143
+ */
144
+ line: string;
145
+ /**
146
+ * The `RegExp.exec()` result for regex patterns, or `null` for string
147
+ * patterns.
148
+ */
149
+ match: RegExpExecArray | null;
150
+ /**
151
+ * The wall-clock timestamp, in milliseconds since the Unix epoch, when the
152
+ * line was emitted to subscribers.
153
+ */
154
+ timestamp: number;
155
+ }
156
+ /**
157
+ * Callback invoked for each future matching line.
158
+ */
159
+ type MatchCallback = (event: MatchEvent) => void;
160
+ /**
161
+ * Stops a callback subscription created by `match()`.
162
+ */
163
+ type Unsubscribe = () => void;
164
+ /**
165
+ * Options for `stop()`.
166
+ */
167
+ interface StopOptions {
168
+ /**
169
+ * Signal used for the initial process-tree shutdown attempt.
170
+ *
171
+ * Defaults to `"SIGTERM"`.
172
+ */
173
+ signal?: KillSignal;
174
+ /**
175
+ * Delay before escalating to `SIGKILL`.
176
+ *
177
+ * Defaults to `5000`.
178
+ */
179
+ killAfterMs?: number;
180
+ }
181
+ /**
182
+ * Final state for a supervised process after all restart attempts are done.
183
+ */
184
+ interface ProcessResult {
185
+ /**
186
+ * The `ProcessConfig.name` for the completed process.
187
+ */
188
+ name: string;
189
+ /**
190
+ * Final child exit code, or `null` when the process exited by signal.
191
+ */
192
+ code: number | null;
193
+ /**
194
+ * Shell-style exit status for the final process outcome.
195
+ *
196
+ * This is equal to `code` for normal exits, or `128 + signalNumber` when the
197
+ * process exited by signal.
198
+ */
199
+ exitCode: number;
200
+ /**
201
+ * Final terminating signal, or `null` when the process exited normally.
202
+ */
203
+ signal: Signals | null;
204
+ /**
205
+ * Number of restart attempts that were started.
206
+ */
207
+ restarts: number;
208
+ /**
209
+ * Whether restart was disabled by the failure-suppression guard.
210
+ */
211
+ restartSuppressed: boolean;
212
+ }
213
+ /**
214
+ * A supervised child-process handle.
215
+ *
216
+ * `ProcbandProcess` behaves like the current active `ChildProcess`, while also
217
+ * exposing matching, terminal shutdown, and final-result helpers. The wrapper
218
+ * is thenable, so `await proc` is equivalent to `await proc.wait()`.
219
+ */
220
+ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
221
+ /**
222
+ * Subscribe to future matching lines from this process.
223
+ *
224
+ * Matching is line-based and forward-only. One subscription does not consume
225
+ * events from another. If `onMatch` throws, only that subscription is
226
+ * removed.
227
+ *
228
+ * @param pattern The string or regular expression to match against each
229
+ * future line.
230
+ * @param onMatch Invoked for every future matching line.
231
+ * @param options Optional stream filter.
232
+ * @returns An idempotent function that unsubscribes the callback.
233
+ */
234
+ match(pattern: MatchPattern, onMatch: MatchCallback, options?: MatchOptions): Unsubscribe;
235
+ /**
236
+ * Wait for the first future matching line from this process.
237
+ *
238
+ * @param pattern The string or regular expression to match against each
239
+ * future line.
240
+ * @param options Optional stream filter and timeout.
241
+ * @returns The first future matching event.
242
+ * @throws When `timeoutMs` elapses before a match is observed.
243
+ * @throws When the process becomes terminal before a match is observed.
244
+ */
245
+ waitFor(pattern: MatchPattern, options?: WaitForOptions): Promise<MatchEvent>;
246
+ /**
247
+ * Wait for the process to become terminal with no further restart pending.
248
+ *
249
+ * Unlike many process helpers, this resolves for both success and failure.
250
+ * Inspect the returned `ProcessResult` instead of relying on promise
251
+ * rejection for non-zero exits.
252
+ */
253
+ wait(): Promise<ProcessResult>;
254
+ /**
255
+ * Disable future restarts and terminate the active process tree.
256
+ *
257
+ * `stop()` targets the current child process and any descendants it spawned.
258
+ * It first uses the configured `signal`, then escalates to `SIGKILL` after
259
+ * `killAfterMs` if the tree is still alive.
260
+ */
261
+ stop(options?: StopOptions): Promise<void>;
262
+ }
263
+ //#endregion
264
+ //#region src/process.d.ts
265
+ /**
266
+ * Start supervising a single subprocess.
267
+ *
268
+ * The returned `ProcbandProcess` starts immediately, prefixes child output,
269
+ * exposes line-based matching helpers, and resolves when the process reaches a
270
+ * terminal state with no further restart pending.
271
+ *
272
+ * @param config The subprocess configuration to supervise.
273
+ * @returns A `ChildProcess`-compatible wrapper with procband-specific helpers.
274
+ * @throws When `config` is invalid, such as a missing `name`, missing
275
+ * `command`, or an invalid reserved color.
276
+ * @example
277
+ * ```ts
278
+ * import process from 'node:process'
279
+ * import { supervise } from 'procband'
280
+ *
281
+ * const proc = supervise({
282
+ * name: 'server',
283
+ * command: process.execPath,
284
+ * args: ['-e', 'console.log("ready")'],
285
+ * })
286
+ *
287
+ * await proc.waitFor('ready')
288
+ * const result = await proc
289
+ * console.log(result)
290
+ * ```
291
+ */
292
+ declare function supervise(config: ProcessConfig): ProcbandProcess;
293
+ //#endregion
294
+ export { type MatchCallback, type MatchEvent, type MatchOptions, type ProcbandProcess, type ProcessConfig, type ProcessResult, type RestartPolicy, type RgbColor, type StopOptions, type Unsubscribe, type WaitForOptions, supervise };
package/dist/index.mjs ADDED
@@ -0,0 +1,705 @@
1
+ import { spawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { constants } from "node:os";
4
+ import process from "node:process";
5
+ import ansiStyles from "ansi-styles";
6
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
+ import treeKill, { treeKillSync } from "@alloc/tree-kill";
8
+ //#region src/colors.ts
9
+ const prefixPalette = [
10
+ [
11
+ 99,
12
+ 102,
13
+ 241
14
+ ],
15
+ [
16
+ 14,
17
+ 165,
18
+ 233
19
+ ],
20
+ [
21
+ 6,
22
+ 182,
23
+ 212
24
+ ],
25
+ [
26
+ 16,
27
+ 185,
28
+ 129
29
+ ],
30
+ [
31
+ 132,
32
+ 204,
33
+ 22
34
+ ],
35
+ [
36
+ 245,
37
+ 158,
38
+ 11
39
+ ],
40
+ [
41
+ 249,
42
+ 115,
43
+ 22
44
+ ],
45
+ [
46
+ 236,
47
+ 72,
48
+ 153
49
+ ],
50
+ [
51
+ 168,
52
+ 85,
53
+ 247
54
+ ],
55
+ [
56
+ 139,
57
+ 92,
58
+ 246
59
+ ]
60
+ ];
61
+ const stderrColor = [
62
+ 239,
63
+ 68,
64
+ 68
65
+ ];
66
+ let nextPaletteIndex = 0;
67
+ function resolveProcessColor(color) {
68
+ if (color) return [...color];
69
+ const paletteColor = prefixPalette[nextPaletteIndex % prefixPalette.length];
70
+ nextPaletteIndex += 1;
71
+ return [...paletteColor];
72
+ }
73
+ function validateProcessColor(color) {
74
+ if (!color) return;
75
+ const [red, green, blue] = color;
76
+ for (const value of [
77
+ red,
78
+ green,
79
+ blue
80
+ ]) if (!Number.isInteger(value) || value < 0 || value > 255) throw new Error("ProcessConfig.color must contain integer RGB values between 0 and 255");
81
+ if (red === stderrColor[0] && green === stderrColor[1] && blue === stderrColor[2]) throw new Error("ProcessConfig.color cannot use the reserved stderr color");
82
+ }
83
+ function writePrefixedLine(target, label, color, line, appendNewline) {
84
+ target.write(formatPrefix(label, color) + line + (appendNewline ? "\n" : ""));
85
+ }
86
+ function formatPrefix(label, color) {
87
+ return `${ansiStyles.color.ansi16m(...color)}[${label}]${ansiStyles.color.close} `;
88
+ }
89
+ //#endregion
90
+ //#region src/matching.ts
91
+ var MatchRegistry = class {
92
+ subscriptions = /* @__PURE__ */ new Set();
93
+ closedError = null;
94
+ processName;
95
+ constructor(processName) {
96
+ this.processName = processName;
97
+ }
98
+ match(pattern, onMatch, options) {
99
+ if (this.closedError) return () => {};
100
+ const subscription = {
101
+ active: true,
102
+ kind: "callback",
103
+ pattern,
104
+ stream: options?.stream ?? "both",
105
+ onMatch
106
+ };
107
+ this.subscriptions.add(subscription);
108
+ return () => {
109
+ this.unsubscribe(subscription);
110
+ };
111
+ }
112
+ waitFor(pattern, options) {
113
+ if (this.closedError) return Promise.reject(this.closedError);
114
+ return new Promise((resolve, reject) => {
115
+ const subscription = {
116
+ active: true,
117
+ kind: "promise",
118
+ pattern,
119
+ stream: options?.stream ?? "both",
120
+ resolve: (event) => {
121
+ this.unsubscribe(subscription);
122
+ resolve(event);
123
+ },
124
+ reject: (error) => {
125
+ this.unsubscribe(subscription);
126
+ reject(error);
127
+ },
128
+ timer: null
129
+ };
130
+ if (options?.timeoutMs != null) subscription.timer = setTimeout(() => {
131
+ subscription.reject(/* @__PURE__ */ new Error(`Timed out waiting for a match from process "${this.processName}"`));
132
+ }, options.timeoutMs);
133
+ this.subscriptions.add(subscription);
134
+ });
135
+ }
136
+ emit(stream, line) {
137
+ if (this.closedError) return;
138
+ const baseEvent = createMatchEvent(this.processName, stream, line);
139
+ for (const subscription of [...this.subscriptions]) {
140
+ if (!subscription.active || !matchesStream(subscription.stream, stream)) continue;
141
+ const match = matchPattern(subscription.pattern, line);
142
+ if (!match.matched) continue;
143
+ const event = {
144
+ ...baseEvent,
145
+ match: match.value
146
+ };
147
+ if (subscription.kind === "callback") {
148
+ try {
149
+ subscription.onMatch(event);
150
+ } catch {
151
+ this.unsubscribe(subscription);
152
+ }
153
+ continue;
154
+ }
155
+ subscription.resolve(event);
156
+ }
157
+ }
158
+ close(error) {
159
+ if (this.closedError) return;
160
+ this.closedError = error;
161
+ for (const subscription of [...this.subscriptions]) if (subscription.kind === "promise") subscription.reject(error);
162
+ else this.unsubscribe(subscription);
163
+ }
164
+ unsubscribe(subscription) {
165
+ if (!subscription.active) return;
166
+ subscription.active = false;
167
+ if (subscription.kind === "promise" && subscription.timer) {
168
+ clearTimeout(subscription.timer);
169
+ subscription.timer = null;
170
+ }
171
+ this.subscriptions.delete(subscription);
172
+ }
173
+ };
174
+ function createMatchEvent(processName, stream, line) {
175
+ return {
176
+ process: processName,
177
+ stream,
178
+ line,
179
+ match: null,
180
+ timestamp: Date.now()
181
+ };
182
+ }
183
+ function matchesStream(expected, actual) {
184
+ return expected === "both" || expected === actual;
185
+ }
186
+ function matchPattern(pattern, line) {
187
+ if (typeof pattern === "string") return {
188
+ matched: line.includes(pattern),
189
+ value: null
190
+ };
191
+ pattern.lastIndex = 0;
192
+ const match = pattern.exec(line);
193
+ return {
194
+ matched: match != null,
195
+ value: match
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region src/restart.ts
200
+ const defaultRestartPolicy = Object.freeze({
201
+ when: "on-failure",
202
+ delayMs: 1e3,
203
+ maxFailures: 3,
204
+ windowMs: 3e4
205
+ });
206
+ var RestartController = class {
207
+ policy;
208
+ restarts = 0;
209
+ restartSuppressed = false;
210
+ restartFailures = [];
211
+ restartTimer = null;
212
+ cancelRestartDelay = null;
213
+ constructor(restart) {
214
+ this.policy = normalizeRestartPolicy(restart);
215
+ }
216
+ shouldRestart(code, signal, restartDisabled, finalized) {
217
+ if (finalized || restartDisabled || !this.policy) return false;
218
+ if (this.policy.when === "on-exit") return true;
219
+ return isFailedExit(code, signal);
220
+ }
221
+ prepareRestart(code, signal) {
222
+ if (!this.policy) return false;
223
+ if (isFailedExit(code, signal)) {
224
+ const now = Date.now();
225
+ this.restartFailures.push(now);
226
+ this.restartFailures = this.restartFailures.filter((timestamp) => now - timestamp <= this.policy.windowMs);
227
+ if (this.restartFailures.length > this.policy.maxFailures) {
228
+ this.restartSuppressed = true;
229
+ return false;
230
+ }
231
+ }
232
+ this.restarts += 1;
233
+ return true;
234
+ }
235
+ waitForDelay() {
236
+ if (!this.policy || this.policy.delayMs <= 0) return Promise.resolve(true);
237
+ return new Promise((resolve) => {
238
+ this.restartTimer = setTimeout(() => {
239
+ this.restartTimer = null;
240
+ this.cancelRestartDelay = null;
241
+ resolve(true);
242
+ }, this.policy.delayMs);
243
+ this.cancelRestartDelay = () => {
244
+ if (this.restartTimer) {
245
+ clearTimeout(this.restartTimer);
246
+ this.restartTimer = null;
247
+ }
248
+ this.cancelRestartDelay = null;
249
+ resolve(false);
250
+ };
251
+ });
252
+ }
253
+ cancelDelay() {
254
+ this.cancelRestartDelay?.();
255
+ }
256
+ };
257
+ function normalizeRestartPolicy(restart) {
258
+ if (!restart) return null;
259
+ if (restart === true) return { ...defaultRestartPolicy };
260
+ return {
261
+ when: restart.when ?? defaultRestartPolicy.when,
262
+ delayMs: restart.delayMs ?? defaultRestartPolicy.delayMs,
263
+ maxFailures: restart.maxFailures ?? defaultRestartPolicy.maxFailures,
264
+ windowMs: restart.windowMs ?? defaultRestartPolicy.windowMs
265
+ };
266
+ }
267
+ function isFailedExit(code, signal) {
268
+ return signal != null || code !== 0;
269
+ }
270
+ //#endregion
271
+ //#region src/shutdown.ts
272
+ const liveTargets = /* @__PURE__ */ new Set();
273
+ let parentCleanupInstalled = false;
274
+ let handlingSignal = false;
275
+ function registerCleanupTarget(target) {
276
+ liveTargets.add(target);
277
+ installParentCleanup();
278
+ }
279
+ function unregisterCleanupTarget(target) {
280
+ liveTargets.delete(target);
281
+ if (liveTargets.size === 0) uninstallParentCleanup();
282
+ }
283
+ function killTreeBestEffort(child, signal) {
284
+ try {
285
+ treeKillSync(child, signal);
286
+ } catch {}
287
+ }
288
+ async function stopChildTree(child, close, isClosed, signal, killAfterMs) {
289
+ try {
290
+ await treeKill(child, signal);
291
+ } catch (error) {
292
+ if (!isMissingProcessError(error)) throw error;
293
+ }
294
+ if (!await Promise.race([close.then(() => true), setTimeout$1(killAfterMs, false)]) && !isClosed()) {
295
+ try {
296
+ await treeKill(child, "SIGKILL");
297
+ } catch (error) {
298
+ if (!isMissingProcessError(error)) throw error;
299
+ }
300
+ await close;
301
+ }
302
+ }
303
+ function installParentCleanup() {
304
+ if (parentCleanupInstalled) return;
305
+ parentCleanupInstalled = true;
306
+ process.on("SIGINT", onParentSigint);
307
+ process.on("SIGTERM", onParentSigterm);
308
+ process.on("exit", onParentExit);
309
+ }
310
+ function uninstallParentCleanup() {
311
+ if (!parentCleanupInstalled) return;
312
+ parentCleanupInstalled = false;
313
+ process.off("SIGINT", onParentSigint);
314
+ process.off("SIGTERM", onParentSigterm);
315
+ process.off("exit", onParentExit);
316
+ }
317
+ function onParentExit() {
318
+ for (const target of [...liveTargets]) target.cleanupFromExit();
319
+ }
320
+ function onParentSigint() {
321
+ onParentSignal("SIGINT");
322
+ }
323
+ function onParentSigterm() {
324
+ onParentSignal("SIGTERM");
325
+ }
326
+ function onParentSignal(signal) {
327
+ if (handlingSignal) return;
328
+ handlingSignal = true;
329
+ const handler = signal === "SIGINT" ? onParentSigint : onParentSigterm;
330
+ const pending = [...liveTargets].map((target) => target.cleanupFromSignal(signal));
331
+ Promise.allSettled(pending).finally(() => {
332
+ handlingSignal = false;
333
+ process.off(signal, handler);
334
+ if (process.listenerCount(signal) === 0) {
335
+ process.kill(process.pid, signal);
336
+ return;
337
+ }
338
+ process.on(signal, handler);
339
+ });
340
+ }
341
+ function isMissingProcessError(error) {
342
+ return typeof error === "object" && error != null && "code" in error && error.code === "ESRCH";
343
+ }
344
+ //#endregion
345
+ //#region src/process.ts
346
+ /**
347
+ * Start supervising a single subprocess.
348
+ *
349
+ * The returned `ProcbandProcess` starts immediately, prefixes child output,
350
+ * exposes line-based matching helpers, and resolves when the process reaches a
351
+ * terminal state with no further restart pending.
352
+ *
353
+ * @param config The subprocess configuration to supervise.
354
+ * @returns A `ChildProcess`-compatible wrapper with procband-specific helpers.
355
+ * @throws When `config` is invalid, such as a missing `name`, missing
356
+ * `command`, or an invalid reserved color.
357
+ * @example
358
+ * ```ts
359
+ * import process from 'node:process'
360
+ * import { supervise } from 'procband'
361
+ *
362
+ * const proc = supervise({
363
+ * name: 'server',
364
+ * command: process.execPath,
365
+ * args: ['-e', 'console.log("ready")'],
366
+ * })
367
+ *
368
+ * await proc.waitFor('ready')
369
+ * const result = await proc
370
+ * console.log(result)
371
+ * ```
372
+ */
373
+ function supervise(config) {
374
+ return new ProcbandProcessImpl(config);
375
+ }
376
+ var ProcbandProcessImpl = class extends EventEmitter {
377
+ config;
378
+ name;
379
+ label;
380
+ color;
381
+ matches;
382
+ restart;
383
+ finalPromise;
384
+ finalResolve;
385
+ attempt = null;
386
+ generation = 0;
387
+ stderrSink;
388
+ stderrSinkActive = true;
389
+ stderrSinkCleanup = null;
390
+ stopPromise = null;
391
+ shutdownRequested = false;
392
+ restartDisabled = false;
393
+ finalized = false;
394
+ lastResult = null;
395
+ terminalResultObserved = false;
396
+ constructor(config) {
397
+ super();
398
+ validateProcessConfig(config);
399
+ this.config = config;
400
+ this.name = config.name;
401
+ this.label = config.label ?? config.name;
402
+ this.color = resolveProcessColor(config.color);
403
+ this.matches = new MatchRegistry(this.name);
404
+ this.restart = new RestartController(config.restart);
405
+ this.stderrSink = config.stderr ?? null;
406
+ this.finalPromise = new Promise((resolve) => {
407
+ this.finalResolve = resolve;
408
+ });
409
+ this.bindStderrSink();
410
+ this.spawnAttempt();
411
+ registerCleanupTarget(this);
412
+ liveProcesses.add(this);
413
+ }
414
+ get pid() {
415
+ return this.currentChild?.pid;
416
+ }
417
+ get stdin() {
418
+ return this.currentChild?.stdin ?? null;
419
+ }
420
+ get stdout() {
421
+ return this.currentChild?.stdout ?? null;
422
+ }
423
+ get stderr() {
424
+ return this.currentChild?.stderr ?? null;
425
+ }
426
+ get stdio() {
427
+ return this.currentChild?.stdio ?? [
428
+ null,
429
+ null,
430
+ null
431
+ ];
432
+ }
433
+ get killed() {
434
+ return this.currentChild?.killed ?? false;
435
+ }
436
+ get exitCode() {
437
+ return this.currentChild?.exitCode ?? this.lastResult?.code ?? null;
438
+ }
439
+ get signalCode() {
440
+ return this.currentChild?.signalCode ?? this.lastResult?.signal ?? null;
441
+ }
442
+ get connected() {
443
+ return this.currentChild?.connected ?? false;
444
+ }
445
+ get spawnargs() {
446
+ return this.currentChild?.spawnargs ?? [];
447
+ }
448
+ get spawnfile() {
449
+ return this.currentChild?.spawnfile ?? this.config.command;
450
+ }
451
+ get currentChild() {
452
+ return this.attempt?.child ?? null;
453
+ }
454
+ match(pattern, onMatch, options) {
455
+ return this.matches.match(pattern, onMatch, options);
456
+ }
457
+ waitFor(pattern, options) {
458
+ return this.matches.waitFor(pattern, options);
459
+ }
460
+ wait() {
461
+ this.markTerminalResultObserved();
462
+ return this.finalPromise;
463
+ }
464
+ then(onfulfilled, onrejected) {
465
+ this.markTerminalResultObserved();
466
+ return this.finalPromise.then(onfulfilled, onrejected);
467
+ }
468
+ kill(signal) {
469
+ const child = this.currentChild;
470
+ if (!child) return false;
471
+ return child.kill(signal);
472
+ }
473
+ ref() {
474
+ this.currentChild?.ref();
475
+ return this;
476
+ }
477
+ unref() {
478
+ this.currentChild?.unref();
479
+ return this;
480
+ }
481
+ disconnect() {
482
+ this.currentChild?.disconnect();
483
+ }
484
+ async stop(options) {
485
+ if (this.stopPromise) return this.stopPromise;
486
+ this.shutdownRequested = true;
487
+ this.restartDisabled = true;
488
+ this.restart.cancelDelay();
489
+ this.stopPromise = this.stopActiveTree(options?.signal ?? "SIGTERM", options?.killAfterMs ?? 5e3);
490
+ return this.stopPromise;
491
+ }
492
+ cleanupFromExit() {
493
+ this.shutdownRequested = true;
494
+ this.restartDisabled = true;
495
+ this.restart.cancelDelay();
496
+ const child = this.currentChild;
497
+ if (child) killTreeBestEffort(child);
498
+ }
499
+ async cleanupFromSignal(signal) {
500
+ this.shutdownRequested = true;
501
+ this.restartDisabled = true;
502
+ this.restart.cancelDelay();
503
+ await this.stopActiveTree(signal, 1e3);
504
+ }
505
+ async stopActiveTree(signal, killAfterMs) {
506
+ const attempt = this.attempt;
507
+ if (!attempt || attempt.closed) {
508
+ await this.finalPromise;
509
+ return;
510
+ }
511
+ await stopChildTree(attempt.child, attempt.close, () => attempt.closed, signal, killAfterMs);
512
+ }
513
+ spawnAttempt() {
514
+ this.generation += 1;
515
+ const child = spawn(this.config.command, this.config.args ?? [], {
516
+ cwd: this.config.cwd,
517
+ env: this.config.env,
518
+ stdio: [
519
+ "pipe",
520
+ "pipe",
521
+ "pipe"
522
+ ]
523
+ });
524
+ let settleClose;
525
+ const attempt = {
526
+ child,
527
+ generation: this.generation,
528
+ close: new Promise((resolve) => {
529
+ settleClose = resolve;
530
+ }),
531
+ settleClose,
532
+ stdout: {
533
+ text: "",
534
+ flushed: false
535
+ },
536
+ stderr: {
537
+ text: "",
538
+ flushed: false
539
+ },
540
+ closed: false
541
+ };
542
+ this.attempt = attempt;
543
+ this.attachAttempt(attempt);
544
+ }
545
+ attachAttempt(attempt) {
546
+ const { child } = attempt;
547
+ child.on("spawn", () => {
548
+ if (this.attempt === attempt) this.emit("spawn");
549
+ });
550
+ child.on("error", (error) => {
551
+ if (this.attempt === attempt) this.emit("error", error);
552
+ });
553
+ child.on("exit", (code, signal) => {
554
+ if (this.attempt === attempt) this.emit("exit", code, signal);
555
+ });
556
+ child.on("close", (code, signal) => {
557
+ if (this.attempt !== attempt) return;
558
+ attempt.closed = true;
559
+ this.flushLineBuffer(attempt, "stdout");
560
+ this.flushLineBuffer(attempt, "stderr");
561
+ attempt.settleClose({
562
+ code,
563
+ signal
564
+ });
565
+ this.emit("close", code, signal);
566
+ this.handleAttemptClose(code, signal);
567
+ });
568
+ child.stdout.on("data", (chunk) => {
569
+ if (this.attempt === attempt) this.handleChunk(attempt, "stdout", chunk);
570
+ });
571
+ child.stderr.on("data", (chunk) => {
572
+ if (this.attempt !== attempt) return;
573
+ this.writeStderrSink(chunk);
574
+ this.handleChunk(attempt, "stderr", chunk);
575
+ });
576
+ child.stdout.on("end", () => {
577
+ if (this.attempt === attempt) this.flushLineBuffer(attempt, "stdout");
578
+ });
579
+ child.stderr.on("end", () => {
580
+ if (this.attempt === attempt) this.flushLineBuffer(attempt, "stderr");
581
+ });
582
+ }
583
+ handleChunk(attempt, stream, chunk) {
584
+ const buffer = attempt[stream];
585
+ let text = buffer.text + chunk.toString("utf8");
586
+ let newlineIndex = text.indexOf("\n");
587
+ while (newlineIndex >= 0) {
588
+ const rawLine = text.slice(0, newlineIndex);
589
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
590
+ this.handleLine(stream, line, true);
591
+ text = text.slice(newlineIndex + 1);
592
+ newlineIndex = text.indexOf("\n");
593
+ }
594
+ buffer.text = text;
595
+ }
596
+ handleLine(stream, line, appendNewline) {
597
+ writePrefixedLine(stream === "stdout" ? process.stdout : process.stderr, this.label, stream === "stdout" ? this.color : stderrColor, line, appendNewline);
598
+ this.matches.emit(stream, line);
599
+ }
600
+ async handleAttemptClose(code, signal) {
601
+ if (this.restart.shouldRestart(code, signal, this.restartDisabled, this.finalized)) {
602
+ if (!this.restart.prepareRestart(code, signal)) {
603
+ this.finalize(this.createResult(code, signal));
604
+ return;
605
+ }
606
+ if (!await this.restart.waitForDelay() || this.finalized || this.restartDisabled) {
607
+ this.finalize(this.createResult(code, signal));
608
+ return;
609
+ }
610
+ this.spawnAttempt();
611
+ return;
612
+ }
613
+ this.finalize(this.createResult(code, signal));
614
+ }
615
+ createResult(code, signal) {
616
+ return {
617
+ name: this.name,
618
+ code,
619
+ exitCode: getResultExitCode(code, signal),
620
+ signal,
621
+ restarts: this.restart.restarts,
622
+ restartSuppressed: this.restart.restartSuppressed
623
+ };
624
+ }
625
+ finalize(result) {
626
+ if (this.finalized) return;
627
+ this.finalized = true;
628
+ this.restart.cancelDelay();
629
+ this.lastResult = result;
630
+ liveProcesses.delete(this);
631
+ if (liveProcesses.size === 0) propagatedFailure = false;
632
+ if (this.shouldPropagateFailure(result)) propagateFailure(result.exitCode);
633
+ this.matches.close(/* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`));
634
+ this.unbindStderrSink();
635
+ unregisterCleanupTarget(this);
636
+ this.finalResolve(result);
637
+ }
638
+ markTerminalResultObserved() {
639
+ this.terminalResultObserved = true;
640
+ }
641
+ shouldPropagateFailure(result) {
642
+ return result.exitCode !== 0 && !this.shutdownRequested && !this.terminalResultObserved;
643
+ }
644
+ bindStderrSink() {
645
+ const sink = this.stderrSink;
646
+ if (!sink || typeof sink.on !== "function") return;
647
+ const disable = () => {
648
+ this.stderrSinkActive = false;
649
+ };
650
+ sink.on("error", disable);
651
+ sink.on("close", disable);
652
+ this.stderrSinkCleanup = () => {
653
+ sink.off("error", disable);
654
+ sink.off("close", disable);
655
+ };
656
+ }
657
+ unbindStderrSink() {
658
+ this.stderrSinkCleanup?.();
659
+ this.stderrSinkCleanup = null;
660
+ }
661
+ writeStderrSink(chunk) {
662
+ const sink = this.stderrSink;
663
+ if (!sink || !this.stderrSinkActive) return;
664
+ try {
665
+ if ("destroyed" in sink && sink.destroyed) {
666
+ this.stderrSinkActive = false;
667
+ return;
668
+ }
669
+ sink.write(chunk);
670
+ } catch {
671
+ this.stderrSinkActive = false;
672
+ }
673
+ }
674
+ flushLineBuffer(attempt, stream) {
675
+ const buffer = attempt[stream];
676
+ if (buffer.flushed || buffer.text.length === 0) {
677
+ buffer.flushed = true;
678
+ return;
679
+ }
680
+ buffer.flushed = true;
681
+ const line = buffer.text.endsWith("\r") ? buffer.text.slice(0, -1) : buffer.text;
682
+ buffer.text = "";
683
+ this.handleLine(stream, line, false);
684
+ }
685
+ };
686
+ const liveProcesses = /* @__PURE__ */ new Set();
687
+ let propagatedFailure = false;
688
+ function validateProcessConfig(config) {
689
+ if (!config.name) throw new Error("ProcessConfig.name is required");
690
+ if (!config.command) throw new Error("ProcessConfig.command is required");
691
+ validateProcessColor(config.color);
692
+ }
693
+ function getResultExitCode(code, signal) {
694
+ if (code != null) return code;
695
+ if (signal) return 128 + (constants.signals[signal] ?? 0);
696
+ return 1;
697
+ }
698
+ function propagateFailure(exitCode) {
699
+ if (propagatedFailure) return;
700
+ propagatedFailure = true;
701
+ if (process.exitCode == null || process.exitCode === 0) process.exitCode = exitCode;
702
+ for (const proc of liveProcesses) proc.stop().catch(() => {});
703
+ }
704
+ //#endregion
705
+ export { supervise };
@@ -0,0 +1,146 @@
1
+ # Overview
2
+
3
+ `procband` supervises one subprocess per `supervise()` call.
4
+
5
+ The returned `ProcbandProcess` is both:
6
+
7
+ - a `ChildProcess`-compatible handle for the current active child attempt
8
+ - a thenable wrapper that resolves to a `ProcessResult` when supervision is done
9
+
10
+ Supervision adds four behaviors on top of raw `spawn()`:
11
+
12
+ - prefixed `stdout` and `stderr`
13
+ - line-based matching for future output
14
+ - optional restart policy with failure suppression
15
+ - tree-aware shutdown for the child and its descendants
16
+ - parent-exit propagation for unobserved terminal failures
17
+
18
+ # When to Use
19
+
20
+ - You are writing project-specific TypeScript scripts, not a CLI.
21
+ - You need to wait for a subprocess to print a "ready" line.
22
+ - You want readable prefixed logs from multiple long-lived child processes.
23
+ - You need one shutdown API that kills descendant processes too.
24
+ - You want automatic restart with a small built-in guard against tight failure
25
+ loops.
26
+
27
+ # When Not to Use
28
+
29
+ - You need a standalone process manager or service supervisor.
30
+ - You need buffered log history or replay for late subscribers.
31
+ - You need shell pipelines, shell parsing, or a command-line tool.
32
+ - You want one API that supervises many processes at once. `procband` keeps the
33
+ unit of supervision to one process per call.
34
+
35
+ # Core Abstractions
36
+
37
+ - `ProcessConfig`
38
+ Declares one supervised subprocess plus its label, color, restart policy, and
39
+ optional raw `stderr` tee.
40
+ - `ProcbandProcess`
41
+ The live wrapper returned by `supervise()`. It is a `ChildProcess`-compatible
42
+ handle, a matching surface, a shutdown surface, and a thenable final result.
43
+ - `MatchEvent`
44
+ A future matched line from `stdout` or `stderr`.
45
+ - `RestartPolicy`
46
+ Rules for restart timing and failed-exit suppression.
47
+
48
+ # Data Flow / Lifecycle
49
+
50
+ 1. `supervise(config)` spawns the first child process immediately.
51
+ 2. Child `stdout` and `stderr` are read as text and split into lines.
52
+ 3. Each line is prefixed and written to the parent `process.stdout` or
53
+ `process.stderr`.
54
+ 4. `stderr` can also be tee'd as raw bytes to `ProcessConfig.stderr`.
55
+ 5. Future matching lines are delivered through `match()` callbacks or
56
+ `waitFor()`.
57
+ 6. When a child exits, `procband` either finalizes or starts a new attempt,
58
+ depending on the restart policy.
59
+ 7. A terminal failed exit that nobody observed through `await proc` or
60
+ `proc.wait()` sets `process.exitCode` and begins
61
+ stopping any other live `procband` processes in the same parent script.
62
+ 8. `await proc` or `await proc.wait()` resolves only after the process is
63
+ terminal and no further restart will happen.
64
+
65
+ # Common Tasks -> Recommended APIs
66
+
67
+ - Wait for one readiness line:
68
+ `proc.waitFor('ready')`
69
+ - React to repeated matching output:
70
+ `proc.match(pattern, callback, options)`
71
+ - Stop the process and its descendants:
72
+ `proc.stop()`
73
+ - Inspect final exit state:
74
+ `await proc` or `await proc.wait()`
75
+ - Take ownership of a process failure:
76
+ `await proc` or `await proc.wait()`
77
+ - Let an unobserved terminal failure fail the parent script:
78
+ Do not call `wait()` or await the thenable result
79
+ - Capture raw child `stderr` in a file or custom stream:
80
+ `ProcessConfig.stderr`
81
+ - Retry failed exits with sane defaults:
82
+ `restart: true`
83
+ - Use explicit retry rules:
84
+ `restart: { when, delayMs, maxFailures, windowMs }`
85
+
86
+ # Invariants and Constraints
87
+
88
+ - Matching is line-based and future-only.
89
+ - String patterns use substring matching.
90
+ - RegExp patterns run against the full observed line.
91
+ - `match()` subscriptions do not interfere with each other.
92
+ - `waitFor()` rejects if the process becomes terminal before a future match is
93
+ observed.
94
+ - `await proc` resolves for both successful and failed exits. Inspect the
95
+ returned `ProcessResult`.
96
+ - `ProcessResult.exitCode` exposes the shell-style exit status for the final
97
+ outcome, including signal exits.
98
+ - Calling `proc.wait()` or awaiting the thenable process marks its terminal
99
+ result as observed and suppresses default parent-exit propagation.
100
+ - An unobserved terminal failure sets `process.exitCode` to the first failing
101
+ process's `ProcessResult.exitCode` and starts stopping other live `procband`
102
+ processes in the same parent script.
103
+ - The wrapper survives restarts, but inherited `pid`, `stdin`, `stdout`,
104
+ `stderr`, and related `ChildProcess` fields always refer to the current active
105
+ child attempt.
106
+ - `kill()` only signals the current direct child. `stop()` disables restart and
107
+ kills the full process tree.
108
+ - Parent cleanup installs both `SIGINT` and `SIGTERM` handlers while any live
109
+ supervised process exists.
110
+ - `stderr` prefixes always use the reserved red, even when a custom process
111
+ color is configured.
112
+
113
+ # Error Model
114
+
115
+ - `supervise()` throws synchronously for invalid config such as missing `name`,
116
+ missing `command`, or an invalid reserved color.
117
+ - `waitFor()` rejects on timeout or terminal exit before a future match.
118
+ - A thrown `match()` callback only unsubscribes that callback.
119
+ - Errors from `ProcessConfig.stderr` stop teeing to that sink but do not stop
120
+ supervision.
121
+ - Unobserved terminal failures do not reject promises. They set the parent
122
+ `process.exitCode` and start stopping sibling `procband` processes.
123
+ - `stop()` may reject if tree-kill fails with a non-`ESRCH` error.
124
+
125
+ # Terminology
126
+
127
+ - Supervised process:
128
+ A `ProcbandProcess` wrapper plus its current child attempt.
129
+ - Child attempt:
130
+ One concrete spawned process instance inside a supervision run.
131
+ - Terminal:
132
+ No child is running and no restart will be started.
133
+ - Restart suppression:
134
+ Automatic disabling of further restarts after too many failed exits inside the
135
+ configured window.
136
+ - Match:
137
+ A future observed output line that satisfies a string or regex pattern.
138
+
139
+ # Non-Goals
140
+
141
+ - A standalone CLI
142
+ - Historical log replay
143
+ - Multi-process orchestration in one top-level API
144
+ - Shell command parsing
145
+ - Full service-management features such as persistence, cron scheduling, or host
146
+ restarts
@@ -0,0 +1,32 @@
1
+ import process from 'node:process'
2
+ import { supervise } from 'procband'
3
+
4
+ const proc = supervise({
5
+ name: 'worker',
6
+ command: process.execPath,
7
+ args: [
8
+ '-e',
9
+ [
10
+ 'console.log("booting")',
11
+ 'setTimeout(() => console.log("ready"), 20)',
12
+ 'setTimeout(() => console.error("warn"), 40)',
13
+ 'setTimeout(() => process.exit(0), 60)',
14
+ ].join(';'),
15
+ ],
16
+ })
17
+
18
+ const unsubscribe = proc.match(
19
+ 'warn',
20
+ event => {
21
+ console.log(`observed ${event.stream}: ${event.line}`)
22
+ },
23
+ { stream: 'stderr' },
24
+ )
25
+
26
+ const ready = await proc.waitFor('ready')
27
+ console.log(`matched ${ready.line}`)
28
+
29
+ const result = await proc
30
+ unsubscribe()
31
+
32
+ console.log(result)
@@ -0,0 +1,42 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import process from 'node:process'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { supervise } from 'procband'
6
+
7
+ const stateDir = mkdtempSync(join(tmpdir(), 'procband-example-'))
8
+ const attemptFile = join(stateDir, 'attempt.txt')
9
+
10
+ writeFileSync(attemptFile, '0')
11
+
12
+ const script = [
13
+ 'const fs = await import("node:fs")',
14
+ 'const file = process.argv[1]',
15
+ 'let attempt = Number(fs.readFileSync(file, "utf8"))',
16
+ 'attempt += 1',
17
+ 'fs.writeFileSync(file, String(attempt))',
18
+ 'console.log(`attempt ${attempt}`)',
19
+ 'if (attempt < 3) process.exit(1)',
20
+ 'console.log("ready")',
21
+ ].join(';')
22
+
23
+ const proc = supervise({
24
+ name: 'job',
25
+ command: process.execPath,
26
+ args: ['-e', script, attemptFile],
27
+ restart: {
28
+ delayMs: 25,
29
+ maxFailures: 5,
30
+ windowMs: 1000,
31
+ },
32
+ })
33
+
34
+ proc.match(/^attempt \d+$/, event => {
35
+ console.log(`observed ${event.line}`)
36
+ })
37
+
38
+ await proc.waitFor('ready')
39
+ const result = await proc
40
+
41
+ console.log(result)
42
+ rmSync(stateDir, { recursive: true, force: true })
@@ -0,0 +1,29 @@
1
+ import process from 'node:process'
2
+ import { supervise } from 'procband'
3
+
4
+ const api = supervise({
5
+ name: 'api',
6
+ command: process.execPath,
7
+ args: [
8
+ '-e',
9
+ [
10
+ 'console.log("booting")',
11
+ 'setTimeout(() => console.log("ready"), 20)',
12
+ 'setInterval(() => {}, 1000)',
13
+ ].join(';'),
14
+ ],
15
+ })
16
+
17
+ await api.waitFor('ready')
18
+
19
+ supervise({
20
+ name: 'worker',
21
+ command: process.execPath,
22
+ args: [
23
+ '-e',
24
+ [
25
+ 'console.log("watching")',
26
+ 'setTimeout(() => process.exit(1), 60)',
27
+ ].join(';'),
28
+ ],
29
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": ["./"],
4
+ "compilerOptions": {
5
+ "types": ["node"]
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,13 +1,42 @@
1
1
  {
2
2
  "name": "procband",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [],
10
- "author": "",
3
+ "version": "0.2.0",
4
+ "description": "Supervise subprocesses from TypeScript: prefix logs, wait for output patterns, and manage lifecycle/restarts.",
11
5
  "license": "MIT",
12
- "type": "commonjs"
13
- }
6
+ "author": "Alec Larson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/aleclarson/procband.git"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "docs",
14
+ "examples"
15
+ ],
16
+ "type": "module",
17
+ "exports": {
18
+ "types": "./dist/index.d.mts",
19
+ "default": "./dist/index.mjs"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.5.2",
23
+ "oxfmt": "^0.43.0",
24
+ "oxlint": "^1.58.0",
25
+ "tsdown": "^0.21.7",
26
+ "typescript": "^6.0.2",
27
+ "vitest": "^4.1.2"
28
+ },
29
+ "dependencies": {
30
+ "@alloc/tree-kill": "^1.4.0",
31
+ "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
+ }
42
+ }
package/readme.md DELETED
@@ -1 +0,0 @@
1
- Coming soon...