@hienlh/ppm 0.8.73 → 0.8.75

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,9 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## [0.8.73] - 2026-04-01
3
+ ## [0.8.75] - 2026-04-01
4
+
5
+ ### Changed
6
+ - **Tunnel always enabled**: `ppm start` now always starts Cloudflare tunnel — `--share` flag deprecated (still accepted, no-op)
7
+
8
+ ## [0.8.74] - 2026-04-01
4
9
 
5
10
  ### Fixed
6
- - **Cloud WS reconnect loop**: Stale WebSocket closure handlers from replaced connections no longer reset module state — prevents infinite reconnect cycle after upgrade/restart
11
+ - **Cloud WS reconnect loop**: Stale WebSocket closure handlers from replaced connections no longer reset module state
12
+ - **Cloud WS auth race**: Delay heartbeat/queue flush 500ms after auth to let server complete async DB auth — prevents 4002 rejection
7
13
 
8
14
  ## [0.8.72] - 2026-03-31
9
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.73",
3
+ "version": "0.8.75",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@ export function registerAutoStartCommands(program: Command): void {
10
10
  .command("enable")
11
11
  .description("Register PPM to start automatically on boot")
12
12
  .option("-p, --port <port>", "Override port")
13
- .option("-s, --share", "Enable Cloudflare tunnel on boot")
13
+ .option("-s, --share", "(deprecated) Tunnel is now always enabled")
14
14
  .option("-c, --config <path>", "Config file path")
15
15
  .option("--profile <name>", "DB profile name")
16
16
  .action(async (options) => {
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ program
16
16
  .command("start")
17
17
  .description("Start the PPM server (background by default)")
18
18
  .option("-p, --port <port>", "Port to listen on")
19
- .option("-s, --share", "Share via public URL (Cloudflare tunnel)")
19
+ .option("-s, --share", "(deprecated) Tunnel is now always enabled")
20
20
  .option("-c, --config <path>", "Path to config file (YAML import into DB)")
21
21
  .option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
22
22
  .action(async (options) => {
@@ -164,6 +164,9 @@ export async function startServer(options: {
164
164
  config?: string;
165
165
  profile?: string;
166
166
  }) {
167
+ // Tunnel always enabled — cloudflared shares the server publicly
168
+ options.share = true;
169
+
167
170
  // Load config
168
171
  configService.load(options.config);
169
172
  const port = parseInt(options.port ?? String(configService.get("port")), 10);
@@ -18,6 +18,7 @@ interface HeartbeatMsg extends WsMessage {
18
18
  tunnelUrl: string | null;
19
19
  state: string;
20
20
  appVersion: string;
21
+ availableVersion: string | null;
21
22
  serverPid: number | null;
22
23
  uptime: number;
23
24
  }
@@ -140,8 +141,10 @@ function doConnect(): void {
140
141
  sock.onopen = () => {
141
142
  if (ws !== sock) return; // stale — newer connection replaced us
142
143
  reconnecting = false;
144
+ reconnectAttempt = 0;
143
145
  log("INFO", "Cloud WS connected, sending auth");
144
146
 
147
+ // Send auth as first message — server must process this before any other msg
145
148
  sock.send(JSON.stringify({
146
149
  type: "auth",
147
150
  deviceId,
@@ -150,23 +153,28 @@ function doConnect(): void {
150
153
  version: 1,
151
154
  }));
152
155
 
153
- connected = true;
154
- reconnectAttempt = 0;
155
-
156
- // Flush queued messages
157
- while (outboundQueue.length > 0 && connected) {
158
- const msg = outboundQueue.shift()!;
159
- sock.send(JSON.stringify(msg));
160
- }
156
+ // Delay setting connected + sending heartbeat to let server process auth.
157
+ // Server's authenticateDevice() is async (DB lookup), so messages sent
158
+ // immediately after auth arrive before authenticated=true → 4002 reject.
159
+ setTimeout(() => {
160
+ if (ws !== sock) return; // replaced during delay
161
+ connected = true;
162
+
163
+ // Flush queued messages
164
+ while (outboundQueue.length > 0 && connected) {
165
+ const msg = outboundQueue.shift()!;
166
+ sock.send(JSON.stringify(msg));
167
+ }
161
168
 
162
- // Send immediate heartbeat
163
- if (getHeartbeatData) send(getHeartbeatData());
169
+ // Send immediate heartbeat
170
+ if (getHeartbeatData) send(getHeartbeatData());
164
171
 
165
- // Start periodic heartbeat
166
- if (heartbeatTimer) clearInterval(heartbeatTimer);
167
- heartbeatTimer = setInterval(() => {
168
- if (getHeartbeatData && connected) send(getHeartbeatData());
169
- }, HEARTBEAT_INTERVAL_MS);
172
+ // Start periodic heartbeat
173
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
174
+ heartbeatTimer = setInterval(() => {
175
+ if (getHeartbeatData && connected) send(getHeartbeatData());
176
+ }, HEARTBEAT_INTERVAL_MS);
177
+ }, 500); // 500ms for DB auth round-trip
170
178
  };
171
179
 
172
180
  sock.onmessage = (event) => {
@@ -456,6 +456,7 @@ async function connectCloud(opts: { port: number }, serverArgs: string[], logFd:
456
456
  tunnelUrl,
457
457
  state: supervisorState,
458
458
  appVersion: VERSION,
459
+ availableVersion: (readStatus().availableVersion as string) || null,
459
460
  serverPid: serverChild?.pid ?? null,
460
461
  uptime: Math.floor((Date.now() - startTime) / 1000),
461
462
  timestamp: new Date().toISOString(),