@aliou/pi-processes 0.6.3 → 0.7.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/CONTRIBUTING.md +123 -0
- package/README.md +133 -102
- package/package.json +8 -2
- package/skills/pi-processes/SKILL.md +39 -0
- package/src/commands/kill/command.ts +6 -6
- package/src/commands/logs/command.ts +1 -1
- package/src/commands/pin/command.ts +2 -1
- package/src/commands/settings/build-sections.ts +1 -1
- package/src/components/log-dock-component.ts +0 -11
- package/src/components/log-overlay-component.ts +0 -9
- package/src/components/processes-component.ts +32 -16
- package/src/config.ts +3 -3
- package/src/constants/index.ts +4 -0
- package/src/constants/types.ts +36 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/message-renderer.ts +35 -3
- package/src/hooks/process-end.ts +2 -0
- package/src/hooks/process-watch.ts +83 -0
- package/src/manager.test.ts +331 -0
- package/src/manager.ts +214 -30
- package/src/tools/actions/debug.ts +148 -0
- package/src/tools/actions/index.ts +105 -10
- package/src/tools/actions/kill.ts +15 -2
- package/src/tools/actions/list.ts +153 -2
- package/src/tools/actions/logs.ts +61 -3
- package/src/tools/actions/output.ts +129 -4
- package/src/tools/actions/start.ts +197 -8
- package/src/tools/actions/write.ts +28 -2
- package/src/tools/index.ts +101 -246
- package/src/utils/command-executor.ts +3 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/index.ts +7 -1
package/src/manager.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { Writable } from "node:stream";
|
|
|
14
14
|
import {
|
|
15
15
|
type KillResult,
|
|
16
16
|
LIVE_STATUSES,
|
|
17
|
+
type LogWatch,
|
|
18
|
+
type LogWatchStream,
|
|
17
19
|
type ManagerEvent,
|
|
18
20
|
type ProcessInfo,
|
|
19
21
|
type ProcessStatus,
|
|
@@ -23,12 +25,24 @@ import {
|
|
|
23
25
|
import { isProcessGroupAlive, killProcessGroup } from "./utils";
|
|
24
26
|
import { spawnCommand } from "./utils/command-executor";
|
|
25
27
|
|
|
28
|
+
interface ResolvedWatch {
|
|
29
|
+
index: number;
|
|
30
|
+
pattern: string;
|
|
31
|
+
regex: RegExp;
|
|
32
|
+
stream: LogWatchStream;
|
|
33
|
+
repeat: boolean;
|
|
34
|
+
fired: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
interface ManagedProcess extends ProcessInfo {
|
|
27
38
|
process: ChildProcess;
|
|
28
39
|
stdin: Writable | null;
|
|
29
40
|
stdinClosed: boolean;
|
|
30
41
|
lastSignalSent: NodeJS.Signals | null;
|
|
31
42
|
combinedFile: string;
|
|
43
|
+
stdoutPendingLine: string;
|
|
44
|
+
stderrPendingLine: string;
|
|
45
|
+
watches: ResolvedWatch[];
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
interface ProcessManagerOptions {
|
|
@@ -43,6 +57,9 @@ export class ProcessManager {
|
|
|
43
57
|
private watcher: ReturnType<typeof setInterval> | null = null;
|
|
44
58
|
private getConfiguredShellPath: () => string | undefined;
|
|
45
59
|
|
|
60
|
+
private lastOutputEmitAt: Map<string, number> = new Map();
|
|
61
|
+
private pendingOutputEmit: Map<string, NodeJS.Timeout> = new Map();
|
|
62
|
+
|
|
46
63
|
constructor(options?: ProcessManagerOptions) {
|
|
47
64
|
this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
|
|
48
65
|
mkdirSync(this.logDir, { recursive: true });
|
|
@@ -59,6 +76,48 @@ export class ProcessManager {
|
|
|
59
76
|
this.events.emit("event", event);
|
|
60
77
|
}
|
|
61
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
|
+
|
|
62
121
|
private transition(managed: ManagedProcess, next: ProcessStatus): void {
|
|
63
122
|
if (managed.status === next) return;
|
|
64
123
|
managed.status = next;
|
|
@@ -107,6 +166,9 @@ export class ProcessManager {
|
|
|
107
166
|
managed.endTime = Date.now();
|
|
108
167
|
}
|
|
109
168
|
|
|
169
|
+
this.flushPendingOutputChanged(managed.id);
|
|
170
|
+
this.flushPendingLines(managed);
|
|
171
|
+
|
|
110
172
|
if (managed.lastSignalSent) {
|
|
111
173
|
managed.success = false;
|
|
112
174
|
managed.exitCode = null;
|
|
@@ -125,6 +187,7 @@ export class ProcessManager {
|
|
|
125
187
|
cwd: string,
|
|
126
188
|
options?: StartOptions,
|
|
127
189
|
): ProcessInfo {
|
|
190
|
+
const resolvedWatches = this.resolveLogWatches(options?.logWatches);
|
|
128
191
|
const id = `proc_${++this.counter}`;
|
|
129
192
|
const stdoutFile = join(this.logDir, `${id}-stdout.log`);
|
|
130
193
|
const stderrFile = join(this.logDir, `${id}-stderr.log`);
|
|
@@ -159,6 +222,9 @@ export class ProcessManager {
|
|
|
159
222
|
stdin: child.stdin,
|
|
160
223
|
stdinClosed: false,
|
|
161
224
|
lastSignalSent: null,
|
|
225
|
+
stdoutPendingLine: "",
|
|
226
|
+
stderrPendingLine: "",
|
|
227
|
+
watches: resolvedWatches,
|
|
162
228
|
};
|
|
163
229
|
|
|
164
230
|
this.processes.set(id, managed);
|
|
@@ -179,15 +245,11 @@ export class ProcessManager {
|
|
|
179
245
|
child.stdout?.on("data", (data: Buffer) => {
|
|
180
246
|
try {
|
|
181
247
|
appendFileSync(stdoutFile, data);
|
|
182
|
-
const lines =
|
|
183
|
-
|
|
184
|
-
// or a partial line. We write all parts with the prefix and newline.
|
|
185
|
-
const tagged = lines
|
|
186
|
-
.map((line, i) =>
|
|
187
|
-
i < lines.length - 1 ? `1:${line}\n` : line ? `1:${line}\n` : "",
|
|
188
|
-
)
|
|
189
|
-
.join("");
|
|
248
|
+
const lines = this.extractCompleteLines(managed, "stdout", data);
|
|
249
|
+
const tagged = lines.map((line) => `1:${line}\n`).join("");
|
|
190
250
|
if (tagged) appendFileSync(combinedFile, tagged);
|
|
251
|
+
this.matchWatches(managed, "stdout", lines);
|
|
252
|
+
this.notifyOutputChanged(id);
|
|
191
253
|
} catch {
|
|
192
254
|
// Ignore
|
|
193
255
|
}
|
|
@@ -196,13 +258,11 @@ export class ProcessManager {
|
|
|
196
258
|
child.stderr?.on("data", (data: Buffer) => {
|
|
197
259
|
try {
|
|
198
260
|
appendFileSync(stderrFile, data);
|
|
199
|
-
const lines =
|
|
200
|
-
const tagged = lines
|
|
201
|
-
.map((line, i) =>
|
|
202
|
-
i < lines.length - 1 ? `2:${line}\n` : line ? `2:${line}\n` : "",
|
|
203
|
-
)
|
|
204
|
-
.join("");
|
|
261
|
+
const lines = this.extractCompleteLines(managed, "stderr", data);
|
|
262
|
+
const tagged = lines.map((line) => `2:${line}\n`).join("");
|
|
205
263
|
if (tagged) appendFileSync(combinedFile, tagged);
|
|
264
|
+
this.matchWatches(managed, "stderr", lines);
|
|
265
|
+
this.notifyOutputChanged(id);
|
|
206
266
|
} catch {
|
|
207
267
|
// Ignore
|
|
208
268
|
}
|
|
@@ -215,6 +275,9 @@ export class ProcessManager {
|
|
|
215
275
|
managed.endTime = Date.now();
|
|
216
276
|
managed.success = code === 0;
|
|
217
277
|
|
|
278
|
+
this.flushPendingOutputChanged(id);
|
|
279
|
+
this.flushPendingLines(managed);
|
|
280
|
+
|
|
218
281
|
if (signal) {
|
|
219
282
|
this.transition(managed, "killed");
|
|
220
283
|
} else {
|
|
@@ -233,6 +296,8 @@ export class ProcessManager {
|
|
|
233
296
|
managed.exitCode = -1;
|
|
234
297
|
managed.success = false;
|
|
235
298
|
managed.endTime = Date.now();
|
|
299
|
+
this.flushPendingOutputChanged(id);
|
|
300
|
+
this.flushPendingLines(managed);
|
|
236
301
|
this.transition(managed, "exited");
|
|
237
302
|
}
|
|
238
303
|
});
|
|
@@ -254,22 +319,6 @@ export class ProcessManager {
|
|
|
254
319
|
return managed ? this.toProcessInfo(managed) : null;
|
|
255
320
|
}
|
|
256
321
|
|
|
257
|
-
find(query: string): ProcessInfo | null {
|
|
258
|
-
const byId = this.processes.get(query);
|
|
259
|
-
if (byId) return this.toProcessInfo(byId);
|
|
260
|
-
|
|
261
|
-
const queryLower = query.toLowerCase();
|
|
262
|
-
for (const managed of this.processes.values()) {
|
|
263
|
-
if (managed.name.toLowerCase().includes(queryLower)) {
|
|
264
|
-
return this.toProcessInfo(managed);
|
|
265
|
-
}
|
|
266
|
-
if (managed.command.toLowerCase().includes(queryLower)) {
|
|
267
|
-
return this.toProcessInfo(managed);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
322
|
getOutput(
|
|
274
323
|
id: string,
|
|
275
324
|
tailLines = 100,
|
|
@@ -405,6 +454,8 @@ export class ProcessManager {
|
|
|
405
454
|
managed.success = false;
|
|
406
455
|
}
|
|
407
456
|
|
|
457
|
+
this.flushPendingOutputChanged(id);
|
|
458
|
+
this.flushPendingLines(managed);
|
|
408
459
|
this.transition(managed, "killed");
|
|
409
460
|
return { ok: true, info: this.toProcessInfo(managed) };
|
|
410
461
|
}
|
|
@@ -468,6 +519,7 @@ export class ProcessManager {
|
|
|
468
519
|
// Ignore
|
|
469
520
|
}
|
|
470
521
|
|
|
522
|
+
this.clearOutputChangedState(id);
|
|
471
523
|
this.processes.delete(id);
|
|
472
524
|
cleared++;
|
|
473
525
|
}
|
|
@@ -501,6 +553,12 @@ export class ProcessManager {
|
|
|
501
553
|
cleanup(): void {
|
|
502
554
|
this.stopWatcher();
|
|
503
555
|
|
|
556
|
+
for (const timeout of this.pendingOutputEmit.values()) {
|
|
557
|
+
clearTimeout(timeout);
|
|
558
|
+
}
|
|
559
|
+
this.pendingOutputEmit.clear();
|
|
560
|
+
this.lastOutputEmitAt.clear();
|
|
561
|
+
|
|
504
562
|
for (const p of this.processes.values()) {
|
|
505
563
|
if (!LIVE_STATUSES.has(p.status)) continue;
|
|
506
564
|
try {
|
|
@@ -531,6 +589,132 @@ export class ProcessManager {
|
|
|
531
589
|
}
|
|
532
590
|
}
|
|
533
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
|
+
|
|
534
718
|
private readTailLines(filePath: string, lines: number): string[] {
|
|
535
719
|
try {
|
|
536
720
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ToolCallHeader } from "@aliou/pi-utils-ui";
|
|
2
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { ExecuteResult, ProcessInfo } from "../../constants";
|
|
4
|
+
|
|
5
|
+
interface DebugParams {
|
|
6
|
+
preview?: "start" | "list" | "output" | "logs" | "error";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderDebugCall(
|
|
10
|
+
args: DebugParams,
|
|
11
|
+
theme: Theme,
|
|
12
|
+
): ToolCallHeader {
|
|
13
|
+
return new ToolCallHeader(
|
|
14
|
+
{
|
|
15
|
+
toolName: "Process",
|
|
16
|
+
action: "debug_preview",
|
|
17
|
+
mainArg: args.preview,
|
|
18
|
+
},
|
|
19
|
+
theme,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mockProcess(overrides?: Partial<ProcessInfo>): ProcessInfo {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
return {
|
|
26
|
+
id: "proc_42",
|
|
27
|
+
name: "demo-server",
|
|
28
|
+
pid: 4242,
|
|
29
|
+
command: "pnpm dev --port 3000",
|
|
30
|
+
cwd: "/tmp/demo",
|
|
31
|
+
startTime: now - 12_000,
|
|
32
|
+
endTime: null,
|
|
33
|
+
status: "running",
|
|
34
|
+
exitCode: null,
|
|
35
|
+
success: null,
|
|
36
|
+
stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
|
|
37
|
+
stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
|
|
38
|
+
alertOnSuccess: false,
|
|
39
|
+
alertOnFailure: true,
|
|
40
|
+
alertOnKill: false,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Temporary no-side-effect previews for process tool renderers.
|
|
47
|
+
* Remove before release.
|
|
48
|
+
*/
|
|
49
|
+
export function executeDebugPreview(params: DebugParams): ExecuteResult {
|
|
50
|
+
const preview = params.preview ?? "start";
|
|
51
|
+
|
|
52
|
+
if (preview === "start") {
|
|
53
|
+
const process = mockProcess();
|
|
54
|
+
const message = [
|
|
55
|
+
`Started "${process.name}" (${process.id}, PID: ${process.pid})`,
|
|
56
|
+
"Log files:",
|
|
57
|
+
` stdout: ${process.stdoutFile}`,
|
|
58
|
+
` stderr: ${process.stderrFile}`,
|
|
59
|
+
].join("\n");
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: message }],
|
|
62
|
+
details: {
|
|
63
|
+
action: "start",
|
|
64
|
+
success: true,
|
|
65
|
+
message,
|
|
66
|
+
process,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (preview === "list") {
|
|
72
|
+
const processes = [
|
|
73
|
+
mockProcess(),
|
|
74
|
+
mockProcess({
|
|
75
|
+
id: "proc_11",
|
|
76
|
+
name: "builder",
|
|
77
|
+
pid: 1011,
|
|
78
|
+
command: "pnpm build --watch",
|
|
79
|
+
}),
|
|
80
|
+
mockProcess({
|
|
81
|
+
id: "proc_10",
|
|
82
|
+
name: "tests",
|
|
83
|
+
pid: 1010,
|
|
84
|
+
command: "pnpm test",
|
|
85
|
+
status: "exited",
|
|
86
|
+
success: false,
|
|
87
|
+
endTime: Date.now() - 3_000,
|
|
88
|
+
exitCode: 1,
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: "Debug preview: list" }],
|
|
94
|
+
details: {
|
|
95
|
+
action: "list",
|
|
96
|
+
success: true,
|
|
97
|
+
message: "Debug preview: list",
|
|
98
|
+
processes,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (preview === "output") {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: "Debug preview: output" }],
|
|
106
|
+
details: {
|
|
107
|
+
action: "output",
|
|
108
|
+
success: true,
|
|
109
|
+
message:
|
|
110
|
+
'"demo-server" (proc_42) [running]: 4 stdout lines, 2 stderr lines',
|
|
111
|
+
output: {
|
|
112
|
+
status: "running",
|
|
113
|
+
stdout: [
|
|
114
|
+
"starting...",
|
|
115
|
+
"loading config",
|
|
116
|
+
"ready on http://localhost:3000",
|
|
117
|
+
"watching for changes",
|
|
118
|
+
],
|
|
119
|
+
stderr: [
|
|
120
|
+
"warn: deprecated option in config",
|
|
121
|
+
"error: simulated stack trace line",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
logFiles: {
|
|
125
|
+
stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
|
|
126
|
+
stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (preview === "logs") {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: "Debug preview: logs" }],
|
|
135
|
+
details: {
|
|
136
|
+
action: "logs",
|
|
137
|
+
success: true,
|
|
138
|
+
message: "Debug preview: logs",
|
|
139
|
+
logFiles: {
|
|
140
|
+
stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
|
|
141
|
+
stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error("Invalid logWatches[0].pattern: Unterminated group");
|
|
148
|
+
}
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { ToolBody, ToolCallHeader } from "@aliou/pi-utils-ui";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolResult,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
Theme,
|
|
6
|
+
ToolRenderResultOptions,
|
|
7
|
+
} from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
9
|
+
import type {
|
|
10
|
+
ExecuteResult,
|
|
11
|
+
ProcessAction,
|
|
12
|
+
ProcessesDetails,
|
|
13
|
+
} from "../../constants";
|
|
3
14
|
import type { ProcessManager } from "../../manager";
|
|
4
15
|
import { executeClear } from "./clear";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
16
|
+
import { executeDebugPreview, renderDebugCall } from "./debug";
|
|
17
|
+
import { executeKill, renderKillCall } from "./kill";
|
|
18
|
+
import { executeList, renderListResult } from "./list";
|
|
19
|
+
import { executeLogs, renderLogsCall, renderLogsResult } from "./logs";
|
|
20
|
+
import { executeOutput, renderOutputCall, renderOutputResult } from "./output";
|
|
21
|
+
import { executeStart, renderStartCall, renderStartResult } from "./start";
|
|
22
|
+
import { executeWrite, renderWriteCall } from "./write";
|
|
23
|
+
|
|
24
|
+
const DEBUG_PREVIEW_ENABLED = process.env.PI_PROCESSES_DEBUG_PREVIEW === "1";
|
|
11
25
|
|
|
12
26
|
interface ActionParams {
|
|
13
|
-
action: string;
|
|
27
|
+
action: ProcessAction | string;
|
|
14
28
|
command?: string;
|
|
15
29
|
name?: string;
|
|
16
30
|
id?: string;
|
|
@@ -19,6 +33,12 @@ interface ActionParams {
|
|
|
19
33
|
alertOnSuccess?: boolean;
|
|
20
34
|
alertOnFailure?: boolean;
|
|
21
35
|
alertOnKill?: boolean;
|
|
36
|
+
logWatches?: Array<{
|
|
37
|
+
pattern: string;
|
|
38
|
+
stream?: "stdout" | "stderr" | "both";
|
|
39
|
+
repeat?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
preview?: "start" | "list" | "output" | "logs" | "error";
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
export async function executeAction(
|
|
@@ -41,14 +61,89 @@ export async function executeAction(
|
|
|
41
61
|
return executeClear(manager);
|
|
42
62
|
case "write":
|
|
43
63
|
return executeWrite(params, manager);
|
|
64
|
+
case "debug_preview":
|
|
65
|
+
if (!DEBUG_PREVIEW_ENABLED) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Action 'debug_preview' is disabled. Set PI_PROCESSES_DEBUG_PREVIEW=1 to enable.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return executeDebugPreview(params);
|
|
44
71
|
default:
|
|
45
72
|
return {
|
|
46
73
|
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
|
47
74
|
details: {
|
|
48
|
-
action: params.action,
|
|
75
|
+
action: params.action as ProcessAction,
|
|
49
76
|
success: false,
|
|
50
77
|
message: `Unknown action: ${params.action}`,
|
|
51
78
|
},
|
|
52
79
|
};
|
|
53
80
|
}
|
|
54
81
|
}
|
|
82
|
+
|
|
83
|
+
export function renderActionCall(
|
|
84
|
+
args: ActionParams,
|
|
85
|
+
theme: Theme,
|
|
86
|
+
): Component | undefined {
|
|
87
|
+
switch (args.action) {
|
|
88
|
+
case "start":
|
|
89
|
+
return renderStartCall(args, theme);
|
|
90
|
+
case "output":
|
|
91
|
+
return renderOutputCall(args, theme);
|
|
92
|
+
case "logs":
|
|
93
|
+
return renderLogsCall(args, theme);
|
|
94
|
+
case "kill":
|
|
95
|
+
return renderKillCall(args, theme);
|
|
96
|
+
case "write":
|
|
97
|
+
return renderWriteCall(args, theme);
|
|
98
|
+
case "debug_preview":
|
|
99
|
+
return renderDebugCall(args, theme);
|
|
100
|
+
default:
|
|
101
|
+
return new ToolCallHeader(
|
|
102
|
+
{ toolName: "Process", action: args.action },
|
|
103
|
+
theme,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderActionResult(
|
|
109
|
+
result: AgentToolResult<ProcessesDetails>,
|
|
110
|
+
options: ToolRenderResultOptions,
|
|
111
|
+
theme: Theme,
|
|
112
|
+
): Component | undefined {
|
|
113
|
+
const { details } = result;
|
|
114
|
+
|
|
115
|
+
if (!details) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
switch (details.action) {
|
|
120
|
+
case "start":
|
|
121
|
+
return renderStartResult(result, options, theme);
|
|
122
|
+
case "list":
|
|
123
|
+
return renderListResult(result, options, theme);
|
|
124
|
+
case "output":
|
|
125
|
+
return renderOutputResult(result, options, theme);
|
|
126
|
+
case "logs":
|
|
127
|
+
return renderLogsResult(result, options, theme);
|
|
128
|
+
case "kill":
|
|
129
|
+
case "write":
|
|
130
|
+
case "clear":
|
|
131
|
+
case "debug_preview":
|
|
132
|
+
// Default rendering for these actions
|
|
133
|
+
return new ToolBody(
|
|
134
|
+
{
|
|
135
|
+
fields: [
|
|
136
|
+
{
|
|
137
|
+
label: "Result",
|
|
138
|
+
value: details.message,
|
|
139
|
+
showCollapsed: true,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
options,
|
|
144
|
+
theme,
|
|
145
|
+
);
|
|
146
|
+
default:
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ToolCallHeader } from "@aliou/pi-utils-ui";
|
|
2
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
1
3
|
import type { ExecuteResult } from "../../constants";
|
|
2
4
|
import type { ProcessManager } from "../../manager";
|
|
3
5
|
|
|
@@ -5,6 +7,17 @@ interface KillParams {
|
|
|
5
7
|
id?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
export function renderKillCall(args: KillParams, theme: Theme): ToolCallHeader {
|
|
11
|
+
return new ToolCallHeader(
|
|
12
|
+
{
|
|
13
|
+
toolName: "Process",
|
|
14
|
+
action: "kill",
|
|
15
|
+
mainArg: args.id,
|
|
16
|
+
},
|
|
17
|
+
theme,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export async function executeKill(
|
|
9
22
|
params: KillParams,
|
|
10
23
|
manager: ProcessManager,
|
|
@@ -20,7 +33,7 @@ export async function executeKill(
|
|
|
20
33
|
};
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
const proc = manager.
|
|
36
|
+
const proc = manager.get(params.id);
|
|
24
37
|
if (!proc) {
|
|
25
38
|
const message = `Process not found: ${params.id}`;
|
|
26
39
|
return {
|
|
@@ -53,7 +66,7 @@ export async function executeKill(
|
|
|
53
66
|
if (result.reason === "timeout") {
|
|
54
67
|
const message =
|
|
55
68
|
`SIGTERM timed out for "${proc.name}" (${proc.id}). ` +
|
|
56
|
-
"Run /
|
|
69
|
+
"Run /ps and press x on terminate_timeout to force kill (SIGKILL).";
|
|
57
70
|
return {
|
|
58
71
|
content: [{ type: "text", text: message }],
|
|
59
72
|
details: {
|