@haemmid/pi-processes 0.9.0 → 0.9.2
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/CHANGELOG.md +30 -0
- package/README.md +18 -11
- package/package.json +1 -1
- package/src/manager.ts +28 -0
- package/src/tools/actions/ensure.ts +87 -0
- package/src/tools/actions/index.ts +3 -0
- package/src/tools/actions/kill.ts +12 -42
- package/src/tools/actions/logs.ts +9 -30
- package/src/tools/actions/output.ts +14 -48
- package/src/tools/actions/restart.ts +7 -1
- package/src/tools/actions/start.ts +7 -1
- package/src/tools/actions/utils.ts +104 -0
- package/src/tools/index.ts +12 -6
- package/src/utils/ansi.ts +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning for public releases.
|
|
6
6
|
|
|
7
|
+
## [0.9.2] - 2026-07-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `ensure` action for idempotent dev-server workflows.
|
|
12
|
+
- `ensure` reuses existing process when name+command+cwd match.
|
|
13
|
+
- `ensure` returns conflict error when name matches but configuration differs.
|
|
14
|
+
- `ensure` returns next commands (output/logs/restart/kill by name).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Version bumped to 0.9.2.
|
|
19
|
+
|
|
20
|
+
## [0.9.1] - 2026-07-02
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `output`, `logs`, and `kill` accept either `id` or `name` as process selector.
|
|
25
|
+
- `start` and `restart` return suggested next commands with `name="..."`.
|
|
26
|
+
- Shared `resolveSelector` utility for consistent validation across actions.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Ambiguous name resolution now uses simpler error messages.
|
|
31
|
+
- Tool description updated to mention id/name duality.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `kill` test: message assertion updated for new ambiguous error format.
|
|
36
|
+
|
|
7
37
|
## [0.9.0] - 2026-07-01
|
|
8
38
|
|
|
9
39
|
Initial public release of `@haemmid/pi-processes`.
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ For Astro/Vite dev servers, ask the agent:
|
|
|
94
94
|
|
|
95
95
|
```text
|
|
96
96
|
Use the process tool for the Astro dev server.
|
|
97
|
-
|
|
97
|
+
Ensure `npm run dev -- --host 0.0.0.0` is running as `my-site:astro`.
|
|
98
98
|
Use `process output` when you need logs.
|
|
99
99
|
Do not restart after ordinary .astro, .ts, or .css edits.
|
|
100
100
|
Restart only after package/config/env changes or if the server exits.
|
|
@@ -104,10 +104,10 @@ Typical tool flow:
|
|
|
104
104
|
|
|
105
105
|
```text
|
|
106
106
|
process list
|
|
107
|
-
process
|
|
108
|
-
process output
|
|
107
|
+
process ensure "npm run dev -- --host 0.0.0.0" name="my-site:astro"
|
|
108
|
+
process output name="my-site:astro"
|
|
109
109
|
process restart "npm run dev -- --host 0.0.0.0" name="my-site:astro"
|
|
110
|
-
process kill
|
|
110
|
+
process kill name="my-site:astro"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
## Demo
|
|
@@ -156,7 +156,8 @@ The tool is named `process`.
|
|
|
156
156
|
|
|
157
157
|
| Action | Description |
|
|
158
158
|
|--------|-------------|
|
|
159
|
-
| `start` | Start a managed process. |
|
|
159
|
+
| `start` | Start a managed process (refuses if a live process with the same name exists). |
|
|
160
|
+
| `ensure` | Idempotent start: reuse existing process if name+command+cwd match, start new otherwise. |
|
|
160
161
|
| `restart` | Kill existing process and start a new one (safe: await kill → start). |
|
|
161
162
|
| `list` | List managed processes. |
|
|
162
163
|
| `output` | Return a one-off tailed stdout/stderr snapshot. |
|
|
@@ -170,25 +171,31 @@ The tool is named `process`.
|
|
|
170
171
|
process start "pnpm dev" name="backend-dev"
|
|
171
172
|
process start "pnpm test --watch" name="tests"
|
|
172
173
|
process start "pnpm dev" name="backend-dev" cwd="/path/to/project"
|
|
174
|
+
process ensure "pnpm dev" name="backend-dev"
|
|
175
|
+
process ensure "pnpm dev" name="backend-dev" cwd="/path/to/project"
|
|
173
176
|
process restart "pnpm dev" name="backend-dev"
|
|
174
177
|
process restart "pnpm dev" name="backend-dev" force=true
|
|
175
178
|
process list
|
|
176
|
-
process output id="
|
|
179
|
+
process output id="proc_1"
|
|
180
|
+
process output name="backend-dev"
|
|
177
181
|
process logs id="proc_1"
|
|
182
|
+
process logs name="backend-dev"
|
|
178
183
|
process kill id="backend-dev"
|
|
179
|
-
process kill
|
|
184
|
+
process kill name="backend-dev" force=true
|
|
180
185
|
process clear
|
|
181
186
|
```
|
|
182
187
|
|
|
183
188
|
### Field rules
|
|
184
189
|
|
|
185
|
-
- `start`/`restart` require `command` and `name`.
|
|
186
|
-
- `output`, `logs`, and `kill`
|
|
190
|
+
- `start`/`ensure`/`restart` require `command` and `name`.
|
|
191
|
+
- `output`, `logs`, and `kill` accept either `id` (exact process ID) or `name` (process name).
|
|
187
192
|
- `kill` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
|
|
188
|
-
- `start` refuses if a process with the same name is already running.
|
|
193
|
+
- `start` refuses if a live process with the same name is already running.
|
|
194
|
+
- `ensure` reuses existing process when name+command+cwd match; returns conflict error otherwise.
|
|
189
195
|
- `restart` safely kills the existing process (awaited) before starting a new one.
|
|
190
196
|
- `restart` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
|
|
191
|
-
- `start`/`restart` accept `cwd` to override the working directory (defaults to session cwd).
|
|
197
|
+
- `start`/`ensure`/`restart` accept `cwd` to override the working directory (defaults to session cwd).
|
|
198
|
+
- `output`, `logs`, and `kill` return an error if both `id` and `name` are provided.
|
|
192
199
|
|
|
193
200
|
## Killing processes
|
|
194
201
|
|
package/package.json
CHANGED
package/src/manager.ts
CHANGED
|
@@ -235,6 +235,34 @@ export class ProcessManager {
|
|
|
235
235
|
return this.toProcessInfo(managed);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Ensure a process is running with the given name, command, and cwd.
|
|
240
|
+
*
|
|
241
|
+
* - If a live process with the same name + cwd exists → return it.
|
|
242
|
+
* - If a live process with the same name exists but different cwd/command →
|
|
243
|
+
* return null (conflict).
|
|
244
|
+
* - If no live process with this name exists → start a new one.
|
|
245
|
+
* - Finished processes with the same name are ignored.
|
|
246
|
+
*/
|
|
247
|
+
ensure(name: string, command: string, cwd: string): ProcessInfo | null {
|
|
248
|
+
// Find live process with this name
|
|
249
|
+
const liveMatch = Array.from(this.processes.values()).find(
|
|
250
|
+
(proc) => proc.name === name && LIVE_STATUSES.has(proc.status),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (liveMatch) {
|
|
254
|
+
// Same name, same cwd → reuse
|
|
255
|
+
if (liveMatch.cwd === cwd && liveMatch.command === command) {
|
|
256
|
+
return this.toProcessInfo(liveMatch);
|
|
257
|
+
}
|
|
258
|
+
// Same name, different cwd/command → conflict
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No live match → start new
|
|
263
|
+
return this.start(name, command, cwd);
|
|
264
|
+
}
|
|
265
|
+
|
|
238
266
|
list(): ProcessInfo[] {
|
|
239
267
|
return Array.from(this.processes.values())
|
|
240
268
|
.map((p) => this.toProcessInfo(p))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExecuteResult } from "../../constants";
|
|
3
|
+
import type { ProcessManager } from "../../manager";
|
|
4
|
+
import { formatTimestamp, sanitizeLine } from "../../utils";
|
|
5
|
+
import { buildNextCommands } from "./utils";
|
|
6
|
+
|
|
7
|
+
interface EnsureParams {
|
|
8
|
+
name?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function executeEnsure(
|
|
14
|
+
params: EnsureParams,
|
|
15
|
+
manager: ProcessManager,
|
|
16
|
+
ctx: ExtensionContext,
|
|
17
|
+
): ExecuteResult {
|
|
18
|
+
if (!params.name) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: "Missing required parameter: name" }],
|
|
21
|
+
details: {
|
|
22
|
+
action: "ensure",
|
|
23
|
+
success: false,
|
|
24
|
+
message: "Missing required parameter: name",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!params.command) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: "Missing required parameter: command" }],
|
|
31
|
+
details: {
|
|
32
|
+
action: "ensure",
|
|
33
|
+
success: false,
|
|
34
|
+
message: "Missing required parameter: command",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const proc = manager.ensure(
|
|
40
|
+
params.name,
|
|
41
|
+
params.command,
|
|
42
|
+
params.cwd ?? ctx.cwd,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (proc === null) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `A process named "${params.name}" is already running with a different command or cwd. Use process kill first, or process restart to replace it.`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
details: {
|
|
54
|
+
action: "ensure",
|
|
55
|
+
success: false,
|
|
56
|
+
message: `A process named "${params.name}" is already running with a different configuration.`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const startedAt = formatTimestamp(proc.startTime);
|
|
62
|
+
|
|
63
|
+
// Check if this was an existing process or newly started
|
|
64
|
+
const isNew = proc.pid > 0;
|
|
65
|
+
const message = isNew
|
|
66
|
+
? [
|
|
67
|
+
`Started "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
|
|
68
|
+
`Started at: ${startedAt}`,
|
|
69
|
+
`Logs: ${proc.stdoutFile}`,
|
|
70
|
+
buildNextCommands(proc.name),
|
|
71
|
+
].join("\n")
|
|
72
|
+
: [
|
|
73
|
+
`Already running "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
|
|
74
|
+
`Reusing existing managed process.`,
|
|
75
|
+
buildNextCommands(proc.name),
|
|
76
|
+
].join("\n");
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: message }],
|
|
80
|
+
details: {
|
|
81
|
+
action: "ensure",
|
|
82
|
+
success: true,
|
|
83
|
+
message,
|
|
84
|
+
process: proc,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { ExecuteResult } from "../../constants";
|
|
3
3
|
import type { ProcessManager } from "../../manager";
|
|
4
4
|
import { executeClear } from "./clear";
|
|
5
|
+
import { executeEnsure } from "./ensure";
|
|
5
6
|
import { executeKill } from "./kill";
|
|
6
7
|
import { executeList } from "./list";
|
|
7
8
|
import { executeLogs } from "./logs";
|
|
@@ -26,6 +27,8 @@ export async function executeAction(
|
|
|
26
27
|
switch (params.action) {
|
|
27
28
|
case "start":
|
|
28
29
|
return executeStart(params, manager, ctx);
|
|
30
|
+
case "ensure":
|
|
31
|
+
return executeEnsure(params, manager, ctx);
|
|
29
32
|
case "restart":
|
|
30
33
|
return executeRestart(params, manager, ctx);
|
|
31
34
|
case "list":
|
|
@@ -1,66 +1,36 @@
|
|
|
1
1
|
import type { ExecuteResult } from "../../constants";
|
|
2
2
|
import type { ProcessManager } from "../../manager";
|
|
3
3
|
import { sanitizeLine } from "../../utils";
|
|
4
|
+
import { resolveSelector } from "./utils";
|
|
4
5
|
|
|
5
6
|
interface KillParams {
|
|
6
7
|
id?: string;
|
|
8
|
+
name?: string;
|
|
7
9
|
force?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
function notFoundResult(id: string): ExecuteResult {
|
|
11
|
-
const message = `Process not found: ${id}`;
|
|
12
|
-
return {
|
|
13
|
-
content: [{ type: "text", text: message }],
|
|
14
|
-
details: {
|
|
15
|
-
action: "kill",
|
|
16
|
-
success: false,
|
|
17
|
-
message,
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ambiguousResult(
|
|
23
|
-
id: string,
|
|
24
|
-
matches: Array<{ id: string; name: string }>,
|
|
25
|
-
): ExecuteResult {
|
|
26
|
-
const choices = matches
|
|
27
|
-
.map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
|
|
28
|
-
.join(", ");
|
|
29
|
-
const message =
|
|
30
|
-
`Process name is ambiguous: ${id}. ` +
|
|
31
|
-
`Use an exact process ID instead. Matches: ${choices}`;
|
|
32
|
-
return {
|
|
33
|
-
content: [{ type: "text", text: message }],
|
|
34
|
-
details: {
|
|
35
|
-
action: "kill",
|
|
36
|
-
success: false,
|
|
37
|
-
message,
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
12
|
export async function executeKill(
|
|
43
13
|
params: KillParams,
|
|
44
14
|
manager: ProcessManager,
|
|
45
15
|
): Promise<ExecuteResult> {
|
|
46
|
-
|
|
16
|
+
const error = resolveSelector(params, manager);
|
|
17
|
+
if (error) {
|
|
18
|
+
return { ...error, details: { ...error.details, action: "kill" } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const query = params.id || params.name || "";
|
|
22
|
+
const resolved = manager.resolve(query);
|
|
23
|
+
if (!resolved.ok) {
|
|
47
24
|
return {
|
|
48
|
-
content: [{ type: "text", text:
|
|
25
|
+
content: [{ type: "text", text: `Process not found: "${query}"` }],
|
|
49
26
|
details: {
|
|
50
27
|
action: "kill",
|
|
51
28
|
success: false,
|
|
52
|
-
message:
|
|
29
|
+
message: `Process not found: "${query}"`,
|
|
53
30
|
},
|
|
54
31
|
};
|
|
55
32
|
}
|
|
56
33
|
|
|
57
|
-
const resolved = manager.resolve(params.id);
|
|
58
|
-
if (!resolved.ok) {
|
|
59
|
-
return resolved.reason === "ambiguous"
|
|
60
|
-
? ambiguousResult(params.id, resolved.matches ?? [])
|
|
61
|
-
: notFoundResult(params.id);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
34
|
const proc = resolved.info;
|
|
65
35
|
const force = params.force ?? false;
|
|
66
36
|
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
@@ -1,52 +1,31 @@
|
|
|
1
1
|
import type { ExecuteResult } from "../../constants";
|
|
2
2
|
import type { ProcessManager } from "../../manager";
|
|
3
3
|
import { sanitizeLine } from "../../utils";
|
|
4
|
+
import { resolveSelector } from "./utils";
|
|
4
5
|
|
|
5
6
|
interface LogsParams {
|
|
6
7
|
id?: string;
|
|
8
|
+
name?: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function executeLogs(
|
|
10
12
|
params: LogsParams,
|
|
11
13
|
manager: ProcessManager,
|
|
12
14
|
): ExecuteResult {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
details: {
|
|
17
|
-
action: "logs",
|
|
18
|
-
success: false,
|
|
19
|
-
message: "Missing required parameter: id",
|
|
20
|
-
},
|
|
21
|
-
};
|
|
15
|
+
const error = resolveSelector(params, manager);
|
|
16
|
+
if (error) {
|
|
17
|
+
return { ...error, details: { ...error.details, action: "logs" } };
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
const
|
|
20
|
+
const query = params.id || params.name || "";
|
|
21
|
+
const resolved = manager.resolve(query);
|
|
25
22
|
if (!resolved.ok) {
|
|
26
|
-
if (resolved.reason === "ambiguous") {
|
|
27
|
-
const choices = (resolved.matches ?? [])
|
|
28
|
-
.map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
|
|
29
|
-
.join(", ");
|
|
30
|
-
const message =
|
|
31
|
-
`Process name is ambiguous: ${params.id}. ` +
|
|
32
|
-
`Use an exact process ID instead. Matches: ${choices}`;
|
|
33
|
-
return {
|
|
34
|
-
content: [{ type: "text", text: message }],
|
|
35
|
-
details: {
|
|
36
|
-
action: "logs",
|
|
37
|
-
success: false,
|
|
38
|
-
message,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const message = `Process not found: ${params.id}`;
|
|
44
23
|
return {
|
|
45
|
-
content: [{ type: "text", text:
|
|
24
|
+
content: [{ type: "text", text: `Process not found: "${query}"` }],
|
|
46
25
|
details: {
|
|
47
26
|
action: "logs",
|
|
48
27
|
success: false,
|
|
49
|
-
message
|
|
28
|
+
message: `Process not found: "${query}"`,
|
|
50
29
|
},
|
|
51
30
|
};
|
|
52
31
|
}
|
|
@@ -1,73 +1,39 @@
|
|
|
1
1
|
import { configLoader } from "../../config";
|
|
2
|
-
import {
|
|
3
|
-
type ExecuteResult,
|
|
4
|
-
LIVE_STATUSES,
|
|
5
|
-
type ResolveProcessResult,
|
|
6
|
-
} from "../../constants";
|
|
2
|
+
import { type ExecuteResult, LIVE_STATUSES } from "../../constants";
|
|
7
3
|
import type { ProcessManager } from "../../manager";
|
|
8
4
|
import { formatStatus, sanitizeLine } from "../../utils";
|
|
5
|
+
import { resolveSelector } from "./utils";
|
|
9
6
|
|
|
10
7
|
const MAX_BYTES = 50 * 1024; // 50KB
|
|
11
8
|
|
|
12
9
|
interface OutputParams {
|
|
13
10
|
id?: string;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function resolveProcessResult(
|
|
17
|
-
result: ResolveProcessResult,
|
|
18
|
-
action: "output" | "logs",
|
|
19
|
-
id: string,
|
|
20
|
-
): ExecuteResult | null {
|
|
21
|
-
if (result.ok) return null;
|
|
22
|
-
|
|
23
|
-
if (result.reason === "ambiguous") {
|
|
24
|
-
const choices = (result.matches ?? [])
|
|
25
|
-
.map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
|
|
26
|
-
.join(", ");
|
|
27
|
-
const message =
|
|
28
|
-
`Process name is ambiguous: ${id}. ` +
|
|
29
|
-
`Use an exact process ID instead. Matches: ${choices}`;
|
|
30
|
-
return {
|
|
31
|
-
content: [{ type: "text", text: message }],
|
|
32
|
-
details: {
|
|
33
|
-
action,
|
|
34
|
-
success: false,
|
|
35
|
-
message,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const message = `Process not found: ${id}`;
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text", text: message }],
|
|
43
|
-
details: {
|
|
44
|
-
action,
|
|
45
|
-
success: false,
|
|
46
|
-
message,
|
|
47
|
-
},
|
|
48
|
-
};
|
|
11
|
+
name?: string;
|
|
49
12
|
}
|
|
50
13
|
|
|
51
14
|
export function executeOutput(
|
|
52
15
|
params: OutputParams,
|
|
53
16
|
manager: ProcessManager,
|
|
54
17
|
): ExecuteResult {
|
|
55
|
-
|
|
18
|
+
const error = resolveSelector(params, manager);
|
|
19
|
+
if (error) {
|
|
20
|
+
return { ...error, details: { ...error.details, action: "output" } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const query = params.id || params.name || "";
|
|
24
|
+
const resolved = manager.resolve(query);
|
|
25
|
+
if (!resolved.ok) {
|
|
26
|
+
// Should not reach here, but guard anyway
|
|
56
27
|
return {
|
|
57
|
-
content: [{ type: "text", text:
|
|
28
|
+
content: [{ type: "text", text: `Process not found: "${query}"` }],
|
|
58
29
|
details: {
|
|
59
30
|
action: "output",
|
|
60
31
|
success: false,
|
|
61
|
-
message:
|
|
32
|
+
message: `Process not found: "${query}"`,
|
|
62
33
|
},
|
|
63
34
|
};
|
|
64
35
|
}
|
|
65
36
|
|
|
66
|
-
const resolved = manager.resolve(params.id);
|
|
67
|
-
if (!resolved.ok) {
|
|
68
|
-
return resolveProcessResult(resolved, "output", params.id) as ExecuteResult;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
37
|
const proc = resolved.info;
|
|
72
38
|
const { defaultTailLines } = configLoader.getConfig().output;
|
|
73
39
|
const output = manager.getOutput(proc.id, defaultTailLines);
|
|
@@ -3,6 +3,7 @@ import type { ExecuteResult } from "../../constants";
|
|
|
3
3
|
import type { ProcessManager } from "../../manager";
|
|
4
4
|
import { formatTimestamp, sanitizeLine } from "../../utils";
|
|
5
5
|
import { executeKill } from "./kill";
|
|
6
|
+
import { buildNextCommands } from "./utils";
|
|
6
7
|
|
|
7
8
|
interface RestartParams {
|
|
8
9
|
name?: string;
|
|
@@ -100,7 +101,12 @@ export async function executeRestart(
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const startedAt = formatTimestamp(proc.startTime);
|
|
103
|
-
const message =
|
|
104
|
+
const message = [
|
|
105
|
+
`Restarted "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
|
|
106
|
+
`Started at: ${startedAt}`,
|
|
107
|
+
`Logs: ${proc.stdoutFile}`,
|
|
108
|
+
buildNextCommands(proc.name),
|
|
109
|
+
].join("\n");
|
|
104
110
|
return {
|
|
105
111
|
content: [{ type: "text", text: message }],
|
|
106
112
|
details: {
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { ExecuteResult } from "../../constants";
|
|
3
3
|
import type { ProcessManager } from "../../manager";
|
|
4
4
|
import { formatTimestamp, sanitizeLine } from "../../utils";
|
|
5
|
+
import { buildNextCommands } from "./utils";
|
|
5
6
|
|
|
6
7
|
interface StartParams {
|
|
7
8
|
name?: string;
|
|
@@ -59,7 +60,12 @@ export function executeStart(
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const startedAt = formatTimestamp(proc.startTime);
|
|
62
|
-
const message =
|
|
63
|
+
const message = [
|
|
64
|
+
`Started "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
|
|
65
|
+
`Started at: ${startedAt}`,
|
|
66
|
+
`Logs: ${proc.stdoutFile}`,
|
|
67
|
+
buildNextCommands(proc.name),
|
|
68
|
+
].join("\n");
|
|
63
69
|
return {
|
|
64
70
|
content: [{ type: "text", text: message }],
|
|
65
71
|
details: {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExecuteResult } from "../../constants";
|
|
2
|
+
import type { ProcessManager } from "../../manager";
|
|
3
|
+
import { sanitizeLine } from "../../utils";
|
|
4
|
+
|
|
5
|
+
interface SelectorParams {
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResolvedProcess {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveSelector(
|
|
16
|
+
params: SelectorParams,
|
|
17
|
+
manager: ProcessManager,
|
|
18
|
+
): ExecuteResult | null {
|
|
19
|
+
// Missing selector
|
|
20
|
+
if (!params.id && !params.name) {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: 'Missing process selector. Use either id="proc_1" or name="backend-dev".',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
details: {
|
|
29
|
+
action: "output",
|
|
30
|
+
success: false,
|
|
31
|
+
message: "Missing process selector. Use either id or name.",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Both specified
|
|
37
|
+
if (params.id && params.name) {
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: "Use either id or name, not both.",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
details: {
|
|
46
|
+
action: "output",
|
|
47
|
+
success: false,
|
|
48
|
+
message: "Use either id or name, not both.",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Resolve by id or name
|
|
54
|
+
const query = params.id || params.name || "";
|
|
55
|
+
const resolved = manager.resolve(query);
|
|
56
|
+
|
|
57
|
+
if (resolved.ok) {
|
|
58
|
+
return null; // resolved successfully
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (resolved.reason === "ambiguous") {
|
|
62
|
+
const choices = (resolved.matches ?? [])
|
|
63
|
+
.map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
|
|
64
|
+
.join(", ");
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `Process name is ambiguous: "${query}". Use an exact process ID instead. Matches: ${choices}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
details: {
|
|
73
|
+
action: "output",
|
|
74
|
+
success: false,
|
|
75
|
+
message: `Process name is ambiguous: "${query}". Matches: ${choices}`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `Process not found: "${query}"`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
details: {
|
|
88
|
+
action: "output",
|
|
89
|
+
success: false,
|
|
90
|
+
message: `Process not found: "${query}"`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildNextCommands(name: string): string {
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
"Next commands:",
|
|
99
|
+
` process output name="${name}"`,
|
|
100
|
+
` process logs name="${name}"`,
|
|
101
|
+
` process restart "<command>" name="${name}"`,
|
|
102
|
+
` process kill name="${name}"`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ const ProcessesParams = Type.Object({
|
|
|
8
8
|
action: Type.Union(
|
|
9
9
|
[
|
|
10
10
|
Type.Literal("start"),
|
|
11
|
+
Type.Literal("ensure"),
|
|
11
12
|
Type.Literal("list"),
|
|
12
13
|
Type.Literal("output"),
|
|
13
14
|
Type.Literal("logs"),
|
|
@@ -17,7 +18,7 @@ const ProcessesParams = Type.Object({
|
|
|
17
18
|
],
|
|
18
19
|
{
|
|
19
20
|
description:
|
|
20
|
-
"Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate or force-kill), clear (remove finished), restart (kill existing and start new)",
|
|
21
|
+
"Action: start (run command), ensure (idempotent start), list (show all), output (get recent output), logs (get log file paths), kill (terminate or force-kill), clear (remove finished), restart (kill existing and start new)",
|
|
21
22
|
},
|
|
22
23
|
),
|
|
23
24
|
command: Type.Optional(
|
|
@@ -26,7 +27,7 @@ const ProcessesParams = Type.Object({
|
|
|
26
27
|
name: Type.Optional(
|
|
27
28
|
Type.String({
|
|
28
29
|
description:
|
|
29
|
-
"Friendly name for the process (required for start/restart
|
|
30
|
+
"Friendly name for the process (required for start/restart; accepted for output/logs/kill as alternative to id)",
|
|
30
31
|
}),
|
|
31
32
|
),
|
|
32
33
|
cwd: Type.Optional(
|
|
@@ -38,7 +39,7 @@ const ProcessesParams = Type.Object({
|
|
|
38
39
|
id: Type.Optional(
|
|
39
40
|
Type.String({
|
|
40
41
|
description:
|
|
41
|
-
"Exact process ID or
|
|
42
|
+
"Exact process ID (e.g. 'proc_1') or process name (for output/logs/kill as alternative to name)",
|
|
42
43
|
}),
|
|
43
44
|
),
|
|
44
45
|
force: Type.Optional(
|
|
@@ -55,12 +56,16 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
55
56
|
label: "Process",
|
|
56
57
|
description: `Manage background processes.
|
|
57
58
|
|
|
58
|
-
Actions: start, list, output, logs, kill, clear, restart.
|
|
59
|
-
- start/restart require 'name' and 'command'
|
|
60
|
-
-
|
|
59
|
+
Actions: start, ensure, list, output, logs, kill, clear, restart.
|
|
60
|
+
- start/restart/ensure require 'name' and 'command'
|
|
61
|
+
- ensure is an idempotent start: reuses existing process if name+command+cwd match
|
|
62
|
+
- output/logs/kill accept either 'id' (exact process ID) or 'name' (process name)
|
|
61
63
|
- kill supports optional 'force=true' for SIGKILL
|
|
62
64
|
- restart is preferred over start+kill: it safely awaits kill before starting new process
|
|
63
65
|
|
|
66
|
+
Processes have both an exact process id (e.g. proc_1) and a stable name (e.g. astro-dev).
|
|
67
|
+
For output/logs/kill, prefer name="..." when referring to a named dev server.
|
|
68
|
+
|
|
64
69
|
Processes continue in the background. Use process output or process logs for a one-off snapshot when you need to inspect status. Do not poll repeatedly just to wait.
|
|
65
70
|
Tool-triggered kills never notify.`,
|
|
66
71
|
promptSnippet:
|
|
@@ -71,6 +76,7 @@ Tool-triggered kills never notify.`,
|
|
|
71
76
|
"Use process output or process logs only for a one-off inspection, explicit user request, or debugging.",
|
|
72
77
|
"Use process restart to replace an existing process — it safely awaits kill before starting the new one.",
|
|
73
78
|
"Do not poll process output/list repeatedly just to wait for a process to finish.",
|
|
79
|
+
'For output/logs/kill, prefer name="..." when referring to a named dev server.',
|
|
74
80
|
],
|
|
75
81
|
|
|
76
82
|
parameters: ProcessesParams,
|
package/src/utils/ansi.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function stripAnsi(str: string): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Strip terminal control chars like carriage return/backspace that can
|
|
44
|
-
// corrupt
|
|
44
|
+
// corrupt output formatting.
|
|
45
45
|
return Array.from(clean)
|
|
46
46
|
.filter((char) => {
|
|
47
47
|
const code = char.codePointAt(0) ?? 0;
|
|
@@ -55,12 +55,11 @@ export function stripAnsi(str: string): string {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* Sanitize process output for single-line
|
|
58
|
+
* Sanitize process output for single-line display.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* alter Pi's surrounding TUI.
|
|
60
|
+
* After terminal escapes are removed, any remaining control bytes
|
|
61
|
+
* (carriage returns, backspaces, BEL, embedded newlines, etc.) are dropped
|
|
62
|
+
* so process output is safe for agent display.
|
|
64
63
|
*/
|
|
65
64
|
export function sanitizeLine(str: string): string {
|
|
66
65
|
return stripAnsi(str).replace(/\t/g, " ");
|