@hienlh/ppm 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -0,0 +1,75 @@
1
+ import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { readFileSync, writeFileSync, existsSync, openSync } from "node:fs";
4
+
5
+ const STATUS_FILE = resolve(homedir(), ".ppm", "status.json");
6
+ const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
7
+
8
+ /** Restart only the server process, keeping the tunnel alive */
9
+ export async function restartServer(options: { config?: string }) {
10
+ if (!existsSync(STATUS_FILE)) {
11
+ console.log("No PPM daemon running. Use 'ppm start' instead.");
12
+ process.exit(1);
13
+ }
14
+
15
+ let status: Record<string, unknown>;
16
+ try {
17
+ status = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
18
+ } catch {
19
+ console.log("Corrupt status file. Use 'ppm stop && ppm start' instead.");
20
+ process.exit(1);
21
+ }
22
+
23
+ const serverPid = status.pid as number | undefined;
24
+ if (!serverPid) {
25
+ console.log("No server PID found. Use 'ppm stop && ppm start' instead.");
26
+ process.exit(1);
27
+ }
28
+
29
+ // Kill old server process
30
+ try {
31
+ process.kill(serverPid);
32
+ console.log(` Stopped server (PID: ${serverPid})`);
33
+ } catch {
34
+ console.log(` Server already stopped (PID: ${serverPid})`);
35
+ }
36
+
37
+ // Brief pause for port release
38
+ await Bun.sleep(500);
39
+
40
+ // Reload config for new server
41
+ const { configService } = await import("../../services/config.service.ts");
42
+ configService.load(options.config);
43
+ const port = status.port as number ?? configService.get("port");
44
+ const host = status.host as string ?? configService.get("host");
45
+
46
+ // Spawn new server child process
47
+ const ppmDir = resolve(homedir(), ".ppm");
48
+ const logFile = resolve(ppmDir, "ppm.log");
49
+ const logFd = openSync(logFile, "a");
50
+ const child = Bun.spawn({
51
+ cmd: [
52
+ process.execPath, "run",
53
+ resolve(import.meta.dir, "../../server/index.ts"), "__serve__",
54
+ String(port), host, options.config ?? "",
55
+ ],
56
+ stdio: ["ignore", logFd, logFd],
57
+ env: process.env,
58
+ });
59
+ child.unref();
60
+
61
+ // Update status with new server PID, keep tunnel info
62
+ status.pid = child.pid;
63
+ writeFileSync(STATUS_FILE, JSON.stringify(status));
64
+ writeFileSync(PID_FILE, String(child.pid));
65
+
66
+ const { VERSION } = await import("../../version.ts");
67
+ console.log(`\n PPM v${VERSION} restarted (PID: ${child.pid})\n`);
68
+ console.log(` ➜ Local: http://localhost:${port}/`);
69
+ if (status.shareUrl) {
70
+ console.log(` ➜ Share: ${status.shareUrl} (tunnel kept alive)`);
71
+ }
72
+ console.log();
73
+
74
+ process.exit(0);
75
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,15 @@ program
35
35
  await stopServer();
36
36
  });
37
37
 
38
+ program
39
+ .command("restart")
40
+ .description("Restart the server (keeps tunnel alive)")
41
+ .option("-c, --config <path>", "Path to config file")
42
+ .action(async (options) => {
43
+ const { restartServer } = await import("./cli/commands/restart.ts");
44
+ await restartServer(options);
45
+ });
46
+
38
47
  program
39
48
  .command("status")
40
49
  .description("Show PPM daemon status")
@@ -155,17 +155,12 @@ export async function startServer(options: {
155
155
  const { ensureCloudflared } = await import("../services/cloudflared.service.ts");
156
156
  const bin = await ensureCloudflared();
157
157
 
158
- // Check if tunnel already running (reuse from previous server crash)
158
+ // Kill any leftover tunnel from previous run
159
159
  if (existsSync(statusFile)) {
160
160
  try {
161
161
  const prev = JSON.parse(readFileSync(statusFile, "utf-8"));
162
- if (prev.tunnelPid && prev.shareUrl) {
163
- try {
164
- process.kill(prev.tunnelPid, 0); // Check alive
165
- console.log(` Reusing existing tunnel (PID: ${prev.tunnelPid})`);
166
- shareUrl = prev.shareUrl;
167
- tunnelPid = prev.tunnelPid;
168
- } catch { /* tunnel dead, spawn new one */ }
162
+ if (prev.tunnelPid) {
163
+ try { process.kill(prev.tunnelPid); } catch { /* already dead */ }
169
164
  }
170
165
  } catch {}
171
166
  }
@@ -173,8 +168,10 @@ export async function startServer(options: {
173
168
  // Spawn new tunnel if no existing one
174
169
  if (!shareUrl) {
175
170
  console.log(" Starting share tunnel...");
176
- const { openSync: openFd } = await import("node:fs");
171
+ const { openSync: openFd, writeFileSync: writeFs } = await import("node:fs");
177
172
  const tunnelLog = resolve(ppmDir, "tunnel.log");
173
+ // Truncate old log so we only match the new tunnel URL
174
+ writeFs(tunnelLog, "");
178
175
  const tfd = openFd(tunnelLog, "a");
179
176
  const tunnelProc = Bun.spawn({
180
177
  cmd: [bin, "tunnel", "--url", `http://localhost:${port}`],
@@ -231,6 +228,12 @@ export async function startServer(options: {
231
228
  qr.generate(shareUrl, { small: true });
232
229
  }
233
230
 
231
+ console.log(` Commands:`);
232
+ console.log(` ppm restart Reload config (keeps tunnel URL)`);
233
+ console.log(` ppm stop Stop server & tunnel`);
234
+ console.log(` ppm logs -f Follow server logs`);
235
+ console.log();
236
+
234
237
  process.exit(0);
235
238
  }
236
239