@haemmid/pi-processes 0.9.1 → 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 CHANGED
@@ -4,6 +4,19 @@ 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
+
7
20
  ## [0.9.1] - 2026-07-02
8
21
 
9
22
  ### Added
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
- Start `npm run dev -- --host 0.0.0.0` as `my-site:astro`.
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 start "npm run dev -- --host 0.0.0.0" name="my-site:astro"
108
- process output id="my-site:astro"
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 id="my-site:astro"
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,6 +171,8 @@ 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
@@ -184,13 +187,14 @@ process clear
184
187
 
185
188
  ### Field rules
186
189
 
187
- - `start`/`restart` require `command` and `name`.
190
+ - `start`/`ensure`/`restart` require `command` and `name`.
188
191
  - `output`, `logs`, and `kill` accept either `id` (exact process ID) or `name` (process name).
189
192
  - `kill` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
190
- - `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.
191
195
  - `restart` safely kills the existing process (awaited) before starting a new one.
192
196
  - `restart` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
193
- - `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).
194
198
  - `output`, `logs`, and `kill` return an error if both `id` and `name` are provided.
195
199
 
196
200
  ## Killing processes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haemmid/pi-processes",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Agent-facing background process manager for Pi and pi-web automation workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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":
@@ -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(
@@ -55,8 +56,9 @@ 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' — restart kills existing process first
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
60
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