@aliou/pi-processes 0.4.4 → 0.4.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.
package/manager.ts DELETED
@@ -1,522 +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
-
13
- import {
14
- type KillResult,
15
- LIVE_STATUSES,
16
- type ManagerEvent,
17
- type ProcessInfo,
18
- type ProcessStatus,
19
- type StartOptions,
20
- } from "./constants";
21
- import { isProcessGroupAlive, killProcessGroup } from "./utils";
22
- import { spawnCommand } from "./utils/command-executor";
23
-
24
- interface ManagedProcess extends ProcessInfo {
25
- process: ChildProcess;
26
- lastSignalSent: NodeJS.Signals | null;
27
- combinedFile: string;
28
- }
29
-
30
- interface ProcessManagerOptions {
31
- getConfiguredShellPath?: () => string | undefined;
32
- }
33
-
34
- export class ProcessManager {
35
- private processes: Map<string, ManagedProcess> = new Map();
36
- private counter = 0;
37
- private logDir: string;
38
- private events = new EventEmitter();
39
- private watcher: ReturnType<typeof setInterval> | null = null;
40
- private getConfiguredShellPath: () => string | undefined;
41
-
42
- constructor(options?: ProcessManagerOptions) {
43
- this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
44
- mkdirSync(this.logDir, { recursive: true });
45
- this.getConfiguredShellPath =
46
- options?.getConfiguredShellPath ?? (() => undefined);
47
- }
48
-
49
- onEvent(listener: (event: ManagerEvent) => void): () => void {
50
- this.events.on("event", listener);
51
- return () => this.events.off("event", listener);
52
- }
53
-
54
- private emit(event: ManagerEvent): void {
55
- this.events.emit("event", event);
56
- }
57
-
58
- private transition(managed: ManagedProcess, next: ProcessStatus): void {
59
- if (managed.status === next) return;
60
- const prev = managed.status;
61
- managed.status = next;
62
-
63
- this.emit({
64
- type: "process_status_changed",
65
- info: this.toProcessInfo(managed),
66
- prev,
67
- });
68
-
69
- if (next === "exited" || next === "killed") {
70
- this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
71
- }
72
-
73
- this.ensureWatcherRunning();
74
- this.stopWatcherIfIdle();
75
- }
76
-
77
- private ensureWatcherRunning(): void {
78
- if (this.watcher) return;
79
- if (!this.hasAliveishProcesses()) return;
80
-
81
- this.watcher = setInterval(() => {
82
- this.livenessTick();
83
- }, 5000);
84
- }
85
-
86
- private stopWatcherIfIdle(): void {
87
- if (!this.watcher) return;
88
- if (this.hasAliveishProcesses()) return;
89
-
90
- clearInterval(this.watcher);
91
- this.watcher = null;
92
- }
93
-
94
- private hasAliveishProcesses(): boolean {
95
- for (const p of this.processes.values()) {
96
- if (LIVE_STATUSES.has(p.status)) return true;
97
- }
98
- return false;
99
- }
100
-
101
- private livenessTick(): void {
102
- for (const managed of this.processes.values()) {
103
- if (!LIVE_STATUSES.has(managed.status)) continue;
104
- if (!managed.pid || managed.pid <= 0) continue;
105
-
106
- const alive = isProcessGroupAlive(managed.pid);
107
- if (alive) continue;
108
-
109
- if (!managed.endTime) {
110
- managed.endTime = Date.now();
111
- }
112
-
113
- if (managed.lastSignalSent) {
114
- managed.success = false;
115
- managed.exitCode = null;
116
- this.transition(managed, "killed");
117
- } else {
118
- managed.success = false;
119
- managed.exitCode = null;
120
- this.transition(managed, "exited");
121
- }
122
- }
123
- }
124
-
125
- start(
126
- name: string,
127
- command: string,
128
- cwd: string,
129
- options?: StartOptions,
130
- ): ProcessInfo {
131
- const id = `proc_${++this.counter}`;
132
- const stdoutFile = join(this.logDir, `${id}-stdout.log`);
133
- const stderrFile = join(this.logDir, `${id}-stderr.log`);
134
- const combinedFile = join(this.logDir, `${id}-combined.log`);
135
-
136
- appendFileSync(stdoutFile, "");
137
- appendFileSync(stderrFile, "");
138
- appendFileSync(combinedFile, "");
139
-
140
- const child = spawnCommand(command, cwd, this.getConfiguredShellPath());
141
-
142
- child.unref();
143
-
144
- const managed: ManagedProcess = {
145
- id,
146
- name,
147
- pid: child.pid ?? -1,
148
- command,
149
- cwd,
150
- startTime: Date.now(),
151
- endTime: null,
152
- status: "running",
153
- exitCode: null,
154
- success: null,
155
- stdoutFile,
156
- stderrFile,
157
- combinedFile,
158
- alertOnSuccess: options?.alertOnSuccess ?? false,
159
- alertOnFailure: options?.alertOnFailure ?? true,
160
- alertOnKill: options?.alertOnKill ?? false,
161
- process: child,
162
- lastSignalSent: null,
163
- };
164
-
165
- this.processes.set(id, managed);
166
-
167
- if (!child.pid) {
168
- try {
169
- appendFileSync(stderrFile, "Spawn error: missing pid\n");
170
- } catch {
171
- // Ignore
172
- }
173
- managed.exitCode = -1;
174
- managed.success = false;
175
- managed.endTime = Date.now();
176
- this.transition(managed, "exited");
177
- return this.toProcessInfo(managed);
178
- }
179
-
180
- child.stdout?.on("data", (data: Buffer) => {
181
- try {
182
- appendFileSync(stdoutFile, data);
183
- const lines = data.toString().split("\n");
184
- // The last element after split is either empty (if data ended with \n)
185
- // or a partial line. We write all parts with the prefix and newline.
186
- const tagged = lines
187
- .map((line, i) =>
188
- i < lines.length - 1 ? `1:${line}\n` : line ? `1:${line}\n` : "",
189
- )
190
- .join("");
191
- if (tagged) appendFileSync(combinedFile, tagged);
192
- } catch {
193
- // Ignore
194
- }
195
- });
196
-
197
- child.stderr?.on("data", (data: Buffer) => {
198
- try {
199
- appendFileSync(stderrFile, data);
200
- const lines = data.toString().split("\n");
201
- const tagged = lines
202
- .map((line, i) =>
203
- i < lines.length - 1 ? `2:${line}\n` : line ? `2:${line}\n` : "",
204
- )
205
- .join("");
206
- if (tagged) appendFileSync(combinedFile, tagged);
207
- } catch {
208
- // Ignore
209
- }
210
- });
211
-
212
- child.on("close", (code, signal) => {
213
- if (managed.endTime) return;
214
-
215
- managed.exitCode = code;
216
- managed.endTime = Date.now();
217
- managed.success = code === 0;
218
-
219
- if (signal) {
220
- this.transition(managed, "killed");
221
- } else {
222
- this.transition(managed, "exited");
223
- }
224
- });
225
-
226
- child.on("error", (err) => {
227
- try {
228
- appendFileSync(stderrFile, `Process error: ${err.message}\n`);
229
- } catch {
230
- // Ignore
231
- }
232
-
233
- if (!managed.endTime) {
234
- managed.exitCode = -1;
235
- managed.success = false;
236
- managed.endTime = Date.now();
237
- this.transition(managed, "exited");
238
- }
239
- });
240
-
241
- this.emit({ type: "process_started", info: this.toProcessInfo(managed) });
242
- this.ensureWatcherRunning();
243
-
244
- return this.toProcessInfo(managed);
245
- }
246
-
247
- list(): ProcessInfo[] {
248
- return Array.from(this.processes.values())
249
- .map((p) => this.toProcessInfo(p))
250
- .reverse();
251
- }
252
-
253
- get(id: string): ProcessInfo | null {
254
- const managed = this.processes.get(id);
255
- return managed ? this.toProcessInfo(managed) : null;
256
- }
257
-
258
- find(query: string): ProcessInfo | null {
259
- const byId = this.processes.get(query);
260
- if (byId) return this.toProcessInfo(byId);
261
-
262
- const queryLower = query.toLowerCase();
263
- for (const managed of this.processes.values()) {
264
- if (managed.name.toLowerCase().includes(queryLower)) {
265
- return this.toProcessInfo(managed);
266
- }
267
- if (managed.command.toLowerCase().includes(queryLower)) {
268
- return this.toProcessInfo(managed);
269
- }
270
- }
271
- return null;
272
- }
273
-
274
- getOutput(
275
- id: string,
276
- tailLines = 100,
277
- ): { stdout: string[]; stderr: string[]; status: string } | null {
278
- const managed = this.processes.get(id);
279
- if (!managed) return null;
280
-
281
- return {
282
- stdout: this.readTailLines(managed.stdoutFile, tailLines),
283
- stderr: this.readTailLines(managed.stderrFile, tailLines),
284
- status: managed.status,
285
- };
286
- }
287
-
288
- getCombinedOutput(
289
- id: string,
290
- tailLines = 100,
291
- ): { type: "stdout" | "stderr"; text: string }[] | null {
292
- const managed = this.processes.get(id);
293
- if (!managed) return null;
294
-
295
- const rawLines = this.readTailLines(managed.combinedFile, tailLines);
296
- return rawLines.map((line) => {
297
- if (line.startsWith("2:")) {
298
- return { type: "stderr", text: line.slice(2) };
299
- }
300
- // Default to stdout (handles "1:" prefix and any malformed lines).
301
- return {
302
- type: "stdout",
303
- text: line.startsWith("1:") ? line.slice(2) : line,
304
- };
305
- });
306
- }
307
-
308
- getFullOutput(id: string): { stdout: string; stderr: string } | null {
309
- const managed = this.processes.get(id);
310
- if (!managed) return null;
311
-
312
- try {
313
- return {
314
- stdout: readFileSync(managed.stdoutFile, "utf-8"),
315
- stderr: readFileSync(managed.stderrFile, "utf-8"),
316
- };
317
- } catch {
318
- return { stdout: "", stderr: "" };
319
- }
320
- }
321
-
322
- getLogFiles(id: string): { stdoutFile: string; stderrFile: string } | null {
323
- const managed = this.processes.get(id);
324
- if (!managed) return null;
325
- return {
326
- stdoutFile: managed.stdoutFile,
327
- stderrFile: managed.stderrFile,
328
- };
329
- }
330
-
331
- async kill(
332
- id: string,
333
- opts?: { signal?: NodeJS.Signals; timeoutMs?: number },
334
- ): Promise<KillResult> {
335
- const managed = this.processes.get(id);
336
- if (!managed) {
337
- return {
338
- ok: false,
339
- info: {
340
- id,
341
- name: "(unknown)",
342
- pid: -1,
343
- command: "",
344
- cwd: "",
345
- startTime: 0,
346
- endTime: null,
347
- status: "exited",
348
- exitCode: null,
349
- success: false,
350
- stdoutFile: "",
351
- stderrFile: "",
352
- alertOnSuccess: false,
353
- alertOnFailure: true,
354
- alertOnKill: false,
355
- },
356
- reason: "not_found",
357
- };
358
- }
359
-
360
- const signal = opts?.signal ?? "SIGTERM";
361
- const timeoutMs = opts?.timeoutMs ?? 3000;
362
-
363
- managed.alertOnKill = false;
364
-
365
- if (!LIVE_STATUSES.has(managed.status)) {
366
- return { ok: true, info: this.toProcessInfo(managed) };
367
- }
368
-
369
- this.transition(managed, "terminating");
370
-
371
- try {
372
- killProcessGroup(managed.pid, signal);
373
- managed.lastSignalSent = signal;
374
- } catch (error) {
375
- const err = error as NodeJS.ErrnoException;
376
- if (err.code !== "EPERM") {
377
- return {
378
- ok: false,
379
- info: this.toProcessInfo(managed),
380
- reason: "error",
381
- };
382
- }
383
- }
384
-
385
- const graceMs = signal === "SIGKILL" ? 200 : timeoutMs;
386
-
387
- await new Promise((r) => setTimeout(r, graceMs));
388
-
389
- const alive = isProcessGroupAlive(managed.pid);
390
-
391
- if (alive) {
392
- this.transition(managed, "terminate_timeout");
393
- return {
394
- ok: false,
395
- info: this.toProcessInfo(managed),
396
- reason: "timeout",
397
- };
398
- }
399
-
400
- if (!managed.endTime) {
401
- managed.endTime = Date.now();
402
- managed.exitCode = null;
403
- managed.success = false;
404
- }
405
-
406
- this.transition(managed, "killed");
407
- return { ok: true, info: this.toProcessInfo(managed) };
408
- }
409
-
410
- clearFinished(): number {
411
- let cleared = 0;
412
- for (const [id, managed] of this.processes) {
413
- if (LIVE_STATUSES.has(managed.status)) {
414
- continue;
415
- }
416
-
417
- try {
418
- rmSync(managed.stdoutFile, { force: true });
419
- rmSync(managed.stderrFile, { force: true });
420
- rmSync(managed.combinedFile, { force: true });
421
- } catch {
422
- // Ignore
423
- }
424
-
425
- this.processes.delete(id);
426
- cleared++;
427
- }
428
-
429
- if (cleared > 0) {
430
- this.emit({ type: "processes_changed" });
431
- }
432
-
433
- this.stopWatcherIfIdle();
434
- return cleared;
435
- }
436
-
437
- shutdownKillAll(): void {
438
- for (const p of this.processes.values()) {
439
- if (!LIVE_STATUSES.has(p.status)) continue;
440
- try {
441
- killProcessGroup(p.pid, "SIGKILL");
442
- } catch {
443
- // Ignore - process may already be dead
444
- }
445
- }
446
- }
447
-
448
- stopWatcher(): void {
449
- if (this.watcher) {
450
- clearInterval(this.watcher);
451
- this.watcher = null;
452
- }
453
- }
454
-
455
- cleanup(): void {
456
- this.stopWatcher();
457
-
458
- for (const p of this.processes.values()) {
459
- if (!LIVE_STATUSES.has(p.status)) continue;
460
- try {
461
- killProcessGroup(p.pid, "SIGKILL");
462
- } catch {
463
- // Ignore
464
- }
465
- }
466
-
467
- try {
468
- rmSync(this.logDir, { recursive: true, force: true });
469
- } catch {
470
- // Ignore
471
- }
472
- }
473
-
474
- getFileSize(id: string): { stdout: number; stderr: number } | null {
475
- const managed = this.processes.get(id);
476
- if (!managed) return null;
477
-
478
- try {
479
- return {
480
- stdout: statSync(managed.stdoutFile).size,
481
- stderr: statSync(managed.stderrFile).size,
482
- };
483
- } catch {
484
- return { stdout: 0, stderr: 0 };
485
- }
486
- }
487
-
488
- private readTailLines(filePath: string, lines: number): string[] {
489
- try {
490
- const content = readFileSync(filePath, "utf-8");
491
- const allLines = content.split("\n");
492
- if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
493
- allLines.pop();
494
- }
495
- return allLines.slice(-lines);
496
- } catch {
497
- return [];
498
- }
499
- }
500
-
501
- private toProcessInfo(managed: ManagedProcess): ProcessInfo {
502
- return {
503
- id: managed.id,
504
- name: managed.name,
505
- pid: managed.pid,
506
- command: managed.command,
507
- cwd: managed.cwd,
508
- startTime: managed.startTime,
509
- endTime: managed.endTime,
510
- status: managed.status,
511
- exitCode: managed.exitCode,
512
- success: managed.success,
513
- stdoutFile: managed.stdoutFile,
514
- stderrFile: managed.stderrFile,
515
- alertOnSuccess: managed.alertOnSuccess,
516
- alertOnFailure: managed.alertOnFailure,
517
- alertOnKill: managed.alertOnKill,
518
- };
519
- }
520
- }
521
-
522
- export type { ProcessInfo, ProcessStatus, ManagerEvent, KillResult };
@@ -1,20 +0,0 @@
1
- import type { ExecuteResult } from "../../constants";
2
- import type { ProcessManager } from "../../manager";
3
-
4
- export function executeClear(manager: ProcessManager): ExecuteResult {
5
- const cleared = manager.clearFinished();
6
- const message =
7
- cleared > 0
8
- ? `Cleared ${cleared} finished process(es)`
9
- : "No finished processes to clear";
10
-
11
- return {
12
- content: [{ type: "text", text: message }],
13
- details: {
14
- action: "clear",
15
- success: true,
16
- message,
17
- cleared,
18
- },
19
- };
20
- }
@@ -1,49 +0,0 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { ExecuteResult } from "../../constants";
3
- import type { ProcessManager } from "../../manager";
4
- import { executeClear } from "./clear";
5
- import { executeKill } from "./kill";
6
- import { executeList } from "./list";
7
- import { executeLogs } from "./logs";
8
- import { executeOutput } from "./output";
9
- import { executeStart } from "./start";
10
-
11
- interface ActionParams {
12
- action: string;
13
- command?: string;
14
- name?: string;
15
- id?: string;
16
- alertOnSuccess?: boolean;
17
- alertOnFailure?: boolean;
18
- alertOnKill?: boolean;
19
- }
20
-
21
- export async function executeAction(
22
- params: ActionParams,
23
- manager: ProcessManager,
24
- ctx: ExtensionContext,
25
- ): Promise<ExecuteResult> {
26
- switch (params.action) {
27
- case "start":
28
- return executeStart(params, manager, ctx);
29
- case "list":
30
- return executeList(manager);
31
- case "output":
32
- return executeOutput(params, manager);
33
- case "logs":
34
- return executeLogs(params, manager);
35
- case "kill":
36
- return executeKill(params, manager);
37
- case "clear":
38
- return executeClear(manager);
39
- default:
40
- return {
41
- content: [{ type: "text", text: `Unknown action: ${params.action}` }],
42
- details: {
43
- action: params.action,
44
- success: false,
45
- message: `Unknown action: ${params.action}`,
46
- },
47
- };
48
- }
49
- }
@@ -1,76 +0,0 @@
1
- import type { ExecuteResult } from "../../constants";
2
- import type { ProcessManager } from "../../manager";
3
-
4
- interface KillParams {
5
- id?: string;
6
- }
7
-
8
- export async function executeKill(
9
- params: KillParams,
10
- manager: ProcessManager,
11
- ): Promise<ExecuteResult> {
12
- if (!params.id) {
13
- return {
14
- content: [{ type: "text", text: "Missing required parameter: id" }],
15
- details: {
16
- action: "kill",
17
- success: false,
18
- message: "Missing required parameter: id",
19
- },
20
- };
21
- }
22
-
23
- const proc = manager.find(params.id);
24
- if (!proc) {
25
- const message = `Process not found: ${params.id}`;
26
- return {
27
- content: [{ type: "text", text: message }],
28
- details: {
29
- action: "kill",
30
- success: false,
31
- message,
32
- },
33
- };
34
- }
35
-
36
- const result = await manager.kill(proc.id, {
37
- signal: "SIGTERM",
38
- timeoutMs: 3000,
39
- });
40
-
41
- if (result.ok) {
42
- const message = `Terminated "${proc.name}" (${proc.id})`;
43
- return {
44
- content: [{ type: "text", text: message }],
45
- details: {
46
- action: "kill",
47
- success: true,
48
- message,
49
- },
50
- };
51
- }
52
-
53
- if (result.reason === "timeout") {
54
- const message =
55
- `SIGTERM timed out for "${proc.name}" (${proc.id}). ` +
56
- "Run /process:list and press x on terminate_timeout to force kill (SIGKILL).";
57
- return {
58
- content: [{ type: "text", text: message }],
59
- details: {
60
- action: "kill",
61
- success: false,
62
- message,
63
- },
64
- };
65
- }
66
-
67
- const message = `Failed to terminate "${proc.name}" (${proc.id})`;
68
- return {
69
- content: [{ type: "text", text: message }],
70
- details: {
71
- action: "kill",
72
- success: false,
73
- message,
74
- },
75
- };
76
- }
@@ -1,37 +0,0 @@
1
- import type { ExecuteResult } from "../../constants";
2
- import type { ProcessManager } from "../../manager";
3
- import { formatRuntime, formatStatus, truncateCmd } from "../../utils";
4
-
5
- export function executeList(manager: ProcessManager): ExecuteResult {
6
- const processes = manager.list();
7
-
8
- if (processes.length === 0) {
9
- return {
10
- content: [{ type: "text", text: "No background processes running" }],
11
- details: {
12
- action: "list",
13
- success: true,
14
- message: "No background processes running",
15
- processes: [],
16
- },
17
- };
18
- }
19
-
20
- const summary = processes
21
- .map(
22
- (p) =>
23
- `${p.id} "${p.name}": ${truncateCmd(p.command)} [${formatStatus(p)}] ${formatRuntime(p.startTime, p.endTime)}`,
24
- )
25
- .join("\n");
26
-
27
- const message = `${processes.length} process(es):\n${summary}`;
28
- return {
29
- content: [{ type: "text", text: message }],
30
- details: {
31
- action: "list",
32
- success: true,
33
- message,
34
- processes,
35
- },
36
- };
37
- }