@hienlh/ppm 0.5.3 → 0.5.5

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
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.5] - 2026-03-18
4
+
5
+ ### Fixed
6
+ - **Windows: PowerShell Start-Process fails with empty config arg** — filter empty strings from daemon spawn arguments
7
+
8
+ ## [0.5.4] - 2026-03-18
9
+
10
+ ### Fixed
11
+ - **Windows: daemon process dies when parent exits** — use PowerShell `Start-Process` for truly detached daemon and tunnel processes on Windows
12
+ - **Windows: `ppm status -a` / `stop -a` crash** — replaced `pgrep` (Unix-only) with `wmic` on Windows
13
+ - Daemon startup now verifies child process is alive after 500ms — shows clear error + suggests `-f` if daemon fails
14
+
3
15
  ## [0.5.3] - 2026-03-18
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -61,6 +61,25 @@ interface ProcessInfo {
61
61
 
62
62
  function findSystemProcesses(name: string): ProcessInfo[] {
63
63
  try {
64
+ if (process.platform === "win32") {
65
+ // Windows: use wmic to list processes matching name
66
+ const result = Bun.spawnSync(
67
+ ["wmic", "process", "where", `CommandLine like '%${name}%'`, "get", "ProcessId,CommandLine", "/format:csv"],
68
+ { stdout: "pipe", stderr: "ignore" },
69
+ );
70
+ const output = result.stdout.toString().trim();
71
+ if (!output) return [];
72
+ return output.split("\n").slice(1) // skip header
73
+ .map((line) => {
74
+ const parts = line.trim().split(",");
75
+ if (parts.length < 3) return null;
76
+ const pid = parseInt(parts[parts.length - 1]!, 10);
77
+ const cmdLine = parts.slice(1, -1).join(",");
78
+ return { pid, command: name, args: cmdLine };
79
+ })
80
+ .filter((p): p is ProcessInfo => p !== null && !isNaN(p.pid) && p.args.includes(name));
81
+ }
82
+ // macOS/Linux: use pgrep
64
83
  const result = Bun.spawnSync(["pgrep", "-afl", name], { stdout: "pipe", stderr: "ignore" });
65
84
  const output = result.stdout.toString().trim();
66
85
  if (!output) return [];
@@ -17,20 +17,37 @@ function killPid(pid: number, label: string): boolean {
17
17
  }
18
18
  }
19
19
 
20
- function killAllByName(name: string): number {
20
+ function findPidsByName(name: string): number[] {
21
21
  try {
22
+ if (process.platform === "win32") {
23
+ // Windows: use wmic to find processes
24
+ const result = Bun.spawnSync(
25
+ ["wmic", "process", "where", `CommandLine like '%${name}%'`, "get", "ProcessId", "/format:csv"],
26
+ { stdout: "pipe", stderr: "ignore" },
27
+ );
28
+ const output = result.stdout.toString().trim();
29
+ if (!output) return [];
30
+ return output.split("\n").slice(1)
31
+ .map((line) => parseInt(line.trim().split(",").pop() ?? "", 10))
32
+ .filter((pid) => !isNaN(pid) && pid !== process.pid);
33
+ }
34
+ // macOS/Linux: use pgrep
22
35
  const result = Bun.spawnSync(["pgrep", "-fl", name], { stdout: "pipe", stderr: "ignore" });
23
36
  const output = result.stdout.toString().trim();
24
- if (!output) return 0;
25
- const pids = output.split("\n")
37
+ if (!output) return [];
38
+ return output.split("\n")
26
39
  .map((line) => parseInt(line.trim(), 10))
27
40
  .filter((pid) => !isNaN(pid) && pid !== process.pid);
28
- let killed = 0;
29
- for (const pid of pids) {
30
- if (killPid(pid, `${name}`)) killed++;
31
- }
32
- return killed;
33
- } catch { return 0; }
41
+ } catch { return []; }
42
+ }
43
+
44
+ function killAllByName(name: string): number {
45
+ const pids = findPidsByName(name);
46
+ let killed = 0;
47
+ for (const pid of pids) {
48
+ if (killPid(pid, name)) killed++;
49
+ }
50
+ return killed;
34
51
  }
35
52
 
36
53
  export async function stopServer(options?: { all?: boolean }) {
@@ -254,14 +254,32 @@ export async function startServer(options: {
254
254
  const tunnelLog = resolve(ppmDir, "tunnel.log");
255
255
  // Truncate old log so we only match the new tunnel URL
256
256
  writeFs(tunnelLog, "");
257
- const tfd = openFd(tunnelLog, "a");
258
- const tunnelProc = Bun.spawn({
259
- cmd: [bin, "tunnel", "--url", `http://localhost:${port}`],
260
- stdio: ["ignore", "ignore", tfd],
261
- env: process.env,
262
- });
263
- tunnelProc.unref();
264
- tunnelPid = tunnelProc.pid;
257
+
258
+ if (process.platform === "win32") {
259
+ // Windows: use PowerShell for detached tunnel process
260
+ const psCmd = [
261
+ `$p = Start-Process -PassThru -WindowStyle Hidden`,
262
+ `-FilePath '${bin.replace(/\\/g, "\\\\")}'`,
263
+ `-ArgumentList 'tunnel','--url','http://localhost:${port}'`,
264
+ `-RedirectStandardError '${tunnelLog.replace(/\\/g, "\\\\")}'`,
265
+ `; Write-Output $p.Id`,
266
+ ].join(" ");
267
+ const result = Bun.spawnSync({
268
+ cmd: ["powershell", "-NoProfile", "-Command", psCmd],
269
+ stdout: "pipe", stderr: "pipe",
270
+ });
271
+ tunnelPid = parseInt(result.stdout.toString().trim(), 10);
272
+ if (isNaN(tunnelPid)) tunnelPid = null;
273
+ } else {
274
+ const tfd = openFd(tunnelLog, "a");
275
+ const tunnelProc = Bun.spawn({
276
+ cmd: [bin, "tunnel", "--url", `http://localhost:${port}`],
277
+ stdio: ["ignore", "ignore", tfd],
278
+ env: process.env,
279
+ });
280
+ tunnelProc.unref();
281
+ tunnelPid = tunnelProc.pid;
282
+ }
265
283
 
266
284
  // Parse URL from tunnel.log (poll stderr output)
267
285
  const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
@@ -283,22 +301,64 @@ export async function startServer(options: {
283
301
  const logFile = resolve(ppmDir, "ppm.log");
284
302
  const logFd = openSync(logFile, "a");
285
303
  const { resolve: resolvePath } = await import("node:path");
286
- const child = Bun.spawn({
287
- cmd: [
288
- process.execPath, "run", resolvePath(import.meta.dir, "index.ts"), "__serve__",
289
- String(port), host, options.config ?? "",
290
- ],
291
- stdio: ["ignore", logFd, logFd],
292
- env: process.env,
293
- });
294
- child.unref();
304
+ const script = resolvePath(import.meta.dir, "index.ts");
305
+ const args = ["__serve__", String(port), host, options.config ?? ""].filter(Boolean);
306
+
307
+ let childPid: number;
308
+
309
+ if (process.platform === "win32") {
310
+ // Windows: Bun.spawn child may die when parent exits (same job object).
311
+ // Use PowerShell Start-Process to create a truly detached process.
312
+ const bunExe = process.execPath.replace(/\\/g, "\\\\");
313
+ const argStr = ["run", script, ...args].map((a) => `'${a}'`).join(",");
314
+ const psCmd = [
315
+ `$p = Start-Process -PassThru -WindowStyle Hidden`,
316
+ `-FilePath '${bunExe}'`,
317
+ `-ArgumentList ${argStr}`,
318
+ `-RedirectStandardOutput '${logFile.replace(/\\/g, "\\\\")}'`,
319
+ `-RedirectStandardError '${logFile.replace(/\\/g, "\\\\")}'`,
320
+ `; Write-Output $p.Id`,
321
+ ].join(" ");
322
+ const result = Bun.spawnSync({
323
+ cmd: ["powershell", "-NoProfile", "-Command", psCmd],
324
+ stdout: "pipe",
325
+ stderr: "pipe",
326
+ });
327
+ childPid = parseInt(result.stdout.toString().trim(), 10);
328
+ if (isNaN(childPid)) {
329
+ console.error(" ✗ Failed to start daemon on Windows.");
330
+ console.error(` ${result.stderr.toString().trim()}`);
331
+ console.error(" Try: ppm start -f (foreground mode)");
332
+ process.exit(1);
333
+ }
334
+ } else {
335
+ // macOS/Linux: Bun.spawn + unref works fine
336
+ const child = Bun.spawn({
337
+ cmd: [process.execPath, "run", script, ...args],
338
+ stdio: ["ignore", logFd, logFd],
339
+ env: process.env,
340
+ });
341
+ child.unref();
342
+ childPid = child.pid;
343
+ }
344
+
345
+ // Verify daemon is alive after brief startup
346
+ await Bun.sleep(500);
347
+ let alive = false;
348
+ try { process.kill(childPid, 0); alive = true; } catch {}
349
+ if (!alive) {
350
+ console.error(" ✗ Daemon exited immediately after start.");
351
+ console.error(" Check logs: ppm logs");
352
+ console.error(" Or try: ppm start -f (foreground mode)");
353
+ process.exit(1);
354
+ }
295
355
 
296
356
  // Write status file with both PIDs
297
- const status = { pid: child.pid, port, host, shareUrl, tunnelPid };
357
+ const status = { pid: childPid, port, host, shareUrl, tunnelPid };
298
358
  writeFileSync(statusFile, JSON.stringify(status));
299
- writeFileSync(pidFile, String(child.pid));
359
+ writeFileSync(pidFile, String(childPid));
300
360
 
301
- console.log(`\n PPM v${VERSION} daemon started (PID: ${child.pid})\n`);
361
+ console.log(`\n PPM v${VERSION} daemon started (PID: ${childPid})\n`);
302
362
  console.log(` ➜ Local: http://localhost:${port}/`);
303
363
  if (shareUrl) {
304
364
  console.log(` ➜ Share: ${shareUrl}`);