@aliou/pi-processes 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,14 @@
1
+ import { ToolBody, ToolCallHeader } from "@aliou/pi-utils-ui";
2
+ import type {
3
+ AgentToolResult,
4
+ Theme,
5
+ ToolRenderResultOptions,
6
+ } from "@mariozechner/pi-coding-agent";
7
+ import { Text } from "@mariozechner/pi-tui";
1
8
  import { configLoader } from "../../config";
2
- import type { ExecuteResult } from "../../constants";
9
+ import type { ExecuteResult, ProcessesDetails } from "../../constants";
3
10
  import type { ProcessManager } from "../../manager";
4
- import { formatStatus, stripAnsi } from "../../utils";
11
+ import { formatStatus, hasAnsi, stripAnsi } from "../../utils";
5
12
 
6
13
  const MAX_BYTES = 50 * 1024; // 50KB
7
14
 
@@ -9,6 +16,118 @@ interface OutputParams {
9
16
  id?: string;
10
17
  }
11
18
 
19
+ export function renderOutputCall(
20
+ args: OutputParams,
21
+ theme: Theme,
22
+ ): ToolCallHeader {
23
+ return new ToolCallHeader(
24
+ {
25
+ toolName: "Process",
26
+ action: "output",
27
+ mainArg: args.id,
28
+ },
29
+ theme,
30
+ );
31
+ }
32
+
33
+ export function renderOutputResult(
34
+ result: AgentToolResult<ProcessesDetails>,
35
+ options: ToolRenderResultOptions,
36
+ theme: Theme,
37
+ ): ToolBody {
38
+ const { details } = result;
39
+
40
+ if (!details.output) {
41
+ return new ToolBody(
42
+ {
43
+ fields: [
44
+ {
45
+ label: "Error",
46
+ value: "Missing output details",
47
+ showCollapsed: true,
48
+ },
49
+ ],
50
+ },
51
+ options,
52
+ theme,
53
+ );
54
+ }
55
+
56
+ const lines: string[] = [theme.fg("muted", details.message)];
57
+ let hadAnsi = false;
58
+
59
+ if (details.output.stdout.length > 0) {
60
+ lines.push("", theme.fg("accent", "stdout:"));
61
+ for (const line of details.output.stdout.slice(-20)) {
62
+ if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
63
+ lines.push(stripAnsi(line));
64
+ }
65
+ if (details.output.stdout.length > 20) {
66
+ lines.push(
67
+ theme.fg(
68
+ "muted",
69
+ `... (${details.output.stdout.length - 20} more lines)`,
70
+ ),
71
+ );
72
+ }
73
+ }
74
+
75
+ if (details.output.stderr.length > 0) {
76
+ lines.push("", theme.fg("warning", "stderr:"));
77
+ for (const line of details.output.stderr.slice(-10)) {
78
+ if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
79
+ lines.push(theme.fg("warning", stripAnsi(line)));
80
+ }
81
+ if (details.output.stderr.length > 10) {
82
+ lines.push(
83
+ theme.fg(
84
+ "muted",
85
+ `... (${details.output.stderr.length - 10} more lines)`,
86
+ ),
87
+ );
88
+ }
89
+ }
90
+
91
+ if (details.logFiles) {
92
+ lines.push(
93
+ "",
94
+ theme.fg("success", "Log files:"),
95
+ ` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
96
+ ` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
97
+ );
98
+ }
99
+
100
+ if (hadAnsi) {
101
+ lines.push(
102
+ "",
103
+ theme.fg("muted", "ANSI escape codes were stripped from output"),
104
+ );
105
+ }
106
+
107
+ const fields: Array<
108
+ { label: string; value: string; showCollapsed?: boolean } | Text
109
+ > = [new Text(lines.join("\n"), 0, 0)];
110
+
111
+ // Collapsed summary
112
+ const previewSource =
113
+ details.output.stdout.length > 0
114
+ ? details.output.stdout
115
+ : details.output.stderr;
116
+ const preview = previewSource
117
+ .slice(-2)
118
+ .map((l) => stripAnsi(l))
119
+ .join("\n");
120
+ fields.push({
121
+ label: "Output",
122
+ value: preview
123
+ ? `${theme.fg("muted", preview)}`
124
+ : theme.fg("muted", "(empty)"),
125
+ showCollapsed: true,
126
+ });
127
+
128
+ return new ToolBody({ fields }, options, theme);
129
+ }
130
+
12
131
  export function executeOutput(
13
132
  params: OutputParams,
14
133
  manager: ProcessManager,
@@ -24,7 +143,7 @@ export function executeOutput(
24
143
  };
25
144
  }
26
145
 
27
- const proc = manager.find(params.id);
146
+ const proc = manager.get(params.id);
28
147
  if (!proc) {
29
148
  const message = `Process not found: ${params.id}`;
30
149
  return {
@@ -40,7 +159,7 @@ export function executeOutput(
40
159
  const { defaultTailLines } = configLoader.getConfig().output;
41
160
  const output = manager.getOutput(proc.id, defaultTailLines);
42
161
  if (!output) {
43
- const message = `Could not read output for: ${proc.id}`;
162
+ const message = `Could not read output for "${proc.name}" (${proc.id})`;
44
163
  return {
45
164
  content: [{ type: "text", text: message }],
46
165
  details: {
@@ -79,6 +198,12 @@ export function executeOutput(
79
198
  success: true,
80
199
  message,
81
200
  output,
201
+ logFiles: logFiles
202
+ ? {
203
+ stdoutFile: logFiles.stdoutFile,
204
+ stderrFile: logFiles.stderrFile,
205
+ }
206
+ : undefined,
82
207
  },
83
208
  };
84
209
  }
@@ -1,13 +1,123 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { ExecuteResult } from "../../constants";
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 { Text } from "@mariozechner/pi-tui";
9
+ import type { ExecuteResult, ProcessesDetails } from "../../constants";
3
10
  import type { ProcessManager } from "../../manager";
4
11
 
12
+ type WatchStream = "stdout" | "stderr" | "both";
13
+
14
+ interface StartLogWatch {
15
+ pattern: string;
16
+ stream?: WatchStream;
17
+ repeat?: boolean;
18
+ }
19
+
5
20
  interface StartParams {
6
21
  name?: string;
7
22
  command?: string;
8
23
  alertOnSuccess?: boolean;
9
24
  alertOnFailure?: boolean;
10
25
  alertOnKill?: boolean;
26
+ logWatches?: StartLogWatch[];
27
+ }
28
+
29
+ export function renderStartCall(
30
+ args: StartParams,
31
+ theme: Theme,
32
+ ): ToolCallHeader {
33
+ const longArgs: Array<{ label?: string; value: string }> = [];
34
+ const optionArgs: Array<{ label: string; value: string }> = [];
35
+ let mainArg: string | undefined;
36
+
37
+ if (args.name) {
38
+ mainArg = `"${args.name}"`;
39
+ }
40
+
41
+ if (args.command) {
42
+ if (!mainArg && args.command.length <= 60) {
43
+ mainArg = args.command;
44
+ } else if (args.command.length <= 60) {
45
+ optionArgs.push({ label: "command", value: args.command });
46
+ } else {
47
+ longArgs.push({ label: "command", value: args.command });
48
+ }
49
+ }
50
+
51
+ if (args.logWatches && args.logWatches.length > 0) {
52
+ optionArgs.push({
53
+ label: "watches",
54
+ value: String(args.logWatches.length),
55
+ });
56
+ }
57
+
58
+ return new ToolCallHeader(
59
+ {
60
+ toolName: "Process",
61
+ action: "start",
62
+ mainArg,
63
+ optionArgs,
64
+ longArgs,
65
+ },
66
+ theme,
67
+ );
68
+ }
69
+
70
+ export function renderStartResult(
71
+ result: AgentToolResult<ProcessesDetails>,
72
+ options: ToolRenderResultOptions,
73
+ theme: Theme,
74
+ ): ToolBody {
75
+ const { details } = result;
76
+ const process = details.process;
77
+
78
+ if (!process) {
79
+ return new ToolBody(
80
+ {
81
+ fields: [
82
+ {
83
+ label: "Error",
84
+ value: "Missing process details",
85
+ showCollapsed: true,
86
+ },
87
+ ],
88
+ },
89
+ options,
90
+ theme,
91
+ );
92
+ }
93
+
94
+ const fields: Array<
95
+ { label: string; value: string; showCollapsed?: boolean } | Text
96
+ > = [
97
+ new Text(
98
+ [
99
+ theme.fg("success", "Started process"),
100
+ ` name: ${theme.fg("accent", process.name)}`,
101
+ ` command: ${process.command}`,
102
+ ` id: ${theme.fg("accent", process.id)}`,
103
+ ` pid: ${String(process.pid)}`,
104
+ " Log files:",
105
+ ` - stdout: ${theme.fg("accent", process.stdoutFile)}`,
106
+ ` - stderr: ${theme.fg("accent", process.stderrFile)}`,
107
+ ].join("\n"),
108
+ 0,
109
+ 0,
110
+ ),
111
+ {
112
+ label: "Status",
113
+ value:
114
+ theme.fg("success", "Started") +
115
+ ` ${theme.fg("accent", `"${process.name}"`)} (${process.id}, PID: ${process.pid})`,
116
+ showCollapsed: true,
117
+ },
118
+ ];
119
+
120
+ return new ToolBody({ fields }, options, theme);
11
121
  }
12
122
 
13
123
  export function executeStart(
@@ -36,13 +146,47 @@ export function executeStart(
36
146
  };
37
147
  }
38
148
 
39
- const proc = manager.start(params.name, params.command, ctx.cwd, {
40
- alertOnSuccess: params.alertOnSuccess,
41
- alertOnFailure: params.alertOnFailure,
42
- alertOnKill: params.alertOnKill,
43
- });
149
+ const watchValidationError = validateLogWatches(params.logWatches);
150
+ if (watchValidationError) {
151
+ return {
152
+ content: [{ type: "text", text: watchValidationError }],
153
+ details: {
154
+ action: "start",
155
+ success: false,
156
+ message: watchValidationError,
157
+ },
158
+ };
159
+ }
160
+
161
+ let proc: ReturnType<ProcessManager["start"]>;
162
+ try {
163
+ proc = manager.start(params.name, params.command, ctx.cwd, {
164
+ alertOnSuccess: params.alertOnSuccess,
165
+ alertOnFailure: params.alertOnFailure,
166
+ alertOnKill: params.alertOnKill,
167
+ logWatches: params.logWatches,
168
+ });
169
+ } catch (error) {
170
+ const message =
171
+ error instanceof Error
172
+ ? `Invalid start options: ${error.message}`
173
+ : "Invalid start options";
174
+ return {
175
+ content: [{ type: "text", text: message }],
176
+ details: {
177
+ action: "start",
178
+ success: false,
179
+ message,
180
+ },
181
+ };
182
+ }
44
183
 
45
- const message = `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})\nLogs: ${proc.stdoutFile}`;
184
+ const message = [
185
+ `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})`,
186
+ "Log files:",
187
+ ` stdout: ${proc.stdoutFile}`,
188
+ ` stderr: ${proc.stderrFile}`,
189
+ ].join("\n");
46
190
  return {
47
191
  content: [{ type: "text", text: message }],
48
192
  details: {
@@ -53,3 +197,48 @@ export function executeStart(
53
197
  },
54
198
  };
55
199
  }
200
+
201
+ function validateLogWatches(watches?: StartLogWatch[]): string | null {
202
+ if (!watches) return null;
203
+
204
+ if (!Array.isArray(watches)) {
205
+ return "Invalid parameter: logWatches must be an array";
206
+ }
207
+
208
+ for (const [index, watch] of watches.entries()) {
209
+ if (!watch || typeof watch !== "object") {
210
+ return `Invalid logWatches[${index}]: expected an object`;
211
+ }
212
+
213
+ if (
214
+ typeof watch.pattern !== "string" ||
215
+ watch.pattern.trim().length === 0
216
+ ) {
217
+ return `Invalid logWatches[${index}].pattern: expected non-empty string`;
218
+ }
219
+
220
+ try {
221
+ // Validate regex syntax at process start for fast feedback.
222
+ new RegExp(watch.pattern);
223
+ } catch (error) {
224
+ const message =
225
+ error instanceof Error ? error.message : "invalid regular expression";
226
+ return `Invalid logWatches[${index}].pattern: ${message}`;
227
+ }
228
+
229
+ if (
230
+ watch.stream !== undefined &&
231
+ watch.stream !== "stdout" &&
232
+ watch.stream !== "stderr" &&
233
+ watch.stream !== "both"
234
+ ) {
235
+ return `Invalid logWatches[${index}].stream: expected stdout, stderr, or both`;
236
+ }
237
+
238
+ if (watch.repeat !== undefined && typeof watch.repeat !== "boolean") {
239
+ return `Invalid logWatches[${index}].repeat: expected boolean`;
240
+ }
241
+ }
242
+
243
+ return null;
244
+ }
@@ -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
 
@@ -7,6 +9,30 @@ interface WriteParams {
7
9
  end?: boolean;
8
10
  }
9
11
 
12
+ export function renderWriteCall(
13
+ args: WriteParams,
14
+ theme: Theme,
15
+ ): ToolCallHeader {
16
+ const optionArgs: Array<{ label: string; value: string }> = [];
17
+
18
+ if (args.input) {
19
+ optionArgs.push({ label: "input", value: args.input });
20
+ if (args.end) {
21
+ optionArgs.push({ label: "end", value: "true" });
22
+ }
23
+ }
24
+
25
+ return new ToolCallHeader(
26
+ {
27
+ toolName: "Process",
28
+ action: "write",
29
+ mainArg: args.id,
30
+ optionArgs,
31
+ },
32
+ theme,
33
+ );
34
+ }
35
+
10
36
  export function executeWrite(
11
37
  params: WriteParams,
12
38
  manager: ProcessManager,
@@ -35,7 +61,7 @@ export function executeWrite(
35
61
  };
36
62
  }
37
63
 
38
- const process = manager.find(id);
64
+ const process = manager.get(id);
39
65
  if (!process) {
40
66
  return {
41
67
  content: [{ type: "text", text: `Process not found: ${id}` }],
@@ -75,7 +101,7 @@ export function executeWrite(
75
101
  content: [
76
102
  {
77
103
  type: "text",
78
- text: `Wrote ${input.length} bytes to ${process.id}${suffix}`,
104
+ text: `Wrote ${input.length} bytes to "${process.name}" (${process.id})${suffix}`,
79
105
  },
80
106
  ],
81
107
  details: {