@aliou/pi-processes 0.1.1 → 0.2.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.
- package/README.md +27 -0
- package/commands/index.ts +3 -465
- package/hooks/cleanup.ts +3 -1
- package/hooks/process-end.ts +9 -21
- package/hooks/widget.ts +33 -62
- package/index.ts +8 -0
- package/manager.ts +254 -181
- package/package.json +6 -2
- package/tools/actions/clear.ts +20 -0
- package/tools/actions/index.ts +49 -0
- package/tools/actions/kill.ts +76 -0
- package/tools/actions/list.ts +37 -0
- package/tools/actions/logs.ts +59 -0
- package/tools/actions/output.ts +73 -0
- package/tools/actions/start.ts +55 -0
- package/tools/index.ts +29 -312
- package/constants.ts +0 -2
package/manager.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import {
|
|
3
4
|
appendFileSync,
|
|
4
5
|
mkdirSync,
|
|
@@ -9,122 +10,134 @@ import {
|
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
exitCode: number | null;
|
|
22
|
-
success: boolean | null; // null if running, true if exit code 0, false otherwise
|
|
23
|
-
stdoutFile: string;
|
|
24
|
-
stderrFile: string;
|
|
25
|
-
notifyOnSuccess: boolean;
|
|
26
|
-
notifyOnFailure: boolean;
|
|
27
|
-
notifyOnKill: boolean;
|
|
28
|
-
}
|
|
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";
|
|
29
22
|
|
|
30
23
|
interface ManagedProcess extends ProcessInfo {
|
|
31
24
|
process: ChildProcess;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Generate a friendly name from command
|
|
35
|
-
function inferName(command: string): string {
|
|
36
|
-
const cmd = command.toLowerCase();
|
|
37
|
-
|
|
38
|
-
// Dev servers
|
|
39
|
-
if (cmd.includes("dev") && cmd.includes("backend")) return "backend-dev";
|
|
40
|
-
if (cmd.includes("dev") && cmd.includes("frontend")) return "frontend-dev";
|
|
41
|
-
if (cmd.includes("dev") && cmd.includes("api")) return "api-dev";
|
|
42
|
-
if (
|
|
43
|
-
cmd.includes("pnpm dev") ||
|
|
44
|
-
cmd.includes("npm run dev") ||
|
|
45
|
-
cmd.includes("yarn dev")
|
|
46
|
-
)
|
|
47
|
-
return "dev-server";
|
|
48
|
-
if (cmd.includes("vite")) return "vite-dev";
|
|
49
|
-
if (cmd.includes("next dev")) return "next-dev";
|
|
50
|
-
|
|
51
|
-
// Build
|
|
52
|
-
if (cmd.includes("build")) return "build";
|
|
53
|
-
if (cmd.includes("compile")) return "compile";
|
|
54
|
-
|
|
55
|
-
// Tests
|
|
56
|
-
if (cmd.includes("test") || cmd.includes("jest") || cmd.includes("vitest"))
|
|
57
|
-
return "tests";
|
|
58
|
-
|
|
59
|
-
// Watch
|
|
60
|
-
if (cmd.includes("watch")) return "watcher";
|
|
61
|
-
|
|
62
|
-
// Logs
|
|
63
|
-
if (cmd.includes("tail")) return "log-tail";
|
|
64
|
-
|
|
65
|
-
// Docker
|
|
66
|
-
if (cmd.includes("docker-compose") || cmd.includes("docker compose"))
|
|
67
|
-
return "docker";
|
|
68
|
-
|
|
69
|
-
// Database
|
|
70
|
-
if (
|
|
71
|
-
cmd.includes("postgres") ||
|
|
72
|
-
cmd.includes("mysql") ||
|
|
73
|
-
cmd.includes("mongo")
|
|
74
|
-
)
|
|
75
|
-
return "database";
|
|
76
|
-
|
|
77
|
-
// Extract first meaningful word
|
|
78
|
-
const words = command.split(/\s+/);
|
|
79
|
-
const firstWord = (words[0] ?? "process")
|
|
80
|
-
.replace(/^\.\//, "")
|
|
81
|
-
.replace(/\.(sh|js|ts|py)$/, "");
|
|
82
|
-
return firstWord.slice(0, 20);
|
|
25
|
+
lastSignalSent: NodeJS.Signals | null;
|
|
83
26
|
}
|
|
84
27
|
|
|
85
28
|
export class ProcessManager {
|
|
86
29
|
private processes: Map<string, ManagedProcess> = new Map();
|
|
87
30
|
private counter = 0;
|
|
88
31
|
private logDir: string;
|
|
89
|
-
|
|
32
|
+
private events = new EventEmitter();
|
|
33
|
+
private watcher: ReturnType<typeof setInterval> | null = null;
|
|
90
34
|
|
|
91
35
|
constructor() {
|
|
92
36
|
this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
|
|
93
37
|
mkdirSync(this.logDir, { recursive: true });
|
|
94
38
|
}
|
|
95
39
|
|
|
96
|
-
|
|
97
|
-
this.
|
|
40
|
+
onEvent(listener: (event: ManagerEvent) => void): () => void {
|
|
41
|
+
this.events.on("event", listener);
|
|
42
|
+
return () => this.events.off("event", listener);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private emit(event: ManagerEvent): void {
|
|
46
|
+
this.events.emit("event", event);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private transition(managed: ManagedProcess, next: ProcessStatus): void {
|
|
50
|
+
if (managed.status === next) return;
|
|
51
|
+
const prev = managed.status;
|
|
52
|
+
managed.status = next;
|
|
53
|
+
|
|
54
|
+
this.emit({
|
|
55
|
+
type: "process_status_changed",
|
|
56
|
+
info: this.toProcessInfo(managed),
|
|
57
|
+
prev,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (next === "exited" || next === "killed") {
|
|
61
|
+
this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.ensureWatcherRunning();
|
|
65
|
+
this.stopWatcherIfIdle();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private ensureWatcherRunning(): void {
|
|
69
|
+
if (this.watcher) return;
|
|
70
|
+
if (!this.hasAliveishProcesses()) return;
|
|
71
|
+
|
|
72
|
+
this.watcher = setInterval(() => {
|
|
73
|
+
this.livenessTick();
|
|
74
|
+
}, 5000);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private stopWatcherIfIdle(): void {
|
|
78
|
+
if (!this.watcher) return;
|
|
79
|
+
if (this.hasAliveishProcesses()) return;
|
|
80
|
+
|
|
81
|
+
clearInterval(this.watcher);
|
|
82
|
+
this.watcher = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private hasAliveishProcesses(): boolean {
|
|
86
|
+
for (const p of this.processes.values()) {
|
|
87
|
+
if (LIVE_STATUSES.has(p.status)) return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private livenessTick(): void {
|
|
93
|
+
for (const managed of this.processes.values()) {
|
|
94
|
+
if (!LIVE_STATUSES.has(managed.status)) continue;
|
|
95
|
+
if (!managed.pid || managed.pid <= 0) continue;
|
|
96
|
+
|
|
97
|
+
const alive = isProcessGroupAlive(managed.pid);
|
|
98
|
+
if (alive) continue;
|
|
99
|
+
|
|
100
|
+
if (!managed.endTime) {
|
|
101
|
+
managed.endTime = Date.now();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (managed.lastSignalSent) {
|
|
105
|
+
managed.success = false;
|
|
106
|
+
managed.exitCode = null;
|
|
107
|
+
this.transition(managed, "killed");
|
|
108
|
+
} else {
|
|
109
|
+
managed.success = false;
|
|
110
|
+
managed.exitCode = null;
|
|
111
|
+
this.transition(managed, "exited");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
start(
|
|
117
|
+
name: string,
|
|
101
118
|
command: string,
|
|
102
119
|
cwd: string,
|
|
103
|
-
|
|
104
|
-
options?: {
|
|
105
|
-
notifyOnSuccess?: boolean;
|
|
106
|
-
notifyOnFailure?: boolean;
|
|
107
|
-
notifyOnKill?: boolean;
|
|
108
|
-
},
|
|
120
|
+
options?: StartOptions,
|
|
109
121
|
): ProcessInfo {
|
|
110
122
|
const id = `proc_${++this.counter}`;
|
|
111
|
-
const friendlyName = name || inferName(command);
|
|
112
123
|
const stdoutFile = join(this.logDir, `${id}-stdout.log`);
|
|
113
124
|
const stderrFile = join(this.logDir, `${id}-stderr.log`);
|
|
114
125
|
|
|
115
126
|
appendFileSync(stdoutFile, "");
|
|
116
127
|
appendFileSync(stderrFile, "");
|
|
117
128
|
|
|
118
|
-
const child = spawn(command, {
|
|
129
|
+
const child = spawn("/bin/bash", ["-lc", command], {
|
|
119
130
|
cwd,
|
|
120
|
-
|
|
131
|
+
env: process.env,
|
|
121
132
|
stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
-
detached:
|
|
133
|
+
detached: true,
|
|
123
134
|
});
|
|
124
135
|
|
|
136
|
+
child.unref();
|
|
137
|
+
|
|
125
138
|
const managed: ManagedProcess = {
|
|
126
139
|
id,
|
|
127
|
-
name
|
|
140
|
+
name,
|
|
128
141
|
pid: child.pid ?? -1,
|
|
129
142
|
command,
|
|
130
143
|
cwd,
|
|
@@ -139,13 +152,29 @@ export class ProcessManager {
|
|
|
139
152
|
notifyOnFailure: options?.notifyOnFailure ?? true,
|
|
140
153
|
notifyOnKill: options?.notifyOnKill ?? false,
|
|
141
154
|
process: child,
|
|
155
|
+
lastSignalSent: null,
|
|
142
156
|
};
|
|
143
157
|
|
|
158
|
+
this.processes.set(id, managed);
|
|
159
|
+
|
|
160
|
+
if (!child.pid) {
|
|
161
|
+
try {
|
|
162
|
+
appendFileSync(stderrFile, "Spawn error: missing pid\n");
|
|
163
|
+
} catch {
|
|
164
|
+
// Ignore
|
|
165
|
+
}
|
|
166
|
+
managed.exitCode = -1;
|
|
167
|
+
managed.success = false;
|
|
168
|
+
managed.endTime = Date.now();
|
|
169
|
+
this.transition(managed, "exited");
|
|
170
|
+
return this.toProcessInfo(managed);
|
|
171
|
+
}
|
|
172
|
+
|
|
144
173
|
child.stdout?.on("data", (data: Buffer) => {
|
|
145
174
|
try {
|
|
146
175
|
appendFileSync(stdoutFile, data);
|
|
147
176
|
} catch {
|
|
148
|
-
// Ignore
|
|
177
|
+
// Ignore
|
|
149
178
|
}
|
|
150
179
|
});
|
|
151
180
|
|
|
@@ -153,20 +182,22 @@ export class ProcessManager {
|
|
|
153
182
|
try {
|
|
154
183
|
appendFileSync(stderrFile, data);
|
|
155
184
|
} catch {
|
|
156
|
-
// Ignore
|
|
185
|
+
// Ignore
|
|
157
186
|
}
|
|
158
187
|
});
|
|
159
188
|
|
|
160
189
|
child.on("close", (code, signal) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
190
|
+
if (managed.endTime) return;
|
|
191
|
+
|
|
165
192
|
managed.exitCode = code;
|
|
166
193
|
managed.endTime = Date.now();
|
|
167
194
|
managed.success = code === 0;
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
|
|
196
|
+
if (signal) {
|
|
197
|
+
this.transition(managed, "killed");
|
|
198
|
+
} else {
|
|
199
|
+
this.transition(managed, "exited");
|
|
200
|
+
}
|
|
170
201
|
});
|
|
171
202
|
|
|
172
203
|
child.on("error", (err) => {
|
|
@@ -175,41 +206,36 @@ export class ProcessManager {
|
|
|
175
206
|
} catch {
|
|
176
207
|
// Ignore
|
|
177
208
|
}
|
|
178
|
-
|
|
179
|
-
managed.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
209
|
+
|
|
210
|
+
if (!managed.endTime) {
|
|
211
|
+
managed.exitCode = -1;
|
|
212
|
+
managed.success = false;
|
|
213
|
+
managed.endTime = Date.now();
|
|
214
|
+
this.transition(managed, "exited");
|
|
215
|
+
}
|
|
183
216
|
});
|
|
184
217
|
|
|
185
|
-
this.
|
|
218
|
+
this.emit({ type: "process_started", info: this.toProcessInfo(managed) });
|
|
219
|
+
this.ensureWatcherRunning();
|
|
186
220
|
|
|
187
221
|
return this.toProcessInfo(managed);
|
|
188
222
|
}
|
|
189
223
|
|
|
190
224
|
list(): ProcessInfo[] {
|
|
191
|
-
// Check if any "running" processes have actually exited
|
|
192
|
-
this.checkRunningProcesses();
|
|
193
225
|
return Array.from(this.processes.values()).map((p) =>
|
|
194
226
|
this.toProcessInfo(p),
|
|
195
227
|
);
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
get(id: string): ProcessInfo | null {
|
|
199
|
-
this.checkRunningProcesses();
|
|
200
231
|
const managed = this.processes.get(id);
|
|
201
232
|
return managed ? this.toProcessInfo(managed) : null;
|
|
202
233
|
}
|
|
203
234
|
|
|
204
|
-
// Find by ID or name (partial match)
|
|
205
235
|
find(query: string): ProcessInfo | null {
|
|
206
|
-
this.checkRunningProcesses();
|
|
207
|
-
|
|
208
|
-
// Exact ID match first
|
|
209
236
|
const byId = this.processes.get(query);
|
|
210
237
|
if (byId) return this.toProcessInfo(byId);
|
|
211
238
|
|
|
212
|
-
// Search by name (case insensitive, partial match)
|
|
213
239
|
const queryLower = query.toLowerCase();
|
|
214
240
|
for (const managed of this.processes.values()) {
|
|
215
241
|
if (managed.name.toLowerCase().includes(queryLower)) {
|
|
@@ -259,113 +285,145 @@ export class ProcessManager {
|
|
|
259
285
|
};
|
|
260
286
|
}
|
|
261
287
|
|
|
262
|
-
kill(
|
|
288
|
+
async kill(
|
|
289
|
+
id: string,
|
|
290
|
+
opts?: { signal?: NodeJS.Signals; timeoutMs?: number },
|
|
291
|
+
): Promise<KillResult> {
|
|
263
292
|
const managed = this.processes.get(id);
|
|
264
|
-
if (!managed)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
293
|
+
if (!managed) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
info: {
|
|
297
|
+
id,
|
|
298
|
+
name: "(unknown)",
|
|
299
|
+
pid: -1,
|
|
300
|
+
command: "",
|
|
301
|
+
cwd: "",
|
|
302
|
+
startTime: 0,
|
|
303
|
+
endTime: null,
|
|
304
|
+
status: "exited",
|
|
305
|
+
exitCode: null,
|
|
306
|
+
success: false,
|
|
307
|
+
stdoutFile: "",
|
|
308
|
+
stderrFile: "",
|
|
309
|
+
notifyOnSuccess: false,
|
|
310
|
+
notifyOnFailure: true,
|
|
311
|
+
notifyOnKill: false,
|
|
312
|
+
},
|
|
313
|
+
reason: "not_found",
|
|
314
|
+
};
|
|
268
315
|
}
|
|
269
316
|
|
|
270
|
-
|
|
317
|
+
const signal = opts?.signal ?? "SIGTERM";
|
|
318
|
+
const timeoutMs = opts?.timeoutMs ?? 3000;
|
|
319
|
+
|
|
271
320
|
managed.notifyOnKill = false;
|
|
272
321
|
|
|
273
|
-
managed.status
|
|
274
|
-
|
|
275
|
-
|
|
322
|
+
if (!LIVE_STATUSES.has(managed.status)) {
|
|
323
|
+
return { ok: true, info: this.toProcessInfo(managed) };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.transition(managed, "terminating");
|
|
276
327
|
|
|
277
328
|
try {
|
|
278
|
-
managed.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
329
|
+
killProcessGroup(managed.pid, signal);
|
|
330
|
+
managed.lastSignalSent = signal;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const err = error as NodeJS.ErrnoException;
|
|
333
|
+
if (err.code !== "EPERM") {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
info: this.toProcessInfo(managed),
|
|
337
|
+
reason: "error",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const graceMs = signal === "SIGKILL" ? 200 : timeoutMs;
|
|
343
|
+
|
|
344
|
+
await new Promise((r) => setTimeout(r, graceMs));
|
|
345
|
+
|
|
346
|
+
const alive = isProcessGroupAlive(managed.pid);
|
|
347
|
+
|
|
348
|
+
if (alive) {
|
|
349
|
+
this.transition(managed, "terminate_timeout");
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
info: this.toProcessInfo(managed),
|
|
353
|
+
reason: "timeout",
|
|
354
|
+
};
|
|
293
355
|
}
|
|
356
|
+
|
|
357
|
+
if (!managed.endTime) {
|
|
358
|
+
managed.endTime = Date.now();
|
|
359
|
+
managed.exitCode = null;
|
|
360
|
+
managed.success = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.transition(managed, "killed");
|
|
364
|
+
return { ok: true, info: this.toProcessInfo(managed) };
|
|
294
365
|
}
|
|
295
366
|
|
|
296
|
-
// Clear finished processes (not running)
|
|
297
367
|
clearFinished(): number {
|
|
298
368
|
let cleared = 0;
|
|
299
369
|
for (const [id, managed] of this.processes) {
|
|
300
|
-
if (managed.status
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
rmSync(managed.stdoutFile, { force: true });
|
|
304
|
-
rmSync(managed.stderrFile, { force: true });
|
|
305
|
-
} catch {
|
|
306
|
-
// Ignore
|
|
307
|
-
}
|
|
308
|
-
this.processes.delete(id);
|
|
309
|
-
cleared++;
|
|
370
|
+
if (LIVE_STATUSES.has(managed.status)) {
|
|
371
|
+
continue;
|
|
310
372
|
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
rmSync(managed.stdoutFile, { force: true });
|
|
376
|
+
rmSync(managed.stderrFile, { force: true });
|
|
377
|
+
} catch {
|
|
378
|
+
// Ignore
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.processes.delete(id);
|
|
382
|
+
cleared++;
|
|
311
383
|
}
|
|
312
|
-
return cleared;
|
|
313
|
-
}
|
|
314
384
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.kill(id);
|
|
385
|
+
if (cleared > 0) {
|
|
386
|
+
this.emit({ type: "processes_changed" });
|
|
318
387
|
}
|
|
388
|
+
|
|
389
|
+
this.stopWatcherIfIdle();
|
|
390
|
+
return cleared;
|
|
319
391
|
}
|
|
320
392
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// If error code is EPERM, process exists but we don't have permission (still alive)
|
|
330
|
-
const err = error as NodeJS.ErrnoException;
|
|
331
|
-
return err.code === "EPERM";
|
|
393
|
+
shutdownKillAll(): void {
|
|
394
|
+
for (const p of this.processes.values()) {
|
|
395
|
+
if (!LIVE_STATUSES.has(p.status)) continue;
|
|
396
|
+
try {
|
|
397
|
+
killProcessGroup(p.pid, "SIGKILL");
|
|
398
|
+
} catch {
|
|
399
|
+
// Ignore - process may already be dead
|
|
400
|
+
}
|
|
332
401
|
}
|
|
333
402
|
}
|
|
334
403
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Process is no longer alive but we didn't get the close event yet
|
|
340
|
-
// Mark it as exited with unknown exit code
|
|
341
|
-
managed.status = "exited";
|
|
342
|
-
managed.exitCode = null;
|
|
343
|
-
managed.success = false;
|
|
344
|
-
managed.endTime = Date.now();
|
|
345
|
-
this.emitProcessEnd(this.toProcessInfo(managed));
|
|
346
|
-
}
|
|
404
|
+
stopWatcher(): void {
|
|
405
|
+
if (this.watcher) {
|
|
406
|
+
clearInterval(this.watcher);
|
|
407
|
+
this.watcher = null;
|
|
347
408
|
}
|
|
348
409
|
}
|
|
349
410
|
|
|
350
411
|
cleanup(): void {
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
412
|
+
this.stopWatcher();
|
|
413
|
+
|
|
414
|
+
for (const p of this.processes.values()) {
|
|
415
|
+
if (!LIVE_STATUSES.has(p.status)) continue;
|
|
416
|
+
try {
|
|
417
|
+
killProcessGroup(p.pid, "SIGKILL");
|
|
418
|
+
} catch {
|
|
419
|
+
// Ignore
|
|
420
|
+
}
|
|
356
421
|
}
|
|
357
|
-
}
|
|
358
422
|
|
|
359
|
-
private readTailLines(filePath: string, lines: number): string[] {
|
|
360
423
|
try {
|
|
361
|
-
|
|
362
|
-
const allLines = content.split("\n");
|
|
363
|
-
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
364
|
-
allLines.pop();
|
|
365
|
-
}
|
|
366
|
-
return allLines.slice(-lines);
|
|
424
|
+
rmSync(this.logDir, { recursive: true, force: true });
|
|
367
425
|
} catch {
|
|
368
|
-
|
|
426
|
+
// Ignore
|
|
369
427
|
}
|
|
370
428
|
}
|
|
371
429
|
|
|
@@ -383,6 +441,19 @@ export class ProcessManager {
|
|
|
383
441
|
}
|
|
384
442
|
}
|
|
385
443
|
|
|
444
|
+
private readTailLines(filePath: string, lines: number): string[] {
|
|
445
|
+
try {
|
|
446
|
+
const content = readFileSync(filePath, "utf-8");
|
|
447
|
+
const allLines = content.split("\n");
|
|
448
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
449
|
+
allLines.pop();
|
|
450
|
+
}
|
|
451
|
+
return allLines.slice(-lines);
|
|
452
|
+
} catch {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
386
457
|
private toProcessInfo(managed: ManagedProcess): ProcessInfo {
|
|
387
458
|
return {
|
|
388
459
|
id: managed.id,
|
|
@@ -403,3 +474,5 @@ export class ProcessManager {
|
|
|
403
474
|
};
|
|
404
475
|
}
|
|
405
476
|
}
|
|
477
|
+
|
|
478
|
+
export type { ProcessInfo, ProcessStatus, ManagerEvent, KillResult };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"pi",
|
|
10
10
|
"processes"
|
|
11
11
|
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/aliou/pi-extensions"
|
|
15
|
+
},
|
|
12
16
|
"pi": {
|
|
13
17
|
"extensions": [
|
|
14
18
|
"./index.ts"
|
|
@@ -32,4 +36,4 @@
|
|
|
32
36
|
"@mariozechner/pi-coding-agent": ">=0.49.0",
|
|
33
37
|
"@mariozechner/pi-tui": ">=0.49.0"
|
|
34
38
|
}
|
|
35
|
-
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
notifyOnSuccess?: boolean;
|
|
17
|
+
notifyOnFailure?: boolean;
|
|
18
|
+
notifyOnKill?: 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
|
+
}
|