pidnap 0.0.0-dev.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.
@@ -0,0 +1,966 @@
1
+ import { basename, join, resolve } from "node:path";
2
+ import * as v from "valibot";
3
+ import { spawn } from "node:child_process";
4
+ import readline from "node:readline";
5
+ import { PassThrough } from "node:stream";
6
+ import { Cron } from "croner";
7
+ import { appendFileSync, existsSync, globSync, readFileSync, watch } from "node:fs";
8
+ import { parse } from "dotenv";
9
+ import { readFile } from "node:fs/promises";
10
+ import { format } from "node:util";
11
+
12
+ //#region src/lazy-process.ts
13
+ const ProcessDefinitionSchema = v.object({
14
+ command: v.string(),
15
+ args: v.optional(v.array(v.string())),
16
+ cwd: v.optional(v.string()),
17
+ env: v.optional(v.record(v.string(), v.string()))
18
+ });
19
+ const ProcessStateSchema = v.picklist([
20
+ "idle",
21
+ "starting",
22
+ "running",
23
+ "stopping",
24
+ "stopped",
25
+ "error"
26
+ ]);
27
+ /**
28
+ * Kill a process. Tries to kill the process group first (if available),
29
+ * then falls back to killing just the process.
30
+ */
31
+ function killProcess(child, signal) {
32
+ try {
33
+ return child.kill(signal);
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ var LazyProcess = class {
39
+ name;
40
+ definition;
41
+ logger;
42
+ childProcess = null;
43
+ _state = "idle";
44
+ processExit = Promise.withResolvers();
45
+ exitCode = null;
46
+ constructor(name, definition, logger) {
47
+ this.name = name;
48
+ this.definition = definition;
49
+ this.logger = logger;
50
+ }
51
+ get state() {
52
+ return this._state;
53
+ }
54
+ start() {
55
+ if (this._state === "running" || this._state === "starting") throw new Error(`Process "${this.name}" is already ${this._state}`);
56
+ if (this._state === "stopping") throw new Error(`Process "${this.name}" is currently stopping`);
57
+ this._state = "starting";
58
+ this.logger.info(`Starting process: ${this.definition.command}`);
59
+ try {
60
+ const env = this.definition.env ? {
61
+ ...process.env,
62
+ ...this.definition.env
63
+ } : process.env;
64
+ this.childProcess = spawn(this.definition.command, this.definition.args ?? [], {
65
+ cwd: this.definition.cwd,
66
+ env,
67
+ stdio: [
68
+ "ignore",
69
+ "pipe",
70
+ "pipe"
71
+ ]
72
+ });
73
+ this._state = "running";
74
+ const combined = new PassThrough();
75
+ let streamCount = 0;
76
+ if (this.childProcess.stdout) {
77
+ streamCount++;
78
+ this.childProcess.stdout.pipe(combined, { end: false });
79
+ this.childProcess.stdout.on("end", () => {
80
+ if (--streamCount === 0) combined.end();
81
+ });
82
+ }
83
+ if (this.childProcess.stderr) {
84
+ streamCount++;
85
+ this.childProcess.stderr.pipe(combined, { end: false });
86
+ this.childProcess.stderr.on("end", () => {
87
+ if (--streamCount === 0) combined.end();
88
+ });
89
+ }
90
+ readline.createInterface({ input: combined }).on("line", (line) => {
91
+ this.logger.info(line);
92
+ });
93
+ this.childProcess.on("exit", (code, signal) => {
94
+ this.exitCode = code;
95
+ if (this._state === "running") if (code === 0) {
96
+ this._state = "stopped";
97
+ this.logger.info(`Process exited with code ${code}`);
98
+ } else if (signal) {
99
+ this._state = "stopped";
100
+ this.logger.info(`Process killed with signal ${signal}`);
101
+ } else {
102
+ this._state = "error";
103
+ this.logger.error(`Process exited with code ${code}`);
104
+ }
105
+ this.processExit.resolve();
106
+ });
107
+ this.childProcess.on("error", (err) => {
108
+ if (this._state !== "stopping" && this._state !== "stopped") {
109
+ this._state = "error";
110
+ this.logger.error(`Process error:`, err);
111
+ }
112
+ this.processExit.resolve();
113
+ });
114
+ } catch (err) {
115
+ this._state = "error";
116
+ this.logger.error(`Failed to start process:`, err);
117
+ throw err;
118
+ }
119
+ }
120
+ async stop(timeout) {
121
+ if (this._state === "idle" || this._state === "stopped" || this._state === "error") return;
122
+ if (this._state === "stopping") {
123
+ await this.processExit.promise;
124
+ return;
125
+ }
126
+ if (!this.childProcess) {
127
+ this._state = "stopped";
128
+ return;
129
+ }
130
+ this._state = "stopping";
131
+ this.logger.info(`Stopping process with SIGTERM`);
132
+ killProcess(this.childProcess, "SIGTERM");
133
+ const timeoutMs = timeout ?? 5e3;
134
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
135
+ if (await Promise.race([this.processExit.promise.then(() => "exited"), timeoutPromise]) === "timeout" && this.childProcess) {
136
+ this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL`);
137
+ killProcess(this.childProcess, "SIGKILL");
138
+ const killTimeout = new Promise((resolve) => setTimeout(resolve, 1e3));
139
+ await Promise.race([this.processExit.promise, killTimeout]);
140
+ }
141
+ this._state = "stopped";
142
+ this.cleanup();
143
+ this.logger.info(`Process stopped`);
144
+ }
145
+ async reset() {
146
+ if (this.childProcess) {
147
+ killProcess(this.childProcess, "SIGKILL");
148
+ await this.processExit.promise;
149
+ this.cleanup();
150
+ }
151
+ this._state = "idle";
152
+ this.processExit = Promise.withResolvers();
153
+ this.logger.info(`Process reset to idle`);
154
+ }
155
+ updateDefinition(definition) {
156
+ this.definition = definition;
157
+ }
158
+ async waitForExit() {
159
+ if (!this.childProcess) return this._state;
160
+ await this.processExit.promise;
161
+ return this._state;
162
+ }
163
+ cleanup() {
164
+ if (this.childProcess) {
165
+ this.childProcess.stdout?.removeAllListeners();
166
+ this.childProcess.stderr?.removeAllListeners();
167
+ this.childProcess.removeAllListeners();
168
+ this.childProcess = null;
169
+ }
170
+ this.exitCode = null;
171
+ }
172
+ };
173
+
174
+ //#endregion
175
+ //#region src/restarting-process.ts
176
+ const RestartPolicySchema = v.picklist([
177
+ "always",
178
+ "on-failure",
179
+ "never",
180
+ "unless-stopped",
181
+ "on-success"
182
+ ]);
183
+ const BackoffStrategySchema = v.union([v.object({
184
+ type: v.literal("fixed"),
185
+ delayMs: v.number()
186
+ }), v.object({
187
+ type: v.literal("exponential"),
188
+ initialDelayMs: v.number(),
189
+ maxDelayMs: v.number(),
190
+ multiplier: v.optional(v.number())
191
+ })]);
192
+ const CrashLoopConfigSchema = v.object({
193
+ maxRestarts: v.number(),
194
+ windowMs: v.number(),
195
+ backoffMs: v.number()
196
+ });
197
+ const RestartingProcessOptionsSchema = v.object({
198
+ restartPolicy: RestartPolicySchema,
199
+ backoff: v.optional(BackoffStrategySchema),
200
+ crashLoop: v.optional(CrashLoopConfigSchema),
201
+ minUptimeMs: v.optional(v.number()),
202
+ maxTotalRestarts: v.optional(v.number())
203
+ });
204
+ const RestartingProcessStateSchema = v.picklist([
205
+ "idle",
206
+ "running",
207
+ "restarting",
208
+ "stopping",
209
+ "stopped",
210
+ "crash-loop-backoff",
211
+ "max-restarts-reached"
212
+ ]);
213
+ const DEFAULT_BACKOFF = {
214
+ type: "fixed",
215
+ delayMs: 1e3
216
+ };
217
+ const DEFAULT_CRASH_LOOP = {
218
+ maxRestarts: 5,
219
+ windowMs: 6e4,
220
+ backoffMs: 6e4
221
+ };
222
+ var RestartingProcess = class {
223
+ name;
224
+ lazyProcess;
225
+ definition;
226
+ options;
227
+ logger;
228
+ _state = "idle";
229
+ _restartCount = 0;
230
+ restartTimestamps = [];
231
+ consecutiveFailures = 0;
232
+ lastStartTime = null;
233
+ stopRequested = false;
234
+ pendingDelayTimeout = null;
235
+ constructor(name, definition, options, logger) {
236
+ this.name = name;
237
+ this.definition = definition;
238
+ this.logger = logger;
239
+ this.options = {
240
+ restartPolicy: options.restartPolicy,
241
+ backoff: options.backoff ?? DEFAULT_BACKOFF,
242
+ crashLoop: options.crashLoop ?? DEFAULT_CRASH_LOOP,
243
+ minUptimeMs: options.minUptimeMs ?? 0,
244
+ maxTotalRestarts: options.maxTotalRestarts
245
+ };
246
+ this.lazyProcess = new LazyProcess(name, definition, logger);
247
+ }
248
+ get state() {
249
+ return this._state;
250
+ }
251
+ get restarts() {
252
+ return this._restartCount;
253
+ }
254
+ start() {
255
+ if (this._state === "running" || this._state === "restarting") throw new Error(`Process "${this.name}" is already ${this._state}`);
256
+ if (this._state === "stopping") throw new Error(`Process "${this.name}" is currently stopping`);
257
+ if (this._state === "stopped" || this._state === "idle" || this._state === "max-restarts-reached") this.resetCounters();
258
+ this.stopRequested = false;
259
+ this.startProcess();
260
+ }
261
+ async stop(timeout) {
262
+ this.stopRequested = true;
263
+ if (this.pendingDelayTimeout) {
264
+ clearTimeout(this.pendingDelayTimeout);
265
+ this.pendingDelayTimeout = null;
266
+ }
267
+ if (this._state === "idle" || this._state === "stopped" || this._state === "max-restarts-reached") {
268
+ this._state = "stopped";
269
+ return;
270
+ }
271
+ this._state = "stopping";
272
+ await this.lazyProcess.stop(timeout);
273
+ this._state = "stopped";
274
+ this.logger.info(`RestartingProcess stopped`);
275
+ }
276
+ async restart(force = false) {
277
+ if (this._state === "stopped" || this._state === "idle" || this._state === "max-restarts-reached") {
278
+ this.resetCounters();
279
+ this.stopRequested = false;
280
+ this.startProcess();
281
+ return;
282
+ }
283
+ await this.stop();
284
+ this.stopRequested = false;
285
+ if (force) this.startProcess();
286
+ else {
287
+ const delay = this.calculateDelay();
288
+ if (delay > 0) {
289
+ this._state = "restarting";
290
+ this.logger.info(`Restarting in ${delay}ms`);
291
+ await this.delay(delay);
292
+ if (this.stopRequested) return;
293
+ }
294
+ this.startProcess();
295
+ }
296
+ }
297
+ /**
298
+ * Update process definition and optionally restart with new config
299
+ */
300
+ async reload(newDefinition, restartImmediately = true) {
301
+ this.logger.info(`Reloading process with new definition`);
302
+ this.definition = newDefinition;
303
+ this.lazyProcess.updateDefinition(newDefinition);
304
+ if (restartImmediately) await this.restart(true);
305
+ }
306
+ /**
307
+ * Update restart options
308
+ */
309
+ updateOptions(newOptions) {
310
+ this.logger.info(`Updating restart options`);
311
+ this.options = {
312
+ ...this.options,
313
+ restartPolicy: newOptions.restartPolicy ?? this.options.restartPolicy,
314
+ backoff: newOptions.backoff ?? this.options.backoff,
315
+ crashLoop: newOptions.crashLoop ?? this.options.crashLoop,
316
+ minUptimeMs: newOptions.minUptimeMs ?? this.options.minUptimeMs,
317
+ maxTotalRestarts: newOptions.maxTotalRestarts ?? this.options.maxTotalRestarts
318
+ };
319
+ }
320
+ resetCounters() {
321
+ this._restartCount = 0;
322
+ this.consecutiveFailures = 0;
323
+ this.restartTimestamps = [];
324
+ }
325
+ startProcess() {
326
+ this.lastStartTime = Date.now();
327
+ this._state = "running";
328
+ this.lazyProcess.reset().then(() => {
329
+ if (this.stopRequested) return;
330
+ this.lazyProcess.start();
331
+ return this.lazyProcess.waitForExit();
332
+ }).then((exitState) => {
333
+ if (!exitState) return;
334
+ if (this.stopRequested && exitState === "error") {
335
+ this._state = "stopped";
336
+ return;
337
+ }
338
+ if (exitState === "stopped" || exitState === "error") this.handleProcessExit(exitState);
339
+ }).catch((err) => {
340
+ if (this.stopRequested) return;
341
+ this._state = "stopped";
342
+ this.logger.error(`Failed to start process:`, err);
343
+ });
344
+ }
345
+ handleProcessExit(exitState) {
346
+ if (this.stopRequested) {
347
+ this._state = "stopped";
348
+ return;
349
+ }
350
+ const wasHealthy = (this.lastStartTime ? Date.now() - this.lastStartTime : 0) >= this.options.minUptimeMs;
351
+ const exitedWithError = exitState === "error";
352
+ if (wasHealthy) this.consecutiveFailures = 0;
353
+ else this.consecutiveFailures++;
354
+ if (!this.shouldRestart(exitedWithError)) {
355
+ this._state = "stopped";
356
+ this.logger.info(`Process exited, policy "${this.options.restartPolicy}" does not allow restart`);
357
+ return;
358
+ }
359
+ if (this.options.maxTotalRestarts !== void 0 && this._restartCount >= this.options.maxTotalRestarts) {
360
+ this._state = "max-restarts-reached";
361
+ this.logger.warn(`Max total restarts (${this.options.maxTotalRestarts}) reached`);
362
+ return;
363
+ }
364
+ const now = Date.now();
365
+ this.restartTimestamps.push(now);
366
+ if (this.isInCrashLoop()) {
367
+ this._state = "crash-loop-backoff";
368
+ this.logger.warn(`Crash loop detected (${this.options.crashLoop.maxRestarts} restarts in ${this.options.crashLoop.windowMs}ms), backing off for ${this.options.crashLoop.backoffMs}ms`);
369
+ this.scheduleCrashLoopRecovery();
370
+ return;
371
+ }
372
+ this._restartCount++;
373
+ this.scheduleRestart();
374
+ }
375
+ shouldRestart(exitedWithError) {
376
+ switch (this.options.restartPolicy) {
377
+ case "always": return true;
378
+ case "never": return false;
379
+ case "on-failure": return exitedWithError;
380
+ case "on-success": return !exitedWithError;
381
+ case "unless-stopped": return !this.stopRequested;
382
+ default: return false;
383
+ }
384
+ }
385
+ isInCrashLoop() {
386
+ const { maxRestarts, windowMs } = this.options.crashLoop;
387
+ const cutoff = Date.now() - windowMs;
388
+ this.restartTimestamps = this.restartTimestamps.filter((ts) => ts > cutoff);
389
+ return this.restartTimestamps.length >= maxRestarts;
390
+ }
391
+ calculateDelay() {
392
+ const { backoff } = this.options;
393
+ if (backoff.type === "fixed") return backoff.delayMs;
394
+ const multiplier = backoff.multiplier ?? 2;
395
+ const delay = backoff.initialDelayMs * Math.pow(multiplier, this.consecutiveFailures);
396
+ return Math.min(delay, backoff.maxDelayMs);
397
+ }
398
+ scheduleRestart() {
399
+ this._state = "restarting";
400
+ const delay = this.calculateDelay();
401
+ this.logger.info(`Restarting in ${delay}ms (restart #${this._restartCount})`);
402
+ this.pendingDelayTimeout = setTimeout(() => {
403
+ this.pendingDelayTimeout = null;
404
+ if (this.stopRequested) {
405
+ this._state = "stopped";
406
+ return;
407
+ }
408
+ this.startProcess();
409
+ }, delay);
410
+ }
411
+ scheduleCrashLoopRecovery() {
412
+ const { backoffMs } = this.options.crashLoop;
413
+ this.pendingDelayTimeout = setTimeout(() => {
414
+ this.pendingDelayTimeout = null;
415
+ if (this.stopRequested) {
416
+ this._state = "stopped";
417
+ return;
418
+ }
419
+ this.restartTimestamps = [];
420
+ this._restartCount++;
421
+ this.logger.info(`Crash loop backoff complete, restarting (restart #${this._restartCount})`);
422
+ this.startProcess();
423
+ }, backoffMs);
424
+ }
425
+ delay(ms) {
426
+ return new Promise((resolve) => {
427
+ this.pendingDelayTimeout = setTimeout(() => {
428
+ this.pendingDelayTimeout = null;
429
+ resolve();
430
+ }, ms);
431
+ });
432
+ }
433
+ };
434
+
435
+ //#endregion
436
+ //#region src/cron-process.ts
437
+ const RetryConfigSchema = v.object({
438
+ maxRetries: v.number(),
439
+ delayMs: v.optional(v.number())
440
+ });
441
+ const CronProcessOptionsSchema = v.object({
442
+ schedule: v.string(),
443
+ retry: v.optional(RetryConfigSchema),
444
+ runOnStart: v.optional(v.boolean())
445
+ });
446
+ const CronProcessStateSchema = v.picklist([
447
+ "idle",
448
+ "scheduled",
449
+ "running",
450
+ "retrying",
451
+ "queued",
452
+ "stopping",
453
+ "stopped"
454
+ ]);
455
+ const DEFAULT_RETRY_DELAY = 1e3;
456
+ var CronProcess = class {
457
+ name;
458
+ lazyProcess;
459
+ options;
460
+ logger;
461
+ cronJob = null;
462
+ _state = "idle";
463
+ _runCount = 0;
464
+ _failCount = 0;
465
+ currentRetryAttempt = 0;
466
+ queuedRun = false;
467
+ stopRequested = false;
468
+ retryTimeout = null;
469
+ constructor(name, definition, options, logger) {
470
+ this.name = name;
471
+ this.options = options;
472
+ this.logger = logger;
473
+ this.lazyProcess = new LazyProcess(name, definition, logger);
474
+ }
475
+ get state() {
476
+ return this._state;
477
+ }
478
+ get runCount() {
479
+ return this._runCount;
480
+ }
481
+ get failCount() {
482
+ return this._failCount;
483
+ }
484
+ get nextRun() {
485
+ if (!this.cronJob) return null;
486
+ return this.cronJob.nextRun() ?? null;
487
+ }
488
+ start() {
489
+ if (this._state === "scheduled" || this._state === "running" || this._state === "queued") throw new Error(`CronProcess "${this.name}" is already ${this._state}`);
490
+ if (this._state === "stopping") throw new Error(`CronProcess "${this.name}" is currently stopping`);
491
+ this.stopRequested = false;
492
+ this.logger.info(`Starting cron schedule: ${this.options.schedule}`);
493
+ this.cronJob = new Cron(this.options.schedule, { timezone: "UTC" }, () => {
494
+ this.onCronTick();
495
+ });
496
+ this._state = "scheduled";
497
+ if (this.options.runOnStart) this.executeJob();
498
+ }
499
+ async stop(timeout) {
500
+ this.stopRequested = true;
501
+ if (this.cronJob) {
502
+ this.cronJob.stop();
503
+ this.cronJob = null;
504
+ }
505
+ if (this.retryTimeout) {
506
+ clearTimeout(this.retryTimeout);
507
+ this.retryTimeout = null;
508
+ }
509
+ if (this._state === "idle" || this._state === "stopped") {
510
+ this._state = "stopped";
511
+ return;
512
+ }
513
+ if (this._state === "running" || this._state === "retrying" || this._state === "queued") {
514
+ this._state = "stopping";
515
+ await this.lazyProcess.stop(timeout);
516
+ }
517
+ this._state = "stopped";
518
+ this.queuedRun = false;
519
+ this.logger.info(`CronProcess stopped`);
520
+ }
521
+ async trigger() {
522
+ if (this.stopRequested) throw new Error(`CronProcess "${this.name}" is stopped`);
523
+ if (this._state === "queued") return;
524
+ if (this._state === "running" || this._state === "retrying") {
525
+ this.queuedRun = true;
526
+ this._state = "queued";
527
+ this.logger.info(`Run queued (current job still running)`);
528
+ return;
529
+ }
530
+ await this.executeJob();
531
+ }
532
+ onCronTick() {
533
+ if (this.stopRequested) return;
534
+ if (this._state === "running" || this._state === "retrying" || this._state === "queued") {
535
+ this.queuedRun = true;
536
+ if (this._state !== "queued") this._state = "queued";
537
+ this.logger.info(`Cron tick: run queued (current job still running)`);
538
+ return;
539
+ }
540
+ this.executeJob();
541
+ }
542
+ async executeJob() {
543
+ if (this.stopRequested) return;
544
+ this._state = "running";
545
+ this.currentRetryAttempt = 0;
546
+ this.logger.info(`Executing job`);
547
+ await this.runJobWithRetry();
548
+ }
549
+ async runJobWithRetry() {
550
+ if (this.stopRequested) return;
551
+ await this.lazyProcess.reset();
552
+ this.lazyProcess.start();
553
+ const exitState = await this.lazyProcess.waitForExit();
554
+ if (this.stopRequested && exitState === "error") {
555
+ this._state = "stopped";
556
+ return;
557
+ }
558
+ this.handleJobComplete(exitState === "error");
559
+ }
560
+ handleJobComplete(failed) {
561
+ if (this.stopRequested) {
562
+ this._state = "stopped";
563
+ return;
564
+ }
565
+ if (failed) {
566
+ const maxRetries = this.options.retry?.maxRetries ?? 0;
567
+ if (this.currentRetryAttempt < maxRetries) {
568
+ this.currentRetryAttempt++;
569
+ this._state = "retrying";
570
+ const delayMs = this.options.retry?.delayMs ?? DEFAULT_RETRY_DELAY;
571
+ this.logger.warn(`Job failed, retrying in ${delayMs}ms (attempt ${this.currentRetryAttempt}/${maxRetries})`);
572
+ this.retryTimeout = setTimeout(() => {
573
+ this.retryTimeout = null;
574
+ if (this.stopRequested) {
575
+ this._state = "stopped";
576
+ return;
577
+ }
578
+ this.runJobWithRetry();
579
+ }, delayMs);
580
+ return;
581
+ }
582
+ this._failCount++;
583
+ this.logger.error(`Job failed after ${this.currentRetryAttempt} retries`);
584
+ } else {
585
+ this._runCount++;
586
+ this.logger.info(`Job completed successfully`);
587
+ }
588
+ if (this.queuedRun) {
589
+ this.queuedRun = false;
590
+ this.logger.info(`Starting queued run`);
591
+ this.executeJob();
592
+ return;
593
+ }
594
+ if (this.cronJob) this._state = "scheduled";
595
+ else this._state = "stopped";
596
+ }
597
+ };
598
+
599
+ //#endregion
600
+ //#region src/task-list.ts
601
+ const TaskStateSchema = v.picklist([
602
+ "pending",
603
+ "running",
604
+ "completed",
605
+ "failed",
606
+ "skipped"
607
+ ]);
608
+ const NamedProcessDefinitionSchema = v.object({
609
+ name: v.string(),
610
+ process: ProcessDefinitionSchema
611
+ });
612
+ var TaskList = class {
613
+ name;
614
+ _tasks = [];
615
+ _state = "idle";
616
+ logger;
617
+ logFileResolver;
618
+ taskIdCounter = 0;
619
+ runningProcesses = [];
620
+ stopRequested = false;
621
+ runLoopPromise = null;
622
+ constructor(name, logger, initialTasks, logFileResolver) {
623
+ this.name = name;
624
+ this.logger = logger;
625
+ this.logFileResolver = logFileResolver;
626
+ if (initialTasks) for (const task of initialTasks) this.addTask(task);
627
+ }
628
+ get state() {
629
+ return this._state;
630
+ }
631
+ get tasks() {
632
+ return this._tasks;
633
+ }
634
+ removeTaskByTarget(target) {
635
+ const index = typeof target === "number" ? target : this._tasks.findIndex((t) => t.id === target);
636
+ if (index < 0 || index >= this._tasks.length) throw new Error(`Task not found: ${target}`);
637
+ const task = this._tasks[index];
638
+ if (task.state === "running") throw new Error(`Cannot remove running task: ${task.id}`);
639
+ this._tasks.splice(index, 1);
640
+ this.logger.info(`Task "${task.id}" removed`);
641
+ return task;
642
+ }
643
+ /**
644
+ * Add a single process or parallel processes as a new task
645
+ * @returns The unique task ID
646
+ */
647
+ addTask(task) {
648
+ const id = `task-${++this.taskIdCounter}`;
649
+ const processes = Array.isArray(task) ? task : [task];
650
+ const entry = {
651
+ id,
652
+ processes,
653
+ state: "pending"
654
+ };
655
+ this._tasks.push(entry);
656
+ this.logger.info(`Task "${id}" added with ${processes.length} process(es)`);
657
+ return id;
658
+ }
659
+ /**
660
+ * Begin executing pending tasks
661
+ */
662
+ start() {
663
+ if (this._state === "running") throw new Error(`TaskList "${this.name}" is already running`);
664
+ this.stopRequested = false;
665
+ this._state = "running";
666
+ this.logger.info(`TaskList started`);
667
+ this.runLoopPromise = this.runLoop();
668
+ }
669
+ /**
670
+ * Wait until the TaskList becomes idle (all pending tasks completed)
671
+ */
672
+ async waitUntilIdle() {
673
+ if (this._state === "idle" || this._state === "stopped") return;
674
+ if (this.runLoopPromise) await this.runLoopPromise;
675
+ }
676
+ /**
677
+ * Stop execution and mark remaining tasks as skipped
678
+ */
679
+ async stop(timeout) {
680
+ if (this._state === "idle" || this._state === "stopped") {
681
+ this._state = "stopped";
682
+ return;
683
+ }
684
+ this.stopRequested = true;
685
+ this.logger.info(`Stopping TaskList...`);
686
+ const stopPromises = this.runningProcesses.map((p) => p.stop(timeout));
687
+ await Promise.all(stopPromises);
688
+ this.runningProcesses = [];
689
+ for (const task of this._tasks) if (task.state === "pending") task.state = "skipped";
690
+ if (this.runLoopPromise) {
691
+ await this.runLoopPromise;
692
+ this.runLoopPromise = null;
693
+ }
694
+ this._state = "stopped";
695
+ this.logger.info(`TaskList stopped`);
696
+ }
697
+ async runLoop() {
698
+ while (this._state === "running" && !this.stopRequested) {
699
+ const nextTask = this._tasks.find((t) => t.state === "pending");
700
+ if (!nextTask) {
701
+ this._state = "idle";
702
+ this.logger.info(`All tasks completed, TaskList is idle`);
703
+ break;
704
+ }
705
+ await this.executeTask(nextTask);
706
+ }
707
+ }
708
+ async executeTask(task) {
709
+ if (this.stopRequested) {
710
+ task.state = "skipped";
711
+ return;
712
+ }
713
+ task.state = "running";
714
+ const taskNames = task.processes.map((p) => p.name).join(", ");
715
+ this.logger.info(`Executing task "${task.id}": [${taskNames}]`);
716
+ const lazyProcesses = task.processes.map((p) => {
717
+ const logFile = this.logFileResolver?.(p.name);
718
+ const childLogger = logFile ? this.logger.child(p.name, { logFile }) : this.logger.child(p.name);
719
+ return new LazyProcess(p.name, p.process, childLogger);
720
+ });
721
+ this.runningProcesses = lazyProcesses;
722
+ try {
723
+ for (const lp of lazyProcesses) lp.start();
724
+ const anyFailed = (await Promise.all(lazyProcesses.map((lp) => this.waitForProcess(lp)))).some((r) => r === "error");
725
+ if (this.stopRequested) task.state = "skipped";
726
+ else if (anyFailed) {
727
+ task.state = "failed";
728
+ this.logger.warn(`Task "${task.id}" failed`);
729
+ } else {
730
+ task.state = "completed";
731
+ this.logger.info(`Task "${task.id}" completed`);
732
+ }
733
+ } catch (err) {
734
+ task.state = "failed";
735
+ this.logger.error(`Task "${task.id}" error:`, err);
736
+ } finally {
737
+ this.runningProcesses = [];
738
+ }
739
+ }
740
+ async waitForProcess(lp) {
741
+ return await lp.waitForExit() === "error" ? "error" : "stopped";
742
+ }
743
+ };
744
+
745
+ //#endregion
746
+ //#region src/env-manager.ts
747
+ var EnvManager = class {
748
+ env = /* @__PURE__ */ new Map();
749
+ cwd;
750
+ watchEnabled;
751
+ watchers = /* @__PURE__ */ new Map();
752
+ fileToKeys = /* @__PURE__ */ new Map();
753
+ changeCallbacks = /* @__PURE__ */ new Set();
754
+ reloadDebounceTimers = /* @__PURE__ */ new Map();
755
+ constructor(config = {}) {
756
+ this.cwd = config.cwd ?? process.cwd();
757
+ this.watchEnabled = config.watch ?? false;
758
+ this.loadEnvFilesFromCwd();
759
+ if (config.files) for (const [key, filePath] of Object.entries(config.files)) this.loadEnvFile(key, filePath);
760
+ }
761
+ registerFile(key, filePath) {
762
+ this.loadEnvFile(key, filePath);
763
+ }
764
+ getEnvForKey(key) {
765
+ return this.env.get(key) ?? {};
766
+ }
767
+ /**
768
+ * Load .env and .env.* files from the cwd
769
+ */
770
+ loadEnvFilesFromCwd() {
771
+ const dotEnvPath = resolve(this.cwd, ".env");
772
+ if (existsSync(dotEnvPath)) this.loadEnvFile("global", dotEnvPath);
773
+ try {
774
+ const envFiles = globSync(join(this.cwd, ".env.*"));
775
+ for (const filePath of envFiles) {
776
+ const match = basename(filePath).match(/^\.env\.(.+)$/);
777
+ if (match) {
778
+ const suffix = match[1];
779
+ this.loadEnvFile(suffix, filePath);
780
+ }
781
+ }
782
+ } catch (err) {
783
+ console.warn("Failed to scan env files:", err);
784
+ }
785
+ }
786
+ /**
787
+ * Load a single env file and store it in the map
788
+ */
789
+ loadEnvFile(key, filePath) {
790
+ const absolutePath = resolve(this.cwd, filePath);
791
+ if (!existsSync(absolutePath)) return;
792
+ try {
793
+ const parsed = parse(readFileSync(absolutePath, "utf-8"));
794
+ this.env.set(key, parsed);
795
+ if (!this.fileToKeys.has(absolutePath)) this.fileToKeys.set(absolutePath, /* @__PURE__ */ new Set());
796
+ this.fileToKeys.get(absolutePath).add(key);
797
+ if (this.watchEnabled && !this.watchers.has(absolutePath)) this.watchFile(absolutePath);
798
+ } catch (err) {
799
+ console.warn(`Failed to load env file: ${absolutePath}`, err);
800
+ }
801
+ }
802
+ /**
803
+ * Watch a file for changes
804
+ */
805
+ watchFile(absolutePath) {
806
+ try {
807
+ const watcher = watch(absolutePath, (eventType) => {
808
+ if (eventType === "change") this.handleFileChange(absolutePath);
809
+ });
810
+ this.watchers.set(absolutePath, watcher);
811
+ } catch (err) {
812
+ console.warn(`Failed to watch env file: ${absolutePath}`, err);
813
+ }
814
+ }
815
+ /**
816
+ * Handle file change with debouncing
817
+ */
818
+ handleFileChange(absolutePath) {
819
+ const existingTimer = this.reloadDebounceTimers.get(absolutePath);
820
+ if (existingTimer) clearTimeout(existingTimer);
821
+ const timer = setTimeout(() => {
822
+ this.reloadFile(absolutePath);
823
+ this.reloadDebounceTimers.delete(absolutePath);
824
+ }, 100);
825
+ this.reloadDebounceTimers.set(absolutePath, timer);
826
+ }
827
+ /**
828
+ * Reload a file and notify callbacks
829
+ */
830
+ reloadFile(absolutePath) {
831
+ const keys = this.fileToKeys.get(absolutePath);
832
+ if (!keys) return;
833
+ readFile(absolutePath, "utf-8").then((content) => parse(content)).then((parsed) => {
834
+ const changedKeys = [];
835
+ for (const key of keys) {
836
+ this.env.set(key, parsed);
837
+ changedKeys.push(key);
838
+ }
839
+ if (changedKeys.length > 0) for (const callback of this.changeCallbacks) callback(changedKeys);
840
+ }).catch((err) => {
841
+ console.warn(`Failed to reload env file: ${absolutePath}`, err);
842
+ });
843
+ }
844
+ /**
845
+ * Register a callback to be called when env files change
846
+ * Returns a function to unregister the callback
847
+ */
848
+ onChange(callback) {
849
+ this.changeCallbacks.add(callback);
850
+ return () => {
851
+ this.changeCallbacks.delete(callback);
852
+ };
853
+ }
854
+ /**
855
+ * Stop watching all files and cleanup
856
+ */
857
+ dispose() {
858
+ for (const timer of this.reloadDebounceTimers.values()) clearTimeout(timer);
859
+ this.reloadDebounceTimers.clear();
860
+ for (const watcher of this.watchers.values()) watcher.close();
861
+ this.watchers.clear();
862
+ this.changeCallbacks.clear();
863
+ }
864
+ /**
865
+ * Get environment variables for a specific process
866
+ * Merges global env with process-specific env
867
+ * Process-specific env variables override global ones
868
+ */
869
+ getEnvVars(processKey) {
870
+ const globalEnv = this.env.get("global") ?? {};
871
+ if (!processKey) return { ...globalEnv };
872
+ const processEnv = this.env.get(processKey) ?? {};
873
+ return {
874
+ ...globalEnv,
875
+ ...processEnv
876
+ };
877
+ }
878
+ /**
879
+ * Get all loaded env maps (for debugging/inspection)
880
+ */
881
+ getAllEnv() {
882
+ return this.env;
883
+ }
884
+ };
885
+
886
+ //#endregion
887
+ //#region src/logger.ts
888
+ const colors = {
889
+ reset: "\x1B[0m",
890
+ gray: "\x1B[90m",
891
+ white: "\x1B[37m",
892
+ green: "\x1B[32m",
893
+ yellow: "\x1B[33m",
894
+ red: "\x1B[31m",
895
+ bold: "\x1B[1m"
896
+ };
897
+ const levelColors = {
898
+ debug: colors.gray,
899
+ info: colors.green,
900
+ warn: colors.yellow,
901
+ error: colors.red
902
+ };
903
+ const formatTime = (date) => Intl.DateTimeFormat("en-US", {
904
+ hour: "2-digit",
905
+ minute: "2-digit",
906
+ second: "2-digit",
907
+ fractionalSecondDigits: 3,
908
+ hourCycle: "h23"
909
+ }).format(date);
910
+ const formatPrefixNoColor = (level, name, time) => {
911
+ const levelFormatted = level.toUpperCase().padStart(5);
912
+ return `[${formatTime(time)}] ${levelFormatted} (${name})`;
913
+ };
914
+ const formatPrefixWithColor = (level, name, time) => {
915
+ const levelFormatted = level.toUpperCase().padStart(5);
916
+ const timestamp = formatTime(time);
917
+ const levelTint = levelColors[level] ?? "";
918
+ return `${colors.gray}[${timestamp}]${colors.reset} ${levelTint}${levelFormatted}${colors.reset} (${name})`;
919
+ };
920
+ const writeLogFile = (logFile, line) => {
921
+ if (!logFile) return;
922
+ appendFileSync(logFile, `${line}\n`);
923
+ };
924
+ const logLine = (config, level, args) => {
925
+ const message = args.length > 0 ? format(...args) : "";
926
+ const time = /* @__PURE__ */ new Date();
927
+ const plainLine = `${formatPrefixNoColor(level, config.name, time)} ${message}`;
928
+ writeLogFile(config.logFile, plainLine);
929
+ if (!config.stdout) return;
930
+ const coloredLine = `${formatPrefixWithColor(level, config.name, time)} ${message}`;
931
+ switch (level) {
932
+ case "error":
933
+ console.error(coloredLine);
934
+ break;
935
+ case "warn":
936
+ console.warn(coloredLine);
937
+ break;
938
+ case "info":
939
+ console.info(coloredLine);
940
+ break;
941
+ default:
942
+ console.debug(coloredLine);
943
+ break;
944
+ }
945
+ };
946
+ const logger = (input) => {
947
+ const config = {
948
+ stdout: true,
949
+ ...input
950
+ };
951
+ return {
952
+ info: (...args) => logLine(config, "info", args),
953
+ error: (...args) => logLine(config, "error", args),
954
+ warn: (...args) => logLine(config, "warn", args),
955
+ debug: (...args) => logLine(config, "debug", args),
956
+ child: (suffix, overrides = {}) => logger({
957
+ ...config,
958
+ ...overrides,
959
+ name: `${config.name}:${suffix}`
960
+ })
961
+ };
962
+ };
963
+
964
+ //#endregion
965
+ export { ProcessDefinitionSchema as _, TaskStateSchema as a, CronProcessStateSchema as c, CrashLoopConfigSchema as d, RestartPolicySchema as f, LazyProcess as g, RestartingProcessStateSchema as h, TaskList as i, RetryConfigSchema as l, RestartingProcessOptionsSchema as m, EnvManager as n, CronProcess as o, RestartingProcess as p, NamedProcessDefinitionSchema as r, CronProcessOptionsSchema as s, logger as t, BackoffStrategySchema as u, ProcessStateSchema as v };
966
+ //# sourceMappingURL=logger-crc5neL8.mjs.map