@hienlh/ppm 0.7.22 → 0.7.23

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.23] - 2026-03-22
4
+
5
+ ### Fixed
6
+ - **Restart from PPM terminal**: restart command now spawns a detached worker process so it survives when the old server (and its terminals) are killed — previously running `ppm restart` inside a PPM terminal would kill the restart process itself before the new server could be spawned, causing persistent 502
7
+ - **Restart server script path**: save server entry script path in `status.json` during `ppm start` so restart always uses the stable installed location instead of a potentially ephemeral `bunx` cache path
8
+
3
9
  ## [0.7.22] - 2026-03-22
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.7.22",
3
+ "version": "0.7.23",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { homedir } from "node:os";
3
- import { readFileSync, writeFileSync, existsSync, openSync, unlinkSync } from "node:fs";
3
+ import { readFileSync, writeFileSync, existsSync, openSync } from "node:fs";
4
4
 
5
5
  const PPM_DIR = resolve(homedir(), ".ppm");
6
6
  const STATUS_FILE = resolve(PPM_DIR, "status.json");
@@ -28,62 +28,121 @@ export async function restartServer(options: { config?: string }) {
28
28
  process.exit(1);
29
29
  }
30
30
 
31
+ // Resolve server script: prefer saved path (stable install), fall back to import.meta.dir
32
+ const savedScript = status.serverScript as string | undefined;
33
+ const serverScript = savedScript && existsSync(savedScript)
34
+ ? savedScript
35
+ : resolve(import.meta.dir, "../../server/index.ts");
36
+
37
+ const { configService } = await import("../../services/config.service.ts");
38
+ configService.load(options.config);
39
+ const port = status.port as number ?? configService.get("port");
40
+ const host = status.host as string ?? configService.get("host");
41
+
31
42
  // Write restarting flag so tunnel cleanup handler skips killing cloudflared
32
43
  writeFileSync(RESTARTING_FLAG, "");
33
44
 
34
- // Kill old server process
35
- try {
36
- process.kill(serverPid);
37
- console.log(` Stopped server (PID: ${serverPid})`);
38
- } catch {
39
- console.log(` Server already stopped (PID: ${serverPid})`);
40
- }
45
+ // Generate a self-contained restart worker script.
46
+ // This runs as a detached process so it survives even if the current process
47
+ // (and the PPM server hosting its terminal) is killed.
48
+ const params = JSON.stringify({
49
+ serverPid, port, host, serverScript,
50
+ config: options.config ?? "",
51
+ statusFile: STATUS_FILE,
52
+ pidFile: PID_FILE,
53
+ restartingFlag: RESTARTING_FLAG,
54
+ ppmDir: PPM_DIR,
55
+ });
41
56
 
42
- // Brief pause for port release
43
- await Bun.sleep(500);
57
+ const workerPath = resolve(PPM_DIR, ".restart-worker.ts");
58
+ writeFileSync(workerPath, `
59
+ import { readFileSync, writeFileSync, openSync, unlinkSync, appendFileSync, existsSync } from "node:fs";
60
+ import { createServer } from "node:net";
44
61
 
45
- // Set DB profile before loading config
46
- const { setDbProfile } = await import("../../services/db.service.ts");
47
- if (options.config && /dev/i.test(options.config)) {
48
- setDbProfile("dev");
49
- }
62
+ const P = ${params};
50
63
 
51
- // Reload config for new server
52
- const { configService } = await import("../../services/config.service.ts");
53
- configService.load(options.config);
54
- const port = status.port as number ?? configService.get("port");
55
- const host = status.host as string ?? configService.get("host");
64
+ async function main() {
65
+ const log = (level: string, msg: string) => {
66
+ const ts = new Date().toISOString();
67
+ try { appendFileSync(P.ppmDir + "/ppm.log", "[" + ts + "] [" + level + "] " + msg + "\\n"); } catch {}
68
+ };
69
+
70
+ // Kill old server
71
+ try { process.kill(P.serverPid); log("INFO", "Restart: killed old server PID " + P.serverPid); } catch {}
56
72
 
57
- // Spawn new server child process
58
- const ppmDir = resolve(homedir(), ".ppm");
59
- const logFile = resolve(ppmDir, "ppm.log");
60
- const logFd = openSync(logFile, "a");
73
+ // Wait for port to be free (up to 5s)
74
+ const start = Date.now();
75
+ while (Date.now() - start < 5000) {
76
+ const inUse: boolean = await new Promise((res) => {
77
+ const t = createServer()
78
+ .once("error", () => res(true))
79
+ .once("listening", () => { t.close(() => res(false)); })
80
+ .listen(P.port, P.host);
81
+ });
82
+ if (!inUse) break;
83
+ await Bun.sleep(200);
84
+ }
85
+
86
+ // Spawn new server
87
+ const logFd = openSync(P.ppmDir + "/ppm.log", "a");
61
88
  const child = Bun.spawn({
62
- cmd: [
63
- process.execPath, "run",
64
- resolve(import.meta.dir, "../../server/index.ts"), "__serve__",
65
- String(port), host, options.config ?? "",
66
- ],
89
+ cmd: [process.execPath, "run", P.serverScript, "__serve__", String(P.port), P.host, P.config],
67
90
  stdio: ["ignore", logFd, logFd],
68
91
  env: process.env,
69
92
  });
70
93
  child.unref();
71
94
 
72
- // Update status with new server PID, keep tunnel info
73
- status.pid = child.pid;
74
- writeFileSync(STATUS_FILE, JSON.stringify(status));
75
- writeFileSync(PID_FILE, String(child.pid));
95
+ // Update status.json with new PID, keep tunnel info
96
+ try {
97
+ const status = JSON.parse(readFileSync(P.statusFile, "utf-8"));
98
+ status.pid = child.pid;
99
+ status.serverScript = P.serverScript;
100
+ writeFileSync(P.statusFile, JSON.stringify(status));
101
+ writeFileSync(P.pidFile, String(child.pid));
102
+ } catch {}
76
103
 
77
104
  // Remove restarting flag
78
- try { unlinkSync(RESTARTING_FLAG); } catch {}
105
+ try { unlinkSync(P.restartingFlag); } catch {}
79
106
 
80
- const { VERSION } = await import("../../version.ts");
81
- console.log(`\n PPM v${VERSION} restarted (PID: ${child.pid})\n`);
82
- console.log(` ➜ Local: http://localhost:${port}/`);
83
- if (status.shareUrl) {
84
- console.log(` ➜ Share: ${status.shareUrl} (tunnel kept alive)`);
107
+ // Health check (up to 10s)
108
+ let ready = false;
109
+ const hStart = Date.now();
110
+ while (Date.now() - hStart < 10000) {
111
+ try {
112
+ const res = await fetch("http://127.0.0.1:" + P.port + "/api/health", { signal: AbortSignal.timeout(1000) });
113
+ if (res.ok) { ready = true; break; }
114
+ } catch {}
115
+ await Bun.sleep(300);
85
116
  }
86
- console.log();
117
+
118
+ if (ready) {
119
+ log("INFO", "Restart complete — new server PID " + child.pid);
120
+ } else {
121
+ let alive = false;
122
+ try { process.kill(child.pid, 0); alive = true; } catch {}
123
+ log("ERROR", "Restart failed — server " + (alive ? "not responding on port " + P.port : "crashed on startup"));
124
+ }
125
+
126
+ // Cleanup worker file
127
+ try { unlinkSync(P.ppmDir + "/.restart-worker.ts"); } catch {}
128
+ process.exit(0);
129
+ }
130
+
131
+ main();
132
+ `);
133
+
134
+ // Spawn worker as a fully detached process
135
+ const logFd = openSync(resolve(PPM_DIR, "ppm.log"), "a");
136
+ const worker = Bun.spawn({
137
+ cmd: [process.execPath, "run", workerPath],
138
+ stdio: ["ignore", logFd, logFd],
139
+ env: process.env,
140
+ });
141
+ worker.unref();
142
+
143
+ const { VERSION } = await import("../../version.ts");
144
+ console.log(`\n PPM v${VERSION} restarting... (worker PID: ${worker.pid})`);
145
+ console.log(` Server will restart in background. Check: ppm status\n`);
87
146
 
88
147
  process.exit(0);
89
148
  }
@@ -293,8 +293,8 @@ export async function startServer(options: {
293
293
  process.exit(1);
294
294
  }
295
295
 
296
- // Write status file with both PIDs
297
- const status = { pid: childPid, port, host, shareUrl, tunnelPid };
296
+ // Write status file with both PIDs + server script path for restart
297
+ const status = { pid: childPid, port, host, shareUrl, tunnelPid, serverScript: script };
298
298
  writeFileSync(statusFile, JSON.stringify(status));
299
299
  writeFileSync(pidFile, String(childPid));
300
300