@aliou/pi-processes 0.9.5 → 0.10.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.
Files changed (173) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +34 -9
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +499 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -119
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -155
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output-truncate.ts +0 -187
  166. package/src/tools/actions/output.test.ts +0 -392
  167. package/src/tools/actions/output.ts +0 -447
  168. package/src/tools/actions/start.ts +0 -245
  169. package/src/tools/actions/write.ts +0 -113
  170. package/src/tools/index.ts +0 -207
  171. package/src/utils/ansi.test.ts +0 -22
  172. package/src/utils/command-executor.test.ts +0 -48
  173. package/src/utils/keybindings.ts +0 -71
package/src/manager.ts DELETED
@@ -1,758 +0,0 @@
1
- import type { ChildProcess } from "node:child_process";
2
- import { EventEmitter } from "node:events";
3
- import {
4
- appendFileSync,
5
- mkdirSync,
6
- readFileSync,
7
- rmSync,
8
- statSync,
9
- } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import type { Writable } from "node:stream";
13
-
14
- import {
15
- type KillResult,
16
- LIVE_STATUSES,
17
- type LogWatch,
18
- type LogWatchStream,
19
- type ManagerEvent,
20
- type ProcessInfo,
21
- type ProcessStatus,
22
- type StartOptions,
23
- type WriteResult,
24
- } from "./constants";
25
- import { isProcessGroupAlive, killProcessGroup } from "./utils";
26
- import { spawnCommand } from "./utils/command-executor";
27
-
28
- interface ResolvedWatch {
29
- index: number;
30
- pattern: string;
31
- regex: RegExp;
32
- stream: LogWatchStream;
33
- repeat: boolean;
34
- fired: boolean;
35
- }
36
-
37
- interface ManagedProcess extends ProcessInfo {
38
- process: ChildProcess;
39
- stdin: Writable | null;
40
- stdinClosed: boolean;
41
- lastSignalSent: NodeJS.Signals | null;
42
- combinedFile: string;
43
- stdoutPendingLine: string;
44
- stderrPendingLine: string;
45
- watches: ResolvedWatch[];
46
- }
47
-
48
- interface ProcessManagerOptions {
49
- getConfiguredShellPath?: () => string | undefined;
50
- }
51
-
52
- export class ProcessManager {
53
- private processes: Map<string, ManagedProcess> = new Map();
54
- private counter = 0;
55
- private logDir: string;
56
- private events = new EventEmitter();
57
- private watcher: ReturnType<typeof setInterval> | null = null;
58
- private getConfiguredShellPath: () => string | undefined;
59
-
60
- private lastOutputEmitAt: Map<string, number> = new Map();
61
- private pendingOutputEmit: Map<string, NodeJS.Timeout> = new Map();
62
-
63
- constructor(options?: ProcessManagerOptions) {
64
- this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
65
- mkdirSync(this.logDir, { recursive: true });
66
- this.getConfiguredShellPath =
67
- options?.getConfiguredShellPath ?? (() => undefined);
68
- }
69
-
70
- onEvent(listener: (event: ManagerEvent) => void): () => void {
71
- this.events.on("event", listener);
72
- return () => this.events.off("event", listener);
73
- }
74
-
75
- private emit(event: ManagerEvent): void {
76
- this.events.emit("event", event);
77
- }
78
-
79
- private notifyOutputChanged(id: string): void {
80
- const now = Date.now();
81
- const lastEmit = this.lastOutputEmitAt.get(id) ?? 0;
82
- const elapsed = now - lastEmit;
83
-
84
- if (elapsed >= 100) {
85
- this.lastOutputEmitAt.set(id, now);
86
- this.emit({ type: "process_output_changed", id });
87
- return;
88
- }
89
-
90
- if (!this.pendingOutputEmit.has(id)) {
91
- const delay = 100 - elapsed;
92
- const timeout = setTimeout(() => {
93
- this.pendingOutputEmit.delete(id);
94
- // Invariant: every path that removes a process from `this.processes`
95
- // must call `clearOutputChangedState(id)` first, which clears this
96
- // timeout. This guard is a safety net, not a primary mechanism.
97
- if (!this.processes.has(id)) return;
98
- this.lastOutputEmitAt.set(id, Date.now());
99
- this.emit({ type: "process_output_changed", id });
100
- }, delay);
101
- this.pendingOutputEmit.set(id, timeout);
102
- }
103
- }
104
-
105
- private flushPendingOutputChanged(id: string): void {
106
- const timeout = this.pendingOutputEmit.get(id);
107
- if (!timeout) return;
108
- clearTimeout(timeout);
109
- this.pendingOutputEmit.delete(id);
110
- this.lastOutputEmitAt.set(id, Date.now());
111
- this.emit({ type: "process_output_changed", id });
112
- }
113
-
114
- private clearOutputChangedState(id: string): void {
115
- const timeout = this.pendingOutputEmit.get(id);
116
- if (timeout) clearTimeout(timeout);
117
- this.pendingOutputEmit.delete(id);
118
- this.lastOutputEmitAt.delete(id);
119
- }
120
-
121
- private transition(managed: ManagedProcess, next: ProcessStatus): void {
122
- if (managed.status === next) return;
123
- managed.status = next;
124
-
125
- if (next === "exited" || next === "killed") {
126
- this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
127
- }
128
-
129
- this.ensureWatcherRunning();
130
- this.stopWatcherIfIdle();
131
- }
132
-
133
- private ensureWatcherRunning(): void {
134
- if (this.watcher) return;
135
- if (!this.hasAliveishProcesses()) return;
136
-
137
- this.watcher = setInterval(() => {
138
- this.livenessTick();
139
- }, 5000);
140
- }
141
-
142
- private stopWatcherIfIdle(): void {
143
- if (!this.watcher) return;
144
- if (this.hasAliveishProcesses()) return;
145
-
146
- clearInterval(this.watcher);
147
- this.watcher = null;
148
- }
149
-
150
- private hasAliveishProcesses(): boolean {
151
- for (const p of this.processes.values()) {
152
- if (LIVE_STATUSES.has(p.status)) return true;
153
- }
154
- return false;
155
- }
156
-
157
- private livenessTick(): void {
158
- for (const managed of this.processes.values()) {
159
- if (!LIVE_STATUSES.has(managed.status)) continue;
160
- if (!managed.pid || managed.pid <= 0) continue;
161
-
162
- const alive = isProcessGroupAlive(managed.pid);
163
- if (alive) continue;
164
-
165
- if (!managed.endTime) {
166
- managed.endTime = Date.now();
167
- }
168
-
169
- this.flushPendingOutputChanged(managed.id);
170
- this.flushPendingLines(managed);
171
-
172
- if (managed.lastSignalSent) {
173
- managed.success = false;
174
- managed.exitCode = null;
175
- this.transition(managed, "killed");
176
- } else {
177
- managed.success = false;
178
- managed.exitCode = null;
179
- this.transition(managed, "exited");
180
- }
181
- }
182
- }
183
-
184
- start(
185
- name: string,
186
- command: string,
187
- cwd: string,
188
- options?: StartOptions,
189
- ): ProcessInfo {
190
- const resolvedWatches = this.resolveLogWatches(options?.logWatches);
191
- const id = `proc_${++this.counter}`;
192
- const stdoutFile = join(this.logDir, `${id}-stdout.log`);
193
- const stderrFile = join(this.logDir, `${id}-stderr.log`);
194
- const combinedFile = join(this.logDir, `${id}-combined.log`);
195
-
196
- appendFileSync(stdoutFile, "");
197
- appendFileSync(stderrFile, "");
198
- appendFileSync(combinedFile, "");
199
-
200
- const child = spawnCommand(command, cwd, this.getConfiguredShellPath());
201
-
202
- child.unref();
203
-
204
- const managed: ManagedProcess = {
205
- id,
206
- name,
207
- pid: child.pid ?? -1,
208
- command,
209
- cwd,
210
- startTime: Date.now(),
211
- endTime: null,
212
- status: "running",
213
- exitCode: null,
214
- success: null,
215
- stdoutFile,
216
- stderrFile,
217
- combinedFile,
218
- alertOnSuccess: options?.alertOnSuccess ?? false,
219
- alertOnFailure: options?.alertOnFailure ?? true,
220
- alertOnKill: options?.alertOnKill ?? false,
221
- process: child,
222
- stdin: child.stdin,
223
- stdinClosed: false,
224
- lastSignalSent: null,
225
- stdoutPendingLine: "",
226
- stderrPendingLine: "",
227
- watches: resolvedWatches,
228
- };
229
-
230
- this.processes.set(id, managed);
231
-
232
- if (!child.pid) {
233
- try {
234
- appendFileSync(stderrFile, "Spawn error: missing pid\n");
235
- } catch {
236
- // Ignore
237
- }
238
- managed.exitCode = -1;
239
- managed.success = false;
240
- managed.endTime = Date.now();
241
- this.transition(managed, "exited");
242
- return this.toProcessInfo(managed);
243
- }
244
-
245
- child.stdout?.on("data", (data: Buffer) => {
246
- try {
247
- appendFileSync(stdoutFile, data);
248
- const lines = this.extractCompleteLines(managed, "stdout", data);
249
- const tagged = lines.map((line) => `1:${line}\n`).join("");
250
- if (tagged) appendFileSync(combinedFile, tagged);
251
- this.matchWatches(managed, "stdout", lines);
252
- this.notifyOutputChanged(id);
253
- } catch {
254
- // Ignore
255
- }
256
- });
257
-
258
- child.stderr?.on("data", (data: Buffer) => {
259
- try {
260
- appendFileSync(stderrFile, data);
261
- const lines = this.extractCompleteLines(managed, "stderr", data);
262
- const tagged = lines.map((line) => `2:${line}\n`).join("");
263
- if (tagged) appendFileSync(combinedFile, tagged);
264
- this.matchWatches(managed, "stderr", lines);
265
- this.notifyOutputChanged(id);
266
- } catch {
267
- // Ignore
268
- }
269
- });
270
-
271
- child.on("close", (code, signal) => {
272
- if (managed.endTime) return;
273
-
274
- managed.exitCode = code;
275
- managed.endTime = Date.now();
276
- managed.success = code === 0;
277
-
278
- this.flushPendingOutputChanged(id);
279
- this.flushPendingLines(managed);
280
-
281
- if (signal) {
282
- this.transition(managed, "killed");
283
- } else {
284
- this.transition(managed, "exited");
285
- }
286
- });
287
-
288
- child.on("error", (err) => {
289
- try {
290
- appendFileSync(stderrFile, `Process error: ${err.message}\n`);
291
- } catch {
292
- // Ignore
293
- }
294
-
295
- if (!managed.endTime) {
296
- managed.exitCode = -1;
297
- managed.success = false;
298
- managed.endTime = Date.now();
299
- this.flushPendingOutputChanged(id);
300
- this.flushPendingLines(managed);
301
- this.transition(managed, "exited");
302
- }
303
- });
304
-
305
- this.emit({ type: "process_started", info: this.toProcessInfo(managed) });
306
- this.ensureWatcherRunning();
307
-
308
- return this.toProcessInfo(managed);
309
- }
310
-
311
- list(): ProcessInfo[] {
312
- return Array.from(this.processes.values())
313
- .map((p) => this.toProcessInfo(p))
314
- .reverse();
315
- }
316
-
317
- get(id: string): ProcessInfo | null {
318
- const managed = this.processes.get(id);
319
- return managed ? this.toProcessInfo(managed) : null;
320
- }
321
-
322
- getOutput(
323
- id: string,
324
- tailLines = 100,
325
- ): { stdout: string[]; stderr: string[]; status: string } | null {
326
- const managed = this.processes.get(id);
327
- if (!managed) return null;
328
-
329
- return {
330
- stdout: this.readTailLines(managed.stdoutFile, tailLines),
331
- stderr: this.readTailLines(managed.stderrFile, tailLines),
332
- status: managed.status,
333
- };
334
- }
335
-
336
- getCombinedOutput(
337
- id: string,
338
- tailLines = 100,
339
- ): { type: "stdout" | "stderr"; text: string }[] | null {
340
- const managed = this.processes.get(id);
341
- if (!managed) return null;
342
-
343
- const rawLines = this.readTailLines(managed.combinedFile, tailLines);
344
- return rawLines.map((line) => {
345
- if (line.startsWith("2:")) {
346
- return { type: "stderr", text: line.slice(2) };
347
- }
348
- // Default to stdout (handles "1:" prefix and any malformed lines).
349
- return {
350
- type: "stdout",
351
- text: line.startsWith("1:") ? line.slice(2) : line,
352
- };
353
- });
354
- }
355
-
356
- getFullOutput(id: string): { stdout: string; stderr: string } | null {
357
- const managed = this.processes.get(id);
358
- if (!managed) return null;
359
-
360
- try {
361
- return {
362
- stdout: readFileSync(managed.stdoutFile, "utf-8"),
363
- stderr: readFileSync(managed.stderrFile, "utf-8"),
364
- };
365
- } catch {
366
- return { stdout: "", stderr: "" };
367
- }
368
- }
369
-
370
- getLogFiles(
371
- id: string,
372
- ): { stdoutFile: string; stderrFile: string; combinedFile: string } | null {
373
- const managed = this.processes.get(id);
374
- if (!managed) return null;
375
- return {
376
- stdoutFile: managed.stdoutFile,
377
- stderrFile: managed.stderrFile,
378
- combinedFile: managed.combinedFile,
379
- };
380
- }
381
-
382
- async kill(
383
- id: string,
384
- opts?: { signal?: NodeJS.Signals; timeoutMs?: number },
385
- ): Promise<KillResult> {
386
- const managed = this.processes.get(id);
387
- if (!managed) {
388
- return {
389
- ok: false,
390
- info: {
391
- id,
392
- name: "(unknown)",
393
- pid: -1,
394
- command: "",
395
- cwd: "",
396
- startTime: 0,
397
- endTime: null,
398
- status: "exited",
399
- exitCode: null,
400
- success: false,
401
- stdoutFile: "",
402
- stderrFile: "",
403
- alertOnSuccess: false,
404
- alertOnFailure: true,
405
- alertOnKill: false,
406
- },
407
- reason: "not_found",
408
- };
409
- }
410
-
411
- const signal = opts?.signal ?? "SIGTERM";
412
- const timeoutMs = opts?.timeoutMs ?? 3000;
413
-
414
- managed.alertOnKill = false;
415
-
416
- if (!LIVE_STATUSES.has(managed.status)) {
417
- return { ok: true, info: this.toProcessInfo(managed) };
418
- }
419
-
420
- this.transition(managed, "terminating");
421
-
422
- try {
423
- killProcessGroup(managed.pid, signal);
424
- managed.lastSignalSent = signal;
425
- } catch (error) {
426
- const err = error as NodeJS.ErrnoException;
427
- if (err.code !== "EPERM") {
428
- return {
429
- ok: false,
430
- info: this.toProcessInfo(managed),
431
- reason: "error",
432
- };
433
- }
434
- }
435
-
436
- const graceMs = signal === "SIGKILL" ? 200 : timeoutMs;
437
-
438
- await new Promise((r) => setTimeout(r, graceMs));
439
-
440
- const alive = isProcessGroupAlive(managed.pid);
441
-
442
- if (alive) {
443
- this.transition(managed, "terminate_timeout");
444
- return {
445
- ok: false,
446
- info: this.toProcessInfo(managed),
447
- reason: "timeout",
448
- };
449
- }
450
-
451
- if (!managed.endTime) {
452
- managed.endTime = Date.now();
453
- managed.exitCode = null;
454
- managed.success = false;
455
- }
456
-
457
- this.flushPendingOutputChanged(id);
458
- this.flushPendingLines(managed);
459
- this.transition(managed, "killed");
460
- return { ok: true, info: this.toProcessInfo(managed) };
461
- }
462
-
463
- writeToStdin(
464
- id: string,
465
- data: string,
466
- opts?: { end?: boolean },
467
- ): WriteResult {
468
- const managed = this.processes.get(id);
469
- if (!managed) {
470
- return {
471
- ok: false,
472
- reason: "not_found",
473
- };
474
- }
475
-
476
- if (!LIVE_STATUSES.has(managed.status)) {
477
- return {
478
- ok: false,
479
- reason: "process_exited",
480
- };
481
- }
482
-
483
- if (managed.stdinClosed || !managed.stdin) {
484
- return {
485
- ok: false,
486
- reason: "stdin_closed",
487
- };
488
- }
489
-
490
- try {
491
- managed.stdin.write(data);
492
-
493
- if (opts?.end) {
494
- managed.stdin.end();
495
- managed.stdinClosed = true;
496
- }
497
-
498
- return { ok: true };
499
- } catch {
500
- return {
501
- ok: false,
502
- reason: "write_error",
503
- };
504
- }
505
- }
506
-
507
- clearFinished(): number {
508
- let cleared = 0;
509
- for (const [id, managed] of this.processes) {
510
- if (LIVE_STATUSES.has(managed.status)) {
511
- continue;
512
- }
513
-
514
- try {
515
- rmSync(managed.stdoutFile, { force: true });
516
- rmSync(managed.stderrFile, { force: true });
517
- rmSync(managed.combinedFile, { force: true });
518
- } catch {
519
- // Ignore
520
- }
521
-
522
- this.clearOutputChangedState(id);
523
- this.processes.delete(id);
524
- cleared++;
525
- }
526
-
527
- if (cleared > 0) {
528
- this.emit({ type: "processes_changed" });
529
- }
530
-
531
- this.stopWatcherIfIdle();
532
- return cleared;
533
- }
534
-
535
- shutdownKillAll(): void {
536
- for (const p of this.processes.values()) {
537
- if (!LIVE_STATUSES.has(p.status)) continue;
538
- try {
539
- killProcessGroup(p.pid, "SIGKILL");
540
- } catch {
541
- // Ignore - process may already be dead
542
- }
543
- }
544
- }
545
-
546
- stopWatcher(): void {
547
- if (this.watcher) {
548
- clearInterval(this.watcher);
549
- this.watcher = null;
550
- }
551
- }
552
-
553
- cleanup(): void {
554
- this.stopWatcher();
555
-
556
- for (const timeout of this.pendingOutputEmit.values()) {
557
- clearTimeout(timeout);
558
- }
559
- this.pendingOutputEmit.clear();
560
- this.lastOutputEmitAt.clear();
561
-
562
- for (const p of this.processes.values()) {
563
- if (!LIVE_STATUSES.has(p.status)) continue;
564
- try {
565
- killProcessGroup(p.pid, "SIGKILL");
566
- } catch {
567
- // Ignore
568
- }
569
- }
570
-
571
- try {
572
- rmSync(this.logDir, { recursive: true, force: true });
573
- } catch {
574
- // Ignore
575
- }
576
- }
577
-
578
- getFileSize(id: string): { stdout: number; stderr: number } | null {
579
- const managed = this.processes.get(id);
580
- if (!managed) return null;
581
-
582
- try {
583
- return {
584
- stdout: statSync(managed.stdoutFile).size,
585
- stderr: statSync(managed.stderrFile).size,
586
- };
587
- } catch {
588
- return { stdout: 0, stderr: 0 };
589
- }
590
- }
591
-
592
- private resolveLogWatches(input?: LogWatch[]): ResolvedWatch[] {
593
- if (!input || input.length === 0) return [];
594
-
595
- return input.map((watch, index) => {
596
- const pattern = watch.pattern?.trim();
597
- if (!pattern) {
598
- throw new Error(`logWatches[${index}].pattern is required`);
599
- }
600
-
601
- let regex: RegExp;
602
- try {
603
- regex = new RegExp(pattern);
604
- } catch (error) {
605
- const message =
606
- error instanceof Error ? error.message : "invalid regular expression";
607
- throw new Error(
608
- `Invalid log watch pattern at logWatches[${index}]: ${message}`,
609
- );
610
- }
611
-
612
- const stream = watch.stream ?? "both";
613
- if (stream !== "stdout" && stream !== "stderr" && stream !== "both") {
614
- throw new Error(
615
- `Invalid logWatches[${index}].stream: ${stream}. Expected stdout, stderr, or both`,
616
- );
617
- }
618
-
619
- return {
620
- index,
621
- pattern,
622
- regex,
623
- stream,
624
- repeat: watch.repeat ?? false,
625
- fired: false,
626
- };
627
- });
628
- }
629
-
630
- private extractCompleteLines(
631
- managed: ManagedProcess,
632
- source: "stdout" | "stderr",
633
- data: Buffer,
634
- ): string[] {
635
- const chunk = data.toString();
636
- const pending =
637
- source === "stdout"
638
- ? managed.stdoutPendingLine
639
- : managed.stderrPendingLine;
640
- const merged = pending + chunk;
641
- const parts = merged.split(/\r?\n/);
642
- const completeLines = parts.slice(0, -1);
643
- const nextPending = parts[parts.length - 1] ?? "";
644
-
645
- if (source === "stdout") {
646
- managed.stdoutPendingLine = nextPending;
647
- } else {
648
- managed.stderrPendingLine = nextPending;
649
- }
650
-
651
- return completeLines;
652
- }
653
-
654
- private flushPendingLines(managed: ManagedProcess): void {
655
- if (managed.stdoutPendingLine) {
656
- try {
657
- appendFileSync(
658
- managed.combinedFile,
659
- `1:${managed.stdoutPendingLine}\n`,
660
- );
661
- } catch {
662
- // Ignore
663
- }
664
- this.matchWatches(managed, "stdout", [managed.stdoutPendingLine]);
665
- managed.stdoutPendingLine = "";
666
- }
667
-
668
- if (managed.stderrPendingLine) {
669
- try {
670
- appendFileSync(
671
- managed.combinedFile,
672
- `2:${managed.stderrPendingLine}\n`,
673
- );
674
- } catch {
675
- // Ignore
676
- }
677
- this.matchWatches(managed, "stderr", [managed.stderrPendingLine]);
678
- managed.stderrPendingLine = "";
679
- }
680
- }
681
-
682
- private matchWatches(
683
- managed: ManagedProcess,
684
- source: "stdout" | "stderr",
685
- lines: string[],
686
- ): void {
687
- if (managed.watches.length === 0 || lines.length === 0) return;
688
-
689
- for (const line of lines) {
690
- for (const watch of managed.watches) {
691
- if (!watch.repeat && watch.fired) continue;
692
- if (watch.stream !== "both" && watch.stream !== source) continue;
693
-
694
- if (!watch.regex.test(line)) continue;
695
-
696
- watch.fired = true;
697
-
698
- this.emit({
699
- type: "process_watch_matched",
700
- match: {
701
- processId: managed.id,
702
- processName: managed.name,
703
- processCommand: managed.command,
704
- source,
705
- line,
706
- watch: {
707
- index: watch.index,
708
- pattern: watch.pattern,
709
- stream: watch.stream,
710
- repeat: watch.repeat,
711
- },
712
- },
713
- });
714
- }
715
- }
716
- }
717
-
718
- private readTailLines(filePath: string, lines: number): string[] {
719
- try {
720
- const content = readFileSync(filePath, "utf-8");
721
- const allLines = content.split("\n");
722
- if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
723
- allLines.pop();
724
- }
725
- return allLines.slice(-lines);
726
- } catch {
727
- return [];
728
- }
729
- }
730
-
731
- private toProcessInfo(managed: ManagedProcess): ProcessInfo {
732
- return {
733
- id: managed.id,
734
- name: managed.name,
735
- pid: managed.pid,
736
- command: managed.command,
737
- cwd: managed.cwd,
738
- startTime: managed.startTime,
739
- endTime: managed.endTime,
740
- status: managed.status,
741
- exitCode: managed.exitCode,
742
- success: managed.success,
743
- stdoutFile: managed.stdoutFile,
744
- stderrFile: managed.stderrFile,
745
- alertOnSuccess: managed.alertOnSuccess,
746
- alertOnFailure: managed.alertOnFailure,
747
- alertOnKill: managed.alertOnKill,
748
- };
749
- }
750
- }
751
-
752
- export type {
753
- KillResult,
754
- ManagerEvent,
755
- ProcessInfo,
756
- ProcessStatus,
757
- WriteResult,
758
- };