pidnap 0.0.0-dev.4 → 0.0.0-dev.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.
@@ -1,107 +1,22 @@
1
- import { basename, join, resolve } from "node:path";
1
+ import { basename, isAbsolute, resolve } from "node:path";
2
2
  import * as v from "valibot";
3
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
4
+ import { createInterface } from "node:readline/promises";
3
5
  import { spawn } from "node:child_process";
4
- import readline from "node:readline";
5
- import { PassThrough } from "node:stream";
6
6
  import { Cron } from "croner";
7
- import { appendFileSync, existsSync, globSync, readFileSync, watch } from "node:fs";
7
+ import { appendFileSync, existsSync, globSync, readFileSync } from "node:fs";
8
8
  import { parse } from "dotenv";
9
- import { readFile } from "node:fs/promises";
9
+ import { watch } from "chokidar";
10
10
  import { format } from "node:util";
11
11
 
12
- //#region src/tree-kill.ts
13
- /**
14
- * Kill a process and all its descendants (children, grandchildren, etc.)
15
- * @param pid - The process ID to kill
16
- * @param signal - The signal to send (default: SIGTERM)
17
- * @returns Promise that resolves when all processes have been signaled
18
- */
19
- async function treeKill(pid, signal = "SIGTERM") {
20
- if (Number.isNaN(pid)) throw new Error("pid must be a number");
21
- const tree = { [pid]: [] };
22
- await buildProcessTree(pid, tree, new Set([pid]));
23
- killAll(tree, signal);
24
- }
25
- /**
26
- * Build a tree of all child processes recursively
27
- */
28
- async function buildProcessTree(parentPid, tree, pidsToProcess) {
29
- return new Promise((resolve) => {
30
- const ps = spawn("ps", [
31
- "-o",
32
- "pid",
33
- "--no-headers",
34
- "--ppid",
35
- String(parentPid)
36
- ]);
37
- let allData = "";
38
- ps.stdout.on("data", (data) => {
39
- allData += data.toString("ascii");
40
- });
41
- ps.on("close", async (code) => {
42
- pidsToProcess.delete(parentPid);
43
- if (code !== 0) {
44
- if (pidsToProcess.size === 0) resolve();
45
- return;
46
- }
47
- const childPids = allData.match(/\d+/g);
48
- if (!childPids) {
49
- if (pidsToProcess.size === 0) resolve();
50
- return;
51
- }
52
- const promises = [];
53
- for (const pidStr of childPids) {
54
- const childPid = parseInt(pidStr, 10);
55
- tree[parentPid].push(childPid);
56
- tree[childPid] = [];
57
- pidsToProcess.add(childPid);
58
- promises.push(buildProcessTree(childPid, tree, pidsToProcess));
59
- }
60
- await Promise.all(promises);
61
- if (pidsToProcess.size === 0) resolve();
62
- });
63
- ps.on("error", () => {
64
- pidsToProcess.delete(parentPid);
65
- if (pidsToProcess.size === 0) resolve();
66
- });
67
- });
68
- }
69
- /**
70
- * Kill all processes in the tree
71
- * Kills children before parents to ensure clean shutdown
72
- */
73
- function killAll(tree, signal) {
74
- const killed = /* @__PURE__ */ new Set();
75
- const allPids = Object.keys(tree).map(Number);
76
- for (const pid of allPids) for (const childPid of tree[pid]) if (!killed.has(childPid)) {
77
- killPid(childPid, signal);
78
- killed.add(childPid);
79
- }
80
- for (const pid of allPids) if (!killed.has(pid)) {
81
- killPid(pid, signal);
82
- killed.add(pid);
83
- }
84
- }
85
- /**
86
- * Kill a single process, ignoring ESRCH errors (process already dead)
87
- */
88
- function killPid(pid, signal) {
89
- try {
90
- process.kill(pid, signal);
91
- } catch (err) {
92
- if (err.code !== "ESRCH") throw err;
93
- }
94
- }
95
-
96
- //#endregion
97
12
  //#region src/lazy-process.ts
98
- const ProcessDefinitionSchema = v.object({
13
+ const ProcessDefinition = v.object({
99
14
  command: v.string(),
100
15
  args: v.optional(v.array(v.string())),
101
16
  cwd: v.optional(v.string()),
102
17
  env: v.optional(v.record(v.string(), v.string()))
103
18
  });
104
- const ProcessStateSchema = v.picklist([
19
+ const ProcessState = v.picklist([
105
20
  "idle",
106
21
  "starting",
107
22
  "running",
@@ -110,21 +25,17 @@ const ProcessStateSchema = v.picklist([
110
25
  "error"
111
26
  ]);
112
27
  /**
113
- * Kill a process and all its descendants (children, grandchildren, etc.)
114
- * Falls back to simple child.kill() if tree kill fails.
28
+ * Kill a process group (the process and all its descendants).
29
+ * Uses negative PID to target the entire process group.
115
30
  */
116
- async function killProcessTree(child, signal) {
117
- const pid = child.pid;
118
- if (pid === void 0) return false;
31
+ function killProcessGroup(pid, signal) {
119
32
  try {
120
- await treeKill(pid, signal);
33
+ process.kill(-pid, signal);
121
34
  return true;
122
- } catch {
123
- try {
124
- return child.kill(signal);
125
- } catch {
126
- return false;
127
- }
35
+ } catch (err) {
36
+ const code = err.code;
37
+ if (code === "ESRCH" || code === "EPERM") return true;
38
+ return false;
128
39
  }
129
40
  }
130
41
  var LazyProcess = class {
@@ -138,17 +49,17 @@ var LazyProcess = class {
138
49
  constructor(name, definition, logger) {
139
50
  this.name = name;
140
51
  this.definition = definition;
141
- this.logger = logger;
52
+ this.logger = logger.withPrefix("SYS");
142
53
  }
143
54
  get state() {
144
55
  return this._state;
145
56
  }
146
- start() {
57
+ async start() {
147
58
  if (this._state === "running" || this._state === "starting") throw new Error(`Process "${this.name}" is already ${this._state}`);
148
59
  if (this._state === "stopping") throw new Error(`Process "${this.name}" is currently stopping`);
149
60
  this._state = "starting";
150
61
  this.processExit = Promise.withResolvers();
151
- this.logger.info(`Starting process: ${this.definition.command}`);
62
+ this.logger.debug(`Starting process: ${this.definition.command}`);
152
63
  try {
153
64
  const env = this.definition.env ? {
154
65
  ...process.env,
@@ -161,28 +72,20 @@ var LazyProcess = class {
161
72
  "ignore",
162
73
  "pipe",
163
74
  "pipe"
164
- ]
75
+ ],
76
+ detached: true
165
77
  });
166
78
  this._state = "running";
167
- const combined = new PassThrough();
168
- let streamCount = 0;
169
79
  if (this.childProcess.stdout) {
170
- streamCount++;
171
- this.childProcess.stdout.pipe(combined, { end: false });
172
- this.childProcess.stdout.on("end", () => {
173
- if (--streamCount === 0) combined.end();
174
- });
80
+ const rl = createInterface({ input: this.childProcess.stdout });
81
+ rl.on("line", (line) => this.logger.withPrefix("OUT").info(line));
82
+ this.processExit.promise.then(() => rl.close());
175
83
  }
176
84
  if (this.childProcess.stderr) {
177
- streamCount++;
178
- this.childProcess.stderr.pipe(combined, { end: false });
179
- this.childProcess.stderr.on("end", () => {
180
- if (--streamCount === 0) combined.end();
181
- });
85
+ const rl = createInterface({ input: this.childProcess.stderr });
86
+ rl.on("line", (line) => this.logger.withPrefix("ERR").info(line));
87
+ this.processExit.promise.then(() => rl.close());
182
88
  }
183
- readline.createInterface({ input: combined }).on("line", (line) => {
184
- this.logger.info(line);
185
- });
186
89
  this.childProcess.on("exit", (code, signal) => {
187
90
  this.exitCode = code;
188
91
  if (this._state === "running") if (code === 0) {
@@ -221,28 +124,24 @@ var LazyProcess = class {
221
124
  return;
222
125
  }
223
126
  this._state = "stopping";
224
- this.logger.info(`Stopping process with SIGTERM`);
225
- await killProcessTree(this.childProcess, "SIGTERM");
127
+ const pid = this.childProcess.pid;
128
+ if (pid === void 0) {
129
+ this._state = "stopped";
130
+ this.cleanup();
131
+ return;
132
+ }
133
+ this.logger.debug(`Stopping process group (pid: ${pid}) with SIGTERM`);
226
134
  const timeoutMs = timeout ?? 5e3;
227
- const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), timeoutMs));
228
- this.logger.info(`Waiting for process exit (timeout: ${timeoutMs}ms)`);
229
- const result = await Promise.race([this.processExit.promise.then(() => "exited"), timeoutPromise]);
230
- this.logger.info(`Stop race result: ${result}`);
231
- if (result === "timeout" && this.childProcess) {
232
- const pid = this.childProcess.pid;
135
+ const resultRace = Promise.race([this.processExit.promise.then(() => "exited"), setTimeout$1(timeoutMs, "timeout")]);
136
+ killProcessGroup(pid, "SIGTERM");
137
+ this.logger.debug(`Waiting for process exit (timeout: ${timeoutMs}ms)`);
138
+ const result = await resultRace;
139
+ this.logger.debug(`process exit result: ${result}`);
140
+ if (result === "timeout") {
233
141
  this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL (pid: ${pid})`);
234
- const killed = await killProcessTree(this.childProcess, "SIGKILL");
235
- this.logger.info(`SIGKILL sent via tree-kill: ${killed ? "success" : "failed"}`);
236
- if (!killed && this.childProcess) {
237
- this.logger.warn(`Tree-kill failed, attempting direct SIGKILL`);
238
- try {
239
- this.childProcess.kill("SIGKILL");
240
- this.logger.info(`Direct SIGKILL sent`);
241
- } catch (err) {
242
- this.logger.error(`Direct SIGKILL failed:`, err);
243
- }
244
- }
245
- const killTimeout = new Promise((resolve) => setTimeout(resolve, 1e3));
142
+ const killed = killProcessGroup(pid, "SIGKILL");
143
+ this.logger.info(`SIGKILL sent to process group: ${killed ? "success" : "failed"}`);
144
+ const killTimeout = setTimeout$1(1e3, "timeout");
246
145
  await Promise.race([this.processExit.promise, killTimeout]);
247
146
  if (this.childProcess && !this.childProcess.killed) this.logger.error(`Process still alive after SIGKILL (pid: ${pid})`);
248
147
  }
@@ -251,8 +150,8 @@ var LazyProcess = class {
251
150
  this.logger.info(`Process stopped`);
252
151
  }
253
152
  async reset() {
254
- if (this.childProcess) {
255
- await killProcessTree(this.childProcess, "SIGKILL");
153
+ if (this.childProcess?.pid !== void 0) {
154
+ killProcessGroup(this.childProcess.pid, "SIGKILL");
256
155
  await this.processExit.promise;
257
156
  this.cleanup();
258
157
  }
@@ -270,8 +169,6 @@ var LazyProcess = class {
270
169
  }
271
170
  cleanup() {
272
171
  if (this.childProcess) {
273
- this.childProcess.stdout?.removeAllListeners();
274
- this.childProcess.stderr?.removeAllListeners();
275
172
  this.childProcess.removeAllListeners();
276
173
  this.childProcess = null;
277
174
  }
@@ -281,14 +178,14 @@ var LazyProcess = class {
281
178
 
282
179
  //#endregion
283
180
  //#region src/restarting-process.ts
284
- const RestartPolicySchema = v.picklist([
181
+ const RestartPolicy = v.picklist([
285
182
  "always",
286
183
  "on-failure",
287
184
  "never",
288
185
  "unless-stopped",
289
186
  "on-success"
290
187
  ]);
291
- const BackoffStrategySchema = v.union([v.object({
188
+ const BackoffStrategy = v.union([v.object({
292
189
  type: v.literal("fixed"),
293
190
  delayMs: v.number()
294
191
  }), v.object({
@@ -297,19 +194,19 @@ const BackoffStrategySchema = v.union([v.object({
297
194
  maxDelayMs: v.number(),
298
195
  multiplier: v.optional(v.number())
299
196
  })]);
300
- const CrashLoopConfigSchema = v.object({
197
+ const CrashLoopConfig = v.object({
301
198
  maxRestarts: v.number(),
302
199
  windowMs: v.number(),
303
200
  backoffMs: v.number()
304
201
  });
305
- const RestartingProcessOptionsSchema = v.object({
306
- restartPolicy: RestartPolicySchema,
307
- backoff: v.optional(BackoffStrategySchema),
308
- crashLoop: v.optional(CrashLoopConfigSchema),
202
+ const RestartingProcessOptions = v.object({
203
+ restartPolicy: RestartPolicy,
204
+ backoff: v.optional(BackoffStrategy),
205
+ crashLoop: v.optional(CrashLoopConfig),
309
206
  minUptimeMs: v.optional(v.number()),
310
207
  maxTotalRestarts: v.optional(v.number())
311
208
  });
312
- const RestartingProcessStateSchema = v.picklist([
209
+ const RestartingProcessState = v.picklist([
313
210
  "idle",
314
211
  "running",
315
212
  "restarting",
@@ -330,7 +227,6 @@ const DEFAULT_CRASH_LOOP = {
330
227
  var RestartingProcess = class {
331
228
  name;
332
229
  lazyProcess;
333
- definition;
334
230
  options;
335
231
  logger;
336
232
  _state = "idle";
@@ -342,7 +238,6 @@ var RestartingProcess = class {
342
238
  pendingDelayTimeout = null;
343
239
  constructor(name, definition, options, logger) {
344
240
  this.name = name;
345
- this.definition = definition;
346
241
  this.logger = logger;
347
242
  this.options = {
348
243
  restartPolicy: options.restartPolicy,
@@ -407,7 +302,6 @@ var RestartingProcess = class {
407
302
  */
408
303
  async reload(newDefinition, restartImmediately = true) {
409
304
  this.logger.info(`Reloading process with new definition`);
410
- this.definition = newDefinition;
411
305
  this.lazyProcess.updateDefinition(newDefinition);
412
306
  if (restartImmediately) await this.restart(true);
413
307
  }
@@ -433,9 +327,9 @@ var RestartingProcess = class {
433
327
  startProcess() {
434
328
  this.lastStartTime = Date.now();
435
329
  this._state = "running";
436
- this.lazyProcess.reset().then(() => {
330
+ this.lazyProcess.reset().then(async () => {
437
331
  if (this.stopRequested) return;
438
- this.lazyProcess.start();
332
+ await this.lazyProcess.start();
439
333
  return this.lazyProcess.waitForExit();
440
334
  }).then((exitState) => {
441
335
  if (!exitState) return;
@@ -542,16 +436,16 @@ var RestartingProcess = class {
542
436
 
543
437
  //#endregion
544
438
  //#region src/cron-process.ts
545
- const RetryConfigSchema = v.object({
439
+ const RetryConfig = v.object({
546
440
  maxRetries: v.number(),
547
441
  delayMs: v.optional(v.number())
548
442
  });
549
- const CronProcessOptionsSchema = v.object({
443
+ const CronProcessOptions = v.object({
550
444
  schedule: v.string(),
551
- retry: v.optional(RetryConfigSchema),
445
+ retry: v.optional(RetryConfig),
552
446
  runOnStart: v.optional(v.boolean())
553
447
  });
554
- const CronProcessStateSchema = v.picklist([
448
+ const CronProcessState = v.picklist([
555
449
  "idle",
556
450
  "scheduled",
557
451
  "running",
@@ -657,7 +551,7 @@ var CronProcess = class {
657
551
  async runJobWithRetry() {
658
552
  if (this.stopRequested) return;
659
553
  await this.lazyProcess.reset();
660
- this.lazyProcess.start();
554
+ await this.lazyProcess.start();
661
555
  const exitState = await this.lazyProcess.waitForExit();
662
556
  if (this.stopRequested && exitState === "error") {
663
557
  this._state = "stopped";
@@ -715,7 +609,7 @@ const TaskStateSchema = v.picklist([
715
609
  ]);
716
610
  const NamedProcessDefinitionSchema = v.object({
717
611
  name: v.string(),
718
- process: ProcessDefinitionSchema
612
+ process: ProcessDefinition
719
613
  });
720
614
  var TaskList = class {
721
615
  name;
@@ -828,7 +722,7 @@ var TaskList = class {
828
722
  });
829
723
  this.runningProcesses = lazyProcesses;
830
724
  try {
831
- for (const lp of lazyProcesses) lp.start();
725
+ await Promise.all(lazyProcesses.map((lp) => lp.start()));
832
726
  const anyFailed = (await Promise.all(lazyProcesses.map((lp) => this.waitForProcess(lp)))).some((r) => r === "error");
833
727
  if (this.stopRequested) task.state = "skipped";
834
728
  else if (anyFailed) {
@@ -853,67 +747,96 @@ var TaskList = class {
853
747
  //#endregion
854
748
  //#region src/env-manager.ts
855
749
  var EnvManager = class {
750
+ globalEnv = {};
751
+ globalEnvPath;
856
752
  env = /* @__PURE__ */ new Map();
857
- cwd;
858
- watchEnabled;
859
753
  watchers = /* @__PURE__ */ new Map();
860
- fileToKeys = /* @__PURE__ */ new Map();
754
+ fileToKey = /* @__PURE__ */ new Map();
861
755
  changeCallbacks = /* @__PURE__ */ new Set();
862
- reloadDebounceTimers = /* @__PURE__ */ new Map();
863
- constructor(config = {}) {
864
- this.cwd = config.cwd ?? process.cwd();
865
- this.watchEnabled = config.watch ?? false;
756
+ cwdWatcher = null;
757
+ customKeys = /* @__PURE__ */ new Set();
758
+ constructor(config) {
759
+ this.config = config;
760
+ this.globalEnvPath = config.globalEnvFile ? isAbsolute(config.globalEnvFile) ? config.globalEnvFile : resolve(config.cwd, config.globalEnvFile) : resolve(config.cwd, ".env");
761
+ this.loadCustomEnvFiles();
866
762
  this.loadEnvFilesFromCwd();
867
- if (config.files) for (const [key, filePath] of Object.entries(config.files)) this.loadEnvFile(key, filePath);
763
+ this.watchCwdForNewFiles();
868
764
  }
765
+ /**
766
+ * Register a custom env file for a key.
767
+ * Once registered, auto-discovered .env.{key} files will be ignored for this key.
768
+ */
869
769
  registerFile(key, filePath) {
870
- this.loadEnvFile(key, filePath);
770
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);
771
+ this.customKeys.add(key);
772
+ this.loadEnvFile(key, absolutePath);
871
773
  }
872
- getEnvForKey(key) {
873
- return this.env.get(key) ?? {};
774
+ /**
775
+ * Check if a key has a custom env file registered
776
+ */
777
+ hasCustomFile(key) {
778
+ return this.customKeys.has(key);
874
779
  }
875
780
  /**
876
- * Load .env and .env.* files from the cwd
781
+ * Load custom env files from config
877
782
  */
783
+ loadCustomEnvFiles() {
784
+ if (!this.config.customEnvFiles) return;
785
+ for (const [key, filePath] of Object.entries(this.config.customEnvFiles)) this.registerFile(key, filePath);
786
+ }
787
+ getEnvVars(key) {
788
+ const specificEnv = this.env.get(key);
789
+ return {
790
+ ...this.globalEnv,
791
+ ...specificEnv
792
+ };
793
+ }
878
794
  loadEnvFilesFromCwd() {
879
- const dotEnvPath = resolve(this.cwd, ".env");
880
- if (existsSync(dotEnvPath)) this.loadEnvFile("global", dotEnvPath);
795
+ if (existsSync(this.globalEnvPath)) this.loadGlobalEnv(this.globalEnvPath);
881
796
  try {
882
- const envFiles = globSync(join(this.cwd, ".env.*"));
797
+ const envFiles = globSync(".env.*", { cwd: this.config.cwd });
883
798
  for (const filePath of envFiles) {
884
- const match = basename(filePath).match(/^\.env\.(.+)$/);
885
- if (match) {
886
- const suffix = match[1];
887
- this.loadEnvFile(suffix, filePath);
888
- }
799
+ const key = this.getEnvKeySuffix(basename(filePath));
800
+ if (key && !this.customKeys.has(key)) this.loadEnvFile(key, resolve(this.config.cwd, filePath));
889
801
  }
890
802
  } catch (err) {
891
803
  console.warn("Failed to scan env files:", err);
892
804
  }
893
805
  }
894
- /**
895
- * Load a single env file and store it in the map
896
- */
897
- loadEnvFile(key, filePath) {
898
- const absolutePath = resolve(this.cwd, filePath);
899
- if (!existsSync(absolutePath)) return;
806
+ parseEnvFile(absolutePath) {
900
807
  try {
901
- const parsed = parse(readFileSync(absolutePath, "utf-8"));
902
- this.env.set(key, parsed);
903
- if (!this.fileToKeys.has(absolutePath)) this.fileToKeys.set(absolutePath, /* @__PURE__ */ new Set());
904
- this.fileToKeys.get(absolutePath).add(key);
905
- if (this.watchEnabled && !this.watchers.has(absolutePath)) this.watchFile(absolutePath);
808
+ return parse(readFileSync(absolutePath, "utf-8")) ?? {};
906
809
  } catch (err) {
907
- console.warn(`Failed to load env file: ${absolutePath}`, err);
810
+ console.warn(`Failed to parse env file: ${absolutePath}`, err);
811
+ return null;
908
812
  }
909
813
  }
910
- /**
911
- * Watch a file for changes
912
- */
814
+ getEnvKeySuffix(fileName) {
815
+ const match = fileName.match(/^\.env\.(.+)$/);
816
+ return match ? match[1] : null;
817
+ }
818
+ loadGlobalEnv(absolutePath) {
819
+ if (!existsSync(absolutePath)) return;
820
+ const parsed = this.parseEnvFile(absolutePath);
821
+ if (parsed) {
822
+ this.globalEnv = parsed;
823
+ this.watchFile(absolutePath);
824
+ }
825
+ }
826
+ loadEnvFile(key, absolutePath) {
827
+ const parsed = this.parseEnvFile(absolutePath);
828
+ if (!parsed) return;
829
+ this.env.set(key, parsed);
830
+ this.fileToKey.set(absolutePath, key);
831
+ this.watchFile(absolutePath);
832
+ }
913
833
  watchFile(absolutePath) {
834
+ if (this.watchers.has(absolutePath)) return;
914
835
  try {
915
- const watcher = watch(absolutePath, (eventType) => {
916
- if (eventType === "change") this.handleFileChange(absolutePath);
836
+ const watcher = watch(absolutePath, { ignoreInitial: true }).on("change", () => {
837
+ this.handleFileChange(absolutePath);
838
+ }).on("unlink", () => {
839
+ this.handleFileDelete(absolutePath);
917
840
  });
918
841
  this.watchers.set(absolutePath, watcher);
919
842
  } catch (err) {
@@ -921,74 +844,91 @@ var EnvManager = class {
921
844
  }
922
845
  }
923
846
  /**
924
- * Handle file change with debouncing
847
+ * Watch cwd for new .env.* files
925
848
  */
926
- handleFileChange(absolutePath) {
927
- const existingTimer = this.reloadDebounceTimers.get(absolutePath);
928
- if (existingTimer) clearTimeout(existingTimer);
929
- const timer = setTimeout(() => {
930
- this.reloadFile(absolutePath);
931
- this.reloadDebounceTimers.delete(absolutePath);
932
- }, 100);
933
- this.reloadDebounceTimers.set(absolutePath, timer);
849
+ watchCwdForNewFiles() {
850
+ try {
851
+ this.cwdWatcher = watch(this.config.cwd, {
852
+ ignoreInitial: true,
853
+ depth: 0
854
+ }).on("add", (filePath) => {
855
+ this.handleNewFile(filePath);
856
+ });
857
+ } catch (err) {
858
+ console.warn(`Failed to watch cwd for new env files:`, err);
859
+ }
934
860
  }
935
- /**
936
- * Reload a file and notify callbacks
937
- */
938
- reloadFile(absolutePath) {
939
- const keys = this.fileToKeys.get(absolutePath);
940
- if (!keys) return;
941
- readFile(absolutePath, "utf-8").then((content) => parse(content)).then((parsed) => {
942
- const changedKeys = [];
943
- for (const key of keys) {
944
- this.env.set(key, parsed);
945
- changedKeys.push(key);
946
- }
947
- if (changedKeys.length > 0) for (const callback of this.changeCallbacks) callback(changedKeys);
948
- }).catch((err) => {
949
- console.warn(`Failed to reload env file: ${absolutePath}`, err);
861
+ handleFileChange(absolutePath) {
862
+ if (absolutePath === this.globalEnvPath) {
863
+ this.loadGlobalEnv(absolutePath);
864
+ this.notifyCallbacks({ type: "global" });
865
+ return;
866
+ }
867
+ const key = this.fileToKey.get(absolutePath);
868
+ if (!key) return;
869
+ const parsed = this.parseEnvFile(absolutePath);
870
+ if (!parsed) return;
871
+ this.env.set(key, parsed);
872
+ this.notifyCallbacks({
873
+ type: "process",
874
+ key
950
875
  });
951
876
  }
952
- /**
953
- * Register a callback to be called when env files change
954
- * Returns a function to unregister the callback
955
- */
877
+ handleFileDelete(absolutePath) {
878
+ const watcher = this.watchers.get(absolutePath);
879
+ if (watcher) {
880
+ watcher.close();
881
+ this.watchers.delete(absolutePath);
882
+ }
883
+ if (absolutePath === this.globalEnvPath) {
884
+ this.globalEnv = {};
885
+ this.notifyCallbacks({ type: "global" });
886
+ return;
887
+ }
888
+ const key = this.fileToKey.get(absolutePath);
889
+ if (key) {
890
+ this.env.delete(key);
891
+ this.fileToKey.delete(absolutePath);
892
+ this.notifyCallbacks({
893
+ type: "process",
894
+ key
895
+ });
896
+ }
897
+ }
898
+ handleNewFile(filePath) {
899
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);
900
+ if (absolutePath === this.globalEnvPath) {
901
+ this.loadGlobalEnv(absolutePath);
902
+ this.notifyCallbacks({ type: "global" });
903
+ return;
904
+ }
905
+ const key = this.getEnvKeySuffix(basename(filePath));
906
+ if (key && !this.customKeys.has(key) && !this.env.has(key)) {
907
+ this.loadEnvFile(key, absolutePath);
908
+ this.notifyCallbacks({
909
+ type: "process",
910
+ key
911
+ });
912
+ }
913
+ }
914
+ notifyCallbacks(event) {
915
+ for (const callback of this.changeCallbacks) callback(event);
916
+ }
956
917
  onChange(callback) {
957
918
  this.changeCallbacks.add(callback);
958
919
  return () => {
959
920
  this.changeCallbacks.delete(callback);
960
921
  };
961
922
  }
962
- /**
963
- * Stop watching all files and cleanup
964
- */
965
- dispose() {
966
- for (const timer of this.reloadDebounceTimers.values()) clearTimeout(timer);
967
- this.reloadDebounceTimers.clear();
923
+ close() {
968
924
  for (const watcher of this.watchers.values()) watcher.close();
969
925
  this.watchers.clear();
926
+ if (this.cwdWatcher) {
927
+ this.cwdWatcher.close();
928
+ this.cwdWatcher = null;
929
+ }
970
930
  this.changeCallbacks.clear();
971
931
  }
972
- /**
973
- * Get environment variables for a specific process
974
- * Merges global env with process-specific env
975
- * Process-specific env variables override global ones
976
- */
977
- getEnvVars(processKey) {
978
- const globalEnv = this.env.get("global") ?? {};
979
- if (!processKey) return { ...globalEnv };
980
- const processEnv = this.env.get(processKey) ?? {};
981
- return {
982
- ...globalEnv,
983
- ...processEnv
984
- };
985
- }
986
- /**
987
- * Get all loaded env maps (for debugging/inspection)
988
- */
989
- getAllEnv() {
990
- return this.env;
991
- }
992
932
  };
993
933
 
994
934
  //#endregion
@@ -1025,36 +965,37 @@ const formatPrefixWithColor = (level, name, time) => {
1025
965
  const levelTint = levelColors[level] ?? "";
1026
966
  return `${colors.gray}[${timestamp}]${colors.reset} ${levelTint}${levelFormatted}${colors.reset} (${name})`;
1027
967
  };
1028
- const writeLogFile = (logFile, line) => {
1029
- if (!logFile) return;
1030
- appendFileSync(logFile, `${line}\n`);
968
+ const formatMessagePrefix = (prefix, colored) => {
969
+ if (!prefix) return "";
970
+ if (colored) return `${colors.bold}${colors.white}[${prefix}]${colors.reset} `;
971
+ return `[${prefix}]`;
1031
972
  };
1032
973
  const logLine = (config, level, args) => {
1033
- const message = args.length > 0 ? format(...args) : "";
974
+ const message = format(...args);
1034
975
  const time = /* @__PURE__ */ new Date();
1035
- const plainLine = `${formatPrefixNoColor(level, config.name, time)} ${message}`;
1036
- writeLogFile(config.logFile, plainLine);
1037
- if (!config.stdout) return;
1038
- const coloredLine = `${formatPrefixWithColor(level, config.name, time)} ${message}`;
1039
- switch (level) {
1040
- case "error":
1041
- console.error(coloredLine);
1042
- break;
1043
- case "warn":
1044
- console.warn(coloredLine);
1045
- break;
1046
- case "info":
1047
- console.info(coloredLine);
1048
- break;
1049
- default:
1050
- console.debug(coloredLine);
1051
- break;
976
+ if (config.logFile) {
977
+ const plainLine = [
978
+ formatPrefixNoColor(level, config.name, time),
979
+ formatMessagePrefix(config.prefix, false),
980
+ message
981
+ ].filter(Boolean).join(" ");
982
+ try {
983
+ appendFileSync(config.logFile, `${plainLine}\n`);
984
+ } catch {}
985
+ }
986
+ if (config.stdout) {
987
+ const coloredLine = [
988
+ formatPrefixWithColor(level, config.name, time),
989
+ formatMessagePrefix(config.prefix, true),
990
+ message
991
+ ].filter(Boolean).join(" ");
992
+ console[level](coloredLine);
1052
993
  }
1053
994
  };
1054
- const logger = (input) => {
995
+ const logger = (_config) => {
1055
996
  const config = {
1056
997
  stdout: true,
1057
- ...input
998
+ ..._config
1058
999
  };
1059
1000
  return {
1060
1001
  info: (...args) => logLine(config, "info", args),
@@ -1065,10 +1006,14 @@ const logger = (input) => {
1065
1006
  ...config,
1066
1007
  ...overrides,
1067
1008
  name: `${config.name}:${suffix}`
1009
+ }),
1010
+ withPrefix: (prefix) => logger({
1011
+ ...config,
1012
+ prefix
1068
1013
  })
1069
1014
  };
1070
1015
  };
1071
1016
 
1072
1017
  //#endregion
1073
- 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, treeKill as y };
1074
- //# sourceMappingURL=logger-BIJzGqk3.mjs.map
1018
+ export { ProcessDefinition as _, TaskStateSchema as a, CronProcessState as c, CrashLoopConfig as d, RestartPolicy as f, LazyProcess as g, RestartingProcessState as h, TaskList as i, RetryConfig as l, RestartingProcessOptions as m, EnvManager as n, CronProcess as o, RestartingProcess as p, NamedProcessDefinitionSchema as r, CronProcessOptions as s, logger as t, BackoffStrategy as u, ProcessState as v };
1019
+ //# sourceMappingURL=logger-BU2RmetS.mjs.map