pidnap 0.0.0-dev.4 → 0.0.0-dev.5

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,85 +1,94 @@
1
1
  import { parse } from "dotenv";
2
- import { existsSync, globSync, watch, readFileSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
4
- import { resolve, join, basename } from "node:path";
2
+ import { existsSync, globSync, readFileSync } from "node:fs";
3
+ import { resolve, basename, isAbsolute } from "node:path";
4
+ import { watch } from "chokidar";
5
5
 
6
- export type EnvChangeCallback = (changedKeys: string[]) => void;
7
-
8
- export interface EnvManagerConfig {
9
- /**
10
- * Directory to search for .env files
11
- * Defaults to process.cwd()
12
- */
13
- cwd?: string;
6
+ export type EnvChangeEvent =
7
+ | {
8
+ type: "global";
9
+ }
10
+ | {
11
+ type: "process";
12
+ key: string;
13
+ };
14
14
 
15
- /**
16
- * Explicit env file paths to load
17
- * Key is the identifier (e.g., "global", "app1")
18
- * Value is the file path relative to cwd or absolute
19
- */
20
- files?: Record<string, string>;
15
+ export type EnvChangeCallback = (event: EnvChangeEvent) => void;
21
16
 
22
- /**
23
- * Enable file watching for env files
24
- * Defaults to false
25
- */
26
- watch?: boolean;
17
+ export interface EnvManagerConfig {
18
+ cwd: string;
19
+ globalEnvFile?: string;
20
+ customEnvFiles?: Record<string, string>;
27
21
  }
28
22
 
29
23
  export class EnvManager {
24
+ private globalEnv: Record<string, string> = {};
25
+ private globalEnvPath: string;
30
26
  private env: Map<string, Record<string, string>> = new Map();
31
- private cwd: string;
32
- private watchEnabled: boolean;
33
27
  private watchers: Map<string, ReturnType<typeof watch>> = new Map();
34
- private fileToKeys: Map<string, Set<string>> = new Map();
28
+ private fileToKey: Map<string, string> = new Map();
35
29
  private changeCallbacks: Set<EnvChangeCallback> = new Set();
36
- private reloadDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
37
-
38
- constructor(config: EnvManagerConfig = {}) {
39
- this.cwd = config.cwd ?? process.cwd();
40
- this.watchEnabled = config.watch ?? false;
41
-
42
- // Load .env and .env.* files from cwd
30
+ private cwdWatcher: ReturnType<typeof watch> | null = null;
31
+ private customKeys: Set<string> = new Set();
32
+
33
+ constructor(private config: EnvManagerConfig) {
34
+ this.globalEnvPath = config.globalEnvFile
35
+ ? isAbsolute(config.globalEnvFile)
36
+ ? config.globalEnvFile
37
+ : resolve(config.cwd, config.globalEnvFile)
38
+ : resolve(config.cwd, ".env");
39
+
40
+ // Load custom env files first (before auto-discovery)
41
+ this.loadCustomEnvFiles();
43
42
  this.loadEnvFilesFromCwd();
44
-
45
- // Load explicitly specified files
46
- if (config.files) {
47
- for (const [key, filePath] of Object.entries(config.files)) {
48
- this.loadEnvFile(key, filePath);
49
- }
50
- }
43
+ this.watchCwdForNewFiles();
51
44
  }
52
45
 
53
- registerFile(key: string, filePath: string): void {
54
- this.loadEnvFile(key, filePath);
46
+ /**
47
+ * Register a custom env file for a key.
48
+ * Once registered, auto-discovered .env.{key} files will be ignored for this key.
49
+ */
50
+ public registerFile(key: string, filePath: string): void {
51
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);
52
+ this.customKeys.add(key);
53
+ this.loadEnvFile(key, absolutePath);
55
54
  }
56
55
 
57
- getEnvForKey(key: string): Record<string, string> {
58
- return this.env.get(key) ?? {};
56
+ /**
57
+ * Check if a key has a custom env file registered
58
+ */
59
+ public hasCustomFile(key: string): boolean {
60
+ return this.customKeys.has(key);
59
61
  }
60
62
 
61
63
  /**
62
- * Load .env and .env.* files from the cwd
64
+ * Load custom env files from config
63
65
  */
64
- private loadEnvFilesFromCwd(): void {
65
- // Load .env file as global
66
- const dotEnvPath = resolve(this.cwd, ".env");
67
- if (existsSync(dotEnvPath)) {
68
- this.loadEnvFile("global", dotEnvPath);
66
+ private loadCustomEnvFiles(): void {
67
+ if (!this.config.customEnvFiles) return;
68
+
69
+ for (const [key, filePath] of Object.entries(this.config.customEnvFiles)) {
70
+ this.registerFile(key, filePath);
69
71
  }
72
+ }
70
73
 
71
- // Load .env.* files
72
- try {
73
- const pattern = join(this.cwd, ".env.*");
74
- const envFiles = globSync(pattern);
74
+ public getEnvVars(key: string): Record<string, string> {
75
+ const specificEnv = this.env.get(key);
76
+ return {
77
+ ...this.globalEnv,
78
+ ...specificEnv,
79
+ };
80
+ }
75
81
 
82
+ private loadEnvFilesFromCwd(): void {
83
+ if (existsSync(this.globalEnvPath)) this.loadGlobalEnv(this.globalEnvPath);
84
+
85
+ try {
86
+ const envFiles = globSync(".env.*", { cwd: this.config.cwd });
76
87
  for (const filePath of envFiles) {
77
- // Extract the suffix after .env.
78
- const fileName = basename(filePath);
79
- const match = fileName.match(/^\.env\.(.+)$/);
80
- if (match) {
81
- const suffix = match[1];
82
- this.loadEnvFile(suffix, filePath);
88
+ const key = this.getEnvKeySuffix(basename(filePath));
89
+ // Skip if key has a custom file registered
90
+ if (key && !this.customKeys.has(key)) {
91
+ this.loadEnvFile(key, resolve(this.config.cwd, filePath));
83
92
  }
84
93
  }
85
94
  } catch (err) {
@@ -87,46 +96,49 @@ export class EnvManager {
87
96
  }
88
97
  }
89
98
 
90
- /**
91
- * Load a single env file and store it in the map
92
- */
93
- private loadEnvFile(key: string, filePath: string): void {
94
- const absolutePath = resolve(this.cwd, filePath);
95
-
96
- if (!existsSync(absolutePath)) {
97
- return; // Silently skip non-existent files
98
- }
99
-
99
+ private parseEnvFile(absolutePath: string): Record<string, string> | null {
100
100
  try {
101
101
  const content = readFileSync(absolutePath, "utf-8");
102
- const parsed = parse(content);
103
- this.env.set(key, parsed);
102
+ return parse(content) ?? {};
103
+ } catch (err) {
104
+ console.warn(`Failed to parse env file: ${absolutePath}`, err);
105
+ return null;
106
+ }
107
+ }
104
108
 
105
- // Track which file maps to which key
106
- if (!this.fileToKeys.has(absolutePath)) {
107
- this.fileToKeys.set(absolutePath, new Set());
108
- }
109
- this.fileToKeys.get(absolutePath)!.add(key);
109
+ private getEnvKeySuffix(fileName: string): string | null {
110
+ const match = fileName.match(/^\.env\.(.+)$/);
111
+ return match ? match[1] : null;
112
+ }
110
113
 
111
- // Start watching if enabled and not already watching
112
- if (this.watchEnabled && !this.watchers.has(absolutePath)) {
113
- this.watchFile(absolutePath);
114
- }
115
- } catch (err) {
116
- console.warn(`Failed to load env file: ${absolutePath}`, err);
114
+ private loadGlobalEnv(absolutePath: string) {
115
+ if (!existsSync(absolutePath)) return;
116
+ const parsed = this.parseEnvFile(absolutePath);
117
+ if (parsed) {
118
+ this.globalEnv = parsed;
119
+ this.watchFile(absolutePath);
117
120
  }
118
121
  }
119
122
 
120
- /**
121
- * Watch a file for changes
122
- */
123
+ private loadEnvFile(key: string, absolutePath: string) {
124
+ const parsed = this.parseEnvFile(absolutePath);
125
+ if (!parsed) return;
126
+
127
+ this.env.set(key, parsed);
128
+ this.fileToKey.set(absolutePath, key);
129
+ this.watchFile(absolutePath);
130
+ }
131
+
123
132
  private watchFile(absolutePath: string): void {
133
+ if (this.watchers.has(absolutePath)) return;
124
134
  try {
125
- const watcher = watch(absolutePath, (eventType) => {
126
- if (eventType === "change") {
135
+ const watcher = watch(absolutePath, { ignoreInitial: true })
136
+ .on("change", () => {
127
137
  this.handleFileChange(absolutePath);
128
- }
129
- });
138
+ })
139
+ .on("unlink", () => {
140
+ this.handleFileDelete(absolutePath);
141
+ });
130
142
 
131
143
  this.watchers.set(absolutePath, watcher);
132
144
  } catch (err) {
@@ -135,56 +147,80 @@ export class EnvManager {
135
147
  }
136
148
 
137
149
  /**
138
- * Handle file change with debouncing
150
+ * Watch cwd for new .env.* files
139
151
  */
152
+ private watchCwdForNewFiles(): void {
153
+ try {
154
+ this.cwdWatcher = watch(this.config.cwd, {
155
+ ignoreInitial: true,
156
+ depth: 0,
157
+ }).on("add", (filePath) => {
158
+ this.handleNewFile(filePath);
159
+ });
160
+ } catch (err) {
161
+ console.warn(`Failed to watch cwd for new env files:`, err);
162
+ }
163
+ }
164
+
140
165
  private handleFileChange(absolutePath: string): void {
141
- // Clear existing timer if any
142
- const existingTimer = this.reloadDebounceTimers.get(absolutePath);
143
- if (existingTimer) {
144
- clearTimeout(existingTimer);
166
+ if (absolutePath === this.globalEnvPath) {
167
+ this.loadGlobalEnv(absolutePath);
168
+ this.notifyCallbacks({ type: "global" });
169
+ return;
145
170
  }
146
171
 
147
- // Debounce reload by 100ms to avoid multiple rapid reloads
148
- const timer = setTimeout(() => {
149
- this.reloadFile(absolutePath);
150
- this.reloadDebounceTimers.delete(absolutePath);
151
- }, 100);
172
+ const key = this.fileToKey.get(absolutePath);
173
+ if (!key) return;
152
174
 
153
- this.reloadDebounceTimers.set(absolutePath, timer);
175
+ const parsed = this.parseEnvFile(absolutePath);
176
+ if (!parsed) return;
177
+
178
+ this.env.set(key, parsed);
179
+ this.notifyCallbacks({ type: "process", key });
154
180
  }
155
181
 
156
- /**
157
- * Reload a file and notify callbacks
158
- */
159
- private reloadFile(absolutePath: string): void {
160
- const keys = this.fileToKeys.get(absolutePath);
161
- if (!keys) return;
162
-
163
- readFile(absolutePath, "utf-8")
164
- .then((content) => parse(content))
165
- .then((parsed) => {
166
- const changedKeys: string[] = [];
167
- for (const key of keys) {
168
- this.env.set(key, parsed);
169
- changedKeys.push(key);
170
- }
182
+ private handleFileDelete(absolutePath: string): void {
183
+ const watcher = this.watchers.get(absolutePath);
184
+ if (watcher) {
185
+ watcher.close();
186
+ this.watchers.delete(absolutePath);
187
+ }
171
188
 
172
- // Notify all callbacks
173
- if (changedKeys.length > 0) {
174
- for (const callback of this.changeCallbacks) {
175
- callback(changedKeys);
176
- }
177
- }
178
- })
179
- .catch((err) => {
180
- console.warn(`Failed to reload env file: ${absolutePath}`, err);
181
- });
189
+ if (absolutePath === this.globalEnvPath) {
190
+ this.globalEnv = {};
191
+ this.notifyCallbacks({ type: "global" });
192
+ return;
193
+ }
194
+
195
+ const key = this.fileToKey.get(absolutePath);
196
+ if (key) {
197
+ this.env.delete(key);
198
+ this.fileToKey.delete(absolutePath);
199
+ this.notifyCallbacks({ type: "process", key });
200
+ }
201
+ }
202
+
203
+ private handleNewFile(filePath: string): void {
204
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);
205
+
206
+ if (absolutePath === this.globalEnvPath) {
207
+ this.loadGlobalEnv(absolutePath);
208
+ this.notifyCallbacks({ type: "global" });
209
+ return;
210
+ }
211
+
212
+ const key = this.getEnvKeySuffix(basename(filePath));
213
+ // Skip if key has a custom file registered or already loaded
214
+ if (key && !this.customKeys.has(key) && !this.env.has(key)) {
215
+ this.loadEnvFile(key, absolutePath);
216
+ this.notifyCallbacks({ type: "process", key });
217
+ }
218
+ }
219
+
220
+ private notifyCallbacks(event: EnvChangeEvent): void {
221
+ for (const callback of this.changeCallbacks) callback(event);
182
222
  }
183
223
 
184
- /**
185
- * Register a callback to be called when env files change
186
- * Returns a function to unregister the callback
187
- */
188
224
  onChange(callback: EnvChangeCallback): () => void {
189
225
  this.changeCallbacks.add(callback);
190
226
  return () => {
@@ -192,46 +228,17 @@ export class EnvManager {
192
228
  };
193
229
  }
194
230
 
195
- /**
196
- * Stop watching all files and cleanup
197
- */
198
- dispose(): void {
199
- // Clear all timers
200
- for (const timer of this.reloadDebounceTimers.values()) {
201
- clearTimeout(timer);
202
- }
203
- this.reloadDebounceTimers.clear();
204
-
205
- // Close all watchers
231
+ close(): void {
206
232
  for (const watcher of this.watchers.values()) {
207
233
  watcher.close();
208
234
  }
209
235
  this.watchers.clear();
210
236
 
211
- // Clear callbacks
212
- this.changeCallbacks.clear();
213
- }
214
-
215
- /**
216
- * Get environment variables for a specific process
217
- * Merges global env with process-specific env
218
- * Process-specific env variables override global ones
219
- */
220
- getEnvVars(processKey?: string): Record<string, string> {
221
- const globalEnv = this.env.get("global") ?? {};
222
-
223
- if (!processKey) {
224
- return { ...globalEnv };
237
+ if (this.cwdWatcher) {
238
+ this.cwdWatcher.close();
239
+ this.cwdWatcher = null;
225
240
  }
226
241
 
227
- const processEnv = this.env.get(processKey) ?? {};
228
- return { ...globalEnv, ...processEnv };
229
- }
230
-
231
- /**
232
- * Get all loaded env maps (for debugging/inspection)
233
- */
234
- getAllEnv(): ReadonlyMap<string, Record<string, string>> {
235
- return this.env;
242
+ this.changeCallbacks.clear();
236
243
  }
237
244
  }
package/src/index.ts CHANGED
@@ -10,4 +10,3 @@ export * from "./task-list.ts";
10
10
  export * from "./lazy-process.ts";
11
11
  export * from "./env-manager.ts";
12
12
  export * from "./logger.ts";
13
- export * from "./tree-kill.ts";
@@ -1,20 +1,18 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
2
- import readline from "node:readline";
3
- import { PassThrough } from "node:stream";
4
1
  import * as v from "valibot";
5
2
  import type { Logger } from "./logger.ts";
6
- import { treeKill } from "./tree-kill.ts";
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { spawn, type ChildProcess } from "node:child_process";
7
6
 
8
- export const ProcessDefinitionSchema = v.object({
7
+ export const ProcessDefinition = v.object({
9
8
  command: v.string(),
10
9
  args: v.optional(v.array(v.string())),
11
10
  cwd: v.optional(v.string()),
12
11
  env: v.optional(v.record(v.string(), v.string())),
13
12
  });
13
+ export type ProcessDefinition = v.InferOutput<typeof ProcessDefinition>;
14
14
 
15
- export type ProcessDefinition = v.InferOutput<typeof ProcessDefinitionSchema>;
16
-
17
- export const ProcessStateSchema = v.picklist([
15
+ export const ProcessState = v.picklist([
18
16
  "idle",
19
17
  "starting",
20
18
  "running",
@@ -22,35 +20,31 @@ export const ProcessStateSchema = v.picklist([
22
20
  "stopped",
23
21
  "error",
24
22
  ]);
25
-
26
- export type ProcessState = v.InferOutput<typeof ProcessStateSchema>;
23
+ export type ProcessState = v.InferOutput<typeof ProcessState>;
27
24
 
28
25
  /**
29
- * Kill a process and all its descendants (children, grandchildren, etc.)
30
- * Falls back to simple child.kill() if tree kill fails.
26
+ * Kill a process group (the process and all its descendants).
27
+ * Uses negative PID to target the entire process group.
31
28
  */
32
- async function killProcessTree(child: ChildProcess, signal: NodeJS.Signals): Promise<boolean> {
33
- const pid = child.pid;
34
- if (pid === undefined) {
35
- return false;
36
- }
37
-
29
+ function killProcessGroup(pid: number, signal: NodeJS.Signals): boolean {
38
30
  try {
39
- await treeKill(pid, signal);
31
+ // Negative PID kills the entire process group
32
+ process.kill(-pid, signal);
40
33
  return true;
41
- } catch {
42
- // Fallback to simple kill if tree kill fails
43
- try {
44
- return child.kill(signal);
45
- } catch {
46
- return false;
34
+ } catch (err) {
35
+ const code = (err as NodeJS.ErrnoException).code;
36
+ // ESRCH = No such process (already dead)
37
+ // EPERM = Permission denied (might happen if some children already exited)
38
+ if (code === "ESRCH" || code === "EPERM") {
39
+ return true;
47
40
  }
41
+ return false;
48
42
  }
49
43
  }
50
44
 
51
45
  export class LazyProcess {
52
46
  readonly name: string;
53
- private definition: ProcessDefinition;
47
+ definition: ProcessDefinition;
54
48
  private logger: Logger;
55
49
  private childProcess: ChildProcess | null = null;
56
50
  private _state: ProcessState = "idle";
@@ -60,14 +54,14 @@ export class LazyProcess {
60
54
  constructor(name: string, definition: ProcessDefinition, logger: Logger) {
61
55
  this.name = name;
62
56
  this.definition = definition;
63
- this.logger = logger;
57
+ this.logger = logger.withPrefix("SYS");
64
58
  }
65
59
 
66
60
  get state(): ProcessState {
67
61
  return this._state;
68
62
  }
69
63
 
70
- start(): void {
64
+ async start() {
71
65
  if (this._state === "running" || this._state === "starting") {
72
66
  throw new Error(`Process "${this.name}" is already ${this._state}`);
73
67
  }
@@ -78,7 +72,7 @@ export class LazyProcess {
78
72
 
79
73
  this._state = "starting";
80
74
  this.processExit = Promise.withResolvers<void>();
81
- this.logger.info(`Starting process: ${this.definition.command}`);
75
+ this.logger.debug(`Starting process: ${this.definition.command}`);
82
76
 
83
77
  try {
84
78
  const env = this.definition.env ? { ...process.env, ...this.definition.env } : process.env;
@@ -87,37 +81,23 @@ export class LazyProcess {
87
81
  cwd: this.definition.cwd,
88
82
  env,
89
83
  stdio: ["ignore", "pipe", "pipe"],
84
+ detached: true,
90
85
  });
91
86
 
92
87
  this._state = "running";
93
88
 
94
- // Combine stdout and stderr into a single stream for unified logging
95
- const combined = new PassThrough();
96
- let streamCount = 0;
97
-
98
89
  if (this.childProcess.stdout) {
99
- streamCount++;
100
- this.childProcess.stdout.pipe(combined, { end: false });
101
- this.childProcess.stdout.on("end", () => {
102
- if (--streamCount === 0) combined.end();
103
- });
90
+ const rl = createInterface({ input: this.childProcess.stdout });
91
+ rl.on("line", (line) => this.logger.withPrefix("OUT").info(line));
92
+ this.processExit.promise.then(() => rl.close());
104
93
  }
105
94
 
106
95
  if (this.childProcess.stderr) {
107
- streamCount++;
108
- this.childProcess.stderr.pipe(combined, { end: false });
109
- this.childProcess.stderr.on("end", () => {
110
- if (--streamCount === 0) combined.end();
111
- });
96
+ const rl = createInterface({ input: this.childProcess.stderr });
97
+ rl.on("line", (line) => this.logger.withPrefix("ERR").info(line));
98
+ this.processExit.promise.then(() => rl.close());
112
99
  }
113
100
 
114
- // Use readline to handle line-by-line output properly
115
- const rl = readline.createInterface({ input: combined });
116
- rl.on("line", (line) => {
117
- this.logger.info(line);
118
- });
119
-
120
- // Handle process exit
121
101
  this.childProcess.on("exit", (code, signal) => {
122
102
  this.exitCode = code;
123
103
 
@@ -137,7 +117,6 @@ export class LazyProcess {
137
117
  this.processExit.resolve();
138
118
  });
139
119
 
140
- // Handle spawn errors
141
120
  this.childProcess.on("error", (err) => {
142
121
  if (this._state !== "stopping" && this._state !== "stopped") {
143
122
  this._state = "error";
@@ -169,44 +148,34 @@ export class LazyProcess {
169
148
  }
170
149
 
171
150
  this._state = "stopping";
172
- this.logger.info(`Stopping process with SIGTERM`);
173
-
174
- // Send SIGTERM for graceful shutdown (to entire process tree)
175
- await killProcessTree(this.childProcess, "SIGTERM");
151
+ const pid = this.childProcess.pid;
176
152
 
177
- const timeoutMs = timeout ?? 5000;
153
+ if (pid === undefined) {
154
+ this._state = "stopped";
155
+ this.cleanup();
156
+ return;
157
+ }
178
158
 
179
- // Wait for process to exit or timeout
180
- const timeoutPromise = new Promise<"timeout">((resolve) =>
181
- setTimeout(() => resolve("timeout"), timeoutMs),
182
- );
159
+ this.logger.debug(`Stopping process group (pid: ${pid}) with SIGTERM`);
183
160
 
184
- this.logger.info(`Waiting for process exit (timeout: ${timeoutMs}ms)`);
185
- const result = await Promise.race([
161
+ const timeoutMs = timeout ?? 5000;
162
+ const resultRace = Promise.race([
186
163
  this.processExit.promise.then(() => "exited" as const),
187
- timeoutPromise,
164
+ setTimeout(timeoutMs, "timeout"),
188
165
  ]);
189
- this.logger.info(`Stop race result: ${result}`);
190
166
 
191
- if (result === "timeout" && this.childProcess) {
192
- const pid = this.childProcess.pid;
167
+ killProcessGroup(pid, "SIGTERM");
168
+
169
+ this.logger.debug(`Waiting for process exit (timeout: ${timeoutMs}ms)`);
170
+ const result = await resultRace;
171
+ this.logger.debug(`process exit result: ${result}`);
172
+
173
+ if (result === "timeout") {
193
174
  this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL (pid: ${pid})`);
194
- const killed = await killProcessTree(this.childProcess, "SIGKILL");
195
- this.logger.info(`SIGKILL sent via tree-kill: ${killed ? "success" : "failed"}`);
196
-
197
- // If tree-kill failed, try direct kill as last resort
198
- if (!killed && this.childProcess) {
199
- this.logger.warn(`Tree-kill failed, attempting direct SIGKILL`);
200
- try {
201
- this.childProcess.kill("SIGKILL");
202
- this.logger.info(`Direct SIGKILL sent`);
203
- } catch (err) {
204
- this.logger.error(`Direct SIGKILL failed:`, err);
205
- }
206
- }
175
+ const killed = killProcessGroup(pid, "SIGKILL");
176
+ this.logger.info(`SIGKILL sent to process group: ${killed ? "success" : "failed"}`);
207
177
 
208
- // Give SIGKILL a short timeout
209
- const killTimeout = new Promise<void>((resolve) => setTimeout(resolve, 1000));
178
+ const killTimeout = setTimeout(1000, "timeout");
210
179
  await Promise.race([this.processExit.promise, killTimeout]);
211
180
 
212
181
  if (this.childProcess && !this.childProcess.killed) {
@@ -220,9 +189,9 @@ export class LazyProcess {
220
189
  }
221
190
 
222
191
  async reset(): Promise<void> {
223
- if (this.childProcess) {
224
- // Kill the entire process tree if running
225
- await killProcessTree(this.childProcess, "SIGKILL");
192
+ if (this.childProcess?.pid !== undefined) {
193
+ // Kill the entire process group
194
+ killProcessGroup(this.childProcess.pid, "SIGKILL");
226
195
  await this.processExit.promise;
227
196
  this.cleanup();
228
197
  }
@@ -238,19 +207,13 @@ export class LazyProcess {
238
207
  }
239
208
 
240
209
  async waitForExit(): Promise<ProcessState> {
241
- if (!this.childProcess) {
242
- return this._state;
243
- }
244
-
210
+ if (!this.childProcess) return this._state;
245
211
  await this.processExit.promise;
246
212
  return this._state;
247
213
  }
248
214
 
249
215
  private cleanup(): void {
250
216
  if (this.childProcess) {
251
- // Remove all listeners to prevent memory leaks
252
- this.childProcess.stdout?.removeAllListeners();
253
- this.childProcess.stderr?.removeAllListeners();
254
217
  this.childProcess.removeAllListeners();
255
218
  this.childProcess = null;
256
219
  }