@hienlh/ppm 0.13.97 → 0.13.99
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 +11 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{audio-preview-C5XtLYr0.js → audio-preview-nh91uUVB.js} +1 -1
- package/dist/web/assets/{chat-tab-Crh2a5WT.js → chat-tab-DLDMRI1M.js} +3 -3
- package/dist/web/assets/{code-editor-D0n5yRzn.js → code-editor-xUzwLxcl.js} +2 -2
- package/dist/web/assets/{conflict-editor-C7tPFwQu.js → conflict-editor-DKOVCVyg.js} +1 -1
- package/dist/web/assets/{database-viewer-CENJQA63.js → database-viewer-QcgNoqBG.js} +1 -1
- package/dist/web/assets/{diff-viewer-DYskJYPt.js → diff-viewer-BZAwHro2.js} +1 -1
- package/dist/web/assets/{docx-preview-Cs1Vck_b.js → docx-preview-DdSlF2Px.js} +1 -1
- package/dist/web/assets/{extension-webview-DDNsAryv.js → extension-webview-CwyNu9aJ.js} +1 -1
- package/dist/web/assets/{git-log-panel-Bw50iGkP.js → git-log-panel-K47HJ4xO.js} +1 -1
- package/dist/web/assets/{glide-data-grid-D-kV0skS.js → glide-data-grid-DKe_vpg6.js} +1 -1
- package/dist/web/assets/{image-preview-ICmsfJXP.js → image-preview-DJH0O6Ke.js} +1 -1
- package/dist/web/assets/{index-lVDR594A.js → index-DujVI6Rg.js} +4 -4
- package/dist/web/assets/keybindings-store-Cu14oYyO.js +1 -0
- package/dist/web/assets/{markdown-renderer-VOyp6B1p.js → markdown-renderer-C1UYL3yn.js} +1 -1
- package/dist/web/assets/notification-store-Conv2TgB.js +1 -0
- package/dist/web/assets/{panel-store-BETELS-N.js → panel-store-DrDP7lP4.js} +1 -1
- package/dist/web/assets/{pdf-preview-DTlEFagS.js → pdf-preview-Bd4wJVlK.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-Coe4rUGI.js → port-forwarding-tab-2wR1sQE6.js} +1 -1
- package/dist/web/assets/{postgres-viewer-C9G-BZE8.js → postgres-viewer-Fb-C4lu5.js} +1 -1
- package/dist/web/assets/{settings-tab-BTEIHM07.js → settings-tab-DcFSxJEG.js} +1 -1
- package/dist/web/assets/{sql-query-editor-COQcgsYM.js → sql-query-editor-D39hN-d-.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D0pkAQQa.js → sqlite-viewer-u9iCbfHJ.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-VgYnDn6v.js → system-monitor-tab-DpndNYnK.js} +1 -1
- package/dist/web/assets/{tab-store-B_Kf29se.js → tab-store-Cr5X8BFJ.js} +1 -1
- package/dist/web/assets/{terminal-tab-D08UOpkI.js → terminal-tab-BIqqQjrG.js} +1 -1
- package/dist/web/assets/{video-preview-D5ufy0_E.js → video-preview-DdivKT9h.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/index.ts +19 -50
- package/src/server/ws/chat.ts +11 -0
- package/src/services/supervisor.ts +45 -28
- package/src/web/stores/panel-store.ts +5 -1
- package/dist/web/assets/keybindings-store-zLledTJ_.js +0 -1
- package/dist/web/assets/notification-store-XwGVhPdW.js +0 -1
package/src/server/index.ts
CHANGED
|
@@ -633,50 +633,35 @@ if (process.argv.includes("__serve__")) {
|
|
|
633
633
|
setInterval(() => cleanupOldProxyRequests(30), 24 * 60 * 60 * 1000);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
// On Windows
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
636
|
+
// On Windows the supervisor reaps the previous server's whole process tree
|
|
637
|
+
// before respawning, so the port is released cleanly. A lingering bind can
|
|
638
|
+
// still appear for a moment during an upgrade handoff, so wait for it to
|
|
639
|
+
// free instead of moving to a different port — shifting would split-brain
|
|
640
|
+
// the tunnel and supervisor, which still target the original port (this was
|
|
641
|
+
// the recurring "dies after upgrade" failure on Windows).
|
|
640
642
|
if (process.platform === "win32") {
|
|
641
|
-
const
|
|
643
|
+
const isPortInUse = () => new Promise<boolean>((resolve) => {
|
|
642
644
|
const net = require("node:net") as typeof import("node:net");
|
|
643
645
|
const tester = net.createServer()
|
|
644
646
|
.once("error", (e: NodeJS.ErrnoException) => resolve(e.code === "EADDRINUSE"))
|
|
645
647
|
.once("listening", () => tester.close(() => resolve(false)))
|
|
646
648
|
.listen(port, host);
|
|
647
649
|
});
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
console.warn(`[serve] Port ${port} held by dead process (PID: ${ownerPid}) — zombie socket`);
|
|
659
|
-
for (let candidate = port + 1; candidate <= port + 20; candidate++) {
|
|
660
|
-
const busy = await new Promise<boolean>((resolve) => {
|
|
661
|
-
const net = require("node:net") as typeof import("node:net");
|
|
662
|
-
const tester = net.createServer()
|
|
663
|
-
.once("error", (e: NodeJS.ErrnoException) => resolve(e.code === "EADDRINUSE"))
|
|
664
|
-
.once("listening", () => tester.close(() => resolve(false)))
|
|
665
|
-
.listen(candidate, host);
|
|
666
|
-
});
|
|
667
|
-
if (!busy) { actualPort = candidate; break; }
|
|
668
|
-
}
|
|
669
|
-
if (actualPort !== port) {
|
|
670
|
-
console.warn(`[serve] Auto-selected port ${actualPort} instead`);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} catch {}
|
|
650
|
+
const deadline = Date.now() + 10_000;
|
|
651
|
+
while (await isPortInUse()) {
|
|
652
|
+
if (Date.now() > deadline) {
|
|
653
|
+
console.error(`\n ✗ Port ${port} is still in use after waiting 10s.`);
|
|
654
|
+
console.error(` A stale process may be holding it. Run 'ppm stop', then start again.`);
|
|
655
|
+
console.error(` If it persists, run PowerShell as Admin: netsh int tcp reset (then restart).\n`);
|
|
656
|
+
process.exit(1); // supervisor will back off and respawn on the same port
|
|
657
|
+
}
|
|
658
|
+
console.warn(`[serve] Port ${port} still releasing, waiting...`);
|
|
659
|
+
await Bun.sleep(250);
|
|
675
660
|
}
|
|
676
661
|
}
|
|
677
662
|
|
|
678
663
|
const server = Bun.serve({
|
|
679
|
-
port
|
|
664
|
+
port,
|
|
680
665
|
hostname: host,
|
|
681
666
|
fetch(req, server) {
|
|
682
667
|
const url = new URL(req.url);
|
|
@@ -780,22 +765,6 @@ if (process.argv.includes("__serve__")) {
|
|
|
780
765
|
})
|
|
781
766
|
.catch(() => {});
|
|
782
767
|
|
|
783
|
-
// If we auto-selected a different port, update status.json so supervisor
|
|
784
|
-
// health checks and tunnel proxy point at the correct port.
|
|
785
|
-
if (actualPort !== port) {
|
|
786
|
-
try {
|
|
787
|
-
const { resolve: r } = await import("node:path");
|
|
788
|
-
const { readFileSync: rf, writeFileSync: wf, renameSync: rn } = await import("node:fs");
|
|
789
|
-
const { getPpmDir: gd } = await import("../services/ppm-dir.ts");
|
|
790
|
-
const sf = r(gd(), "status.json");
|
|
791
|
-
const st = JSON.parse(rf(sf, "utf-8"));
|
|
792
|
-
st.port = actualPort;
|
|
793
|
-
const tmp = sf + ".tmp." + process.pid;
|
|
794
|
-
wf(tmp, JSON.stringify(st));
|
|
795
|
-
rn(tmp, sf);
|
|
796
|
-
} catch {}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
768
|
// Graceful shutdown: close the listening socket so the port is released
|
|
800
769
|
const gracefulShutdown = () => {
|
|
801
770
|
try { server.stop(true); } catch {}
|
|
@@ -819,5 +788,5 @@ if (process.argv.includes("__serve__")) {
|
|
|
819
788
|
}, 200);
|
|
820
789
|
}
|
|
821
790
|
|
|
822
|
-
console.log(`Server child ready on port ${
|
|
791
|
+
console.log(`Server child ready on port ${port}`);
|
|
823
792
|
}
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -832,6 +832,17 @@ export const chatWebSocket = {
|
|
|
832
832
|
console.log(`[chat] session=${sessionId} FE disconnected (phase=${entry.phase}, clients=${entry.clients.size})`);
|
|
833
833
|
|
|
834
834
|
if (entry.clients.size === 0) {
|
|
835
|
+
// No clients listening anymore. If Claude is idle (turn finished, no pending
|
|
836
|
+
// approval), tear down the persistent streaming query so the next message
|
|
837
|
+
// recreates it via the resume path — picking up fresh MCP/config while
|
|
838
|
+
// preserving context from JSONL, and freeing the idle subprocess (RAM).
|
|
839
|
+
const provider = providerRegistry.get(entry.providerId);
|
|
840
|
+
const idle = entry.phase === "idle" && !entry.isStreamingActive && !entry.pendingApprovalEvent;
|
|
841
|
+
const hasLiveStream = provider?.hasStreamingSession?.(sessionId) ?? false;
|
|
842
|
+
if (idle && hasLiveStream) {
|
|
843
|
+
provider?.abortQuery?.(sessionId, "tab_closed");
|
|
844
|
+
logSessionEvent(sessionId, "INFO", "Streaming query torn down (all clients gone, idle) — next message resumes with fresh config");
|
|
845
|
+
}
|
|
835
846
|
startCleanupTimer(sessionId);
|
|
836
847
|
}
|
|
837
848
|
},
|
|
@@ -86,31 +86,45 @@ function backoffDelay(restartCount: number): number {
|
|
|
86
86
|
return Math.min(BACKOFF_BASE_MS * 2 ** (restartCount - 1), BACKOFF_MAX_MS);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// ───
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// ─── Process-tree kill (orphan-safe) ───────────────────────────────────
|
|
90
|
+
// The server child spawns grandchildren (Claude SDK subprocesses). On
|
|
91
|
+
// Windows a forced single-PID kill leaves those grandchildren alive; they
|
|
92
|
+
// keep the inherited TCP listening socket handle open, leaving the port in
|
|
93
|
+
// a zombie LISTENING state owned by a dead PID. Killing the whole tree
|
|
94
|
+
// releases the socket — the Windows analog of POSIX process-group kill.
|
|
95
|
+
function killProcessTree(pid: number): void {
|
|
96
|
+
if (process.platform === "win32") {
|
|
97
|
+
try {
|
|
98
|
+
const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
|
|
99
|
+
execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5000 });
|
|
100
|
+
} catch {
|
|
101
|
+
// Already dead, or taskkill unavailable — fall back to single-PID kill.
|
|
102
|
+
try { process.kill(pid, "SIGKILL"); } catch {}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Server shutdown ───────────────────────────────────────────────────
|
|
110
|
+
// On Windows the server's Claude SDK grandchildren are node-spawned (the
|
|
111
|
+
// provider forces `executable: "node"`), so they live OUTSIDE Bun's job
|
|
112
|
+
// object. If the server child exits gracefully on its own, those
|
|
113
|
+
// grandchildren orphan and keep the inherited listening socket open →
|
|
114
|
+
// zombie port. They can only be reaped by tree-killing WHILE the parent is
|
|
115
|
+
// still alive, so on Windows we tree-kill immediately rather than waiting
|
|
116
|
+
// for a graceful self-exit (which would let the orphans escape). This
|
|
117
|
+
// mirrors the POSIX process-group kill used on macOS/Linux.
|
|
92
118
|
function requestServerShutdown(child: Subprocess, timeoutMs: number = 2000): Promise<void> {
|
|
93
119
|
return new Promise<void>((resolve) => {
|
|
94
120
|
const pid = child.pid;
|
|
95
121
|
if (process.platform === "win32") {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const poll = setInterval(() => {
|
|
99
|
-
try { process.kill(pid, 0); } catch {
|
|
100
|
-
clearInterval(poll);
|
|
101
|
-
resolve();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (Date.now() > deadline) {
|
|
105
|
-
clearInterval(poll);
|
|
106
|
-
try { process.kill(pid, "SIGKILL"); } catch {}
|
|
107
|
-
resolve();
|
|
108
|
-
}
|
|
109
|
-
}, 100);
|
|
122
|
+
killProcessTree(pid);
|
|
123
|
+
resolve();
|
|
110
124
|
} else {
|
|
111
125
|
try { child.kill("SIGTERM"); } catch {}
|
|
112
126
|
setTimeout(() => {
|
|
113
|
-
|
|
127
|
+
killProcessTree(pid);
|
|
114
128
|
resolve();
|
|
115
129
|
}, timeoutMs).unref();
|
|
116
130
|
resolve();
|
|
@@ -351,10 +365,11 @@ function startServerHealthCheck(port: number) {
|
|
|
351
365
|
log("WARN", `Server unresponsive (${healthFailCount} failures), killing`);
|
|
352
366
|
const pid = serverChild.pid;
|
|
353
367
|
if (process.platform === "win32") {
|
|
354
|
-
|
|
368
|
+
killProcessTree(pid);
|
|
369
|
+
} else {
|
|
370
|
+
try { serverChild.kill("SIGTERM"); } catch {}
|
|
371
|
+
setTimeout(() => { killProcessTree(pid); }, 1000).unref();
|
|
355
372
|
}
|
|
356
|
-
try { serverChild.kill("SIGTERM"); } catch {}
|
|
357
|
-
setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 1000).unref();
|
|
358
373
|
healthFailCount = 0;
|
|
359
374
|
// spawnServer loop handles respawn via exited promise
|
|
360
375
|
}
|
|
@@ -519,9 +534,10 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
519
534
|
}
|
|
520
535
|
|
|
521
536
|
// ── Non-systemd path: spawn new supervisor directly (macOS/Windows) ─
|
|
522
|
-
// Poll until port is actually free (max 10s) — never guess with fixed sleep
|
|
523
|
-
//
|
|
524
|
-
//
|
|
537
|
+
// Poll until port is actually free (max 10s) — never guess with fixed sleep.
|
|
538
|
+
// The tree-kill above already reaped the server's grandchildren, so the
|
|
539
|
+
// listening socket is released; this loop just waits for the OS to finish
|
|
540
|
+
// tearing it down before the new supervisor binds.
|
|
525
541
|
const portFreeStart = Date.now();
|
|
526
542
|
const portTimeout = process.platform === "win32" ? 3_000 : 10_000;
|
|
527
543
|
while (Date.now() - portFreeStart < portTimeout) {
|
|
@@ -860,12 +876,13 @@ export function shutdown() {
|
|
|
860
876
|
|
|
861
877
|
if (serverChild) {
|
|
862
878
|
log("INFO", `Killing server child (PID: ${serverChild.pid})`);
|
|
879
|
+
const pid = serverChild.pid;
|
|
863
880
|
if (process.platform === "win32") {
|
|
864
|
-
|
|
881
|
+
killProcessTree(pid);
|
|
882
|
+
} else {
|
|
883
|
+
try { serverChild.kill("SIGTERM"); } catch {}
|
|
884
|
+
setTimeout(() => { killProcessTree(pid); }, 2000).unref();
|
|
865
885
|
}
|
|
866
|
-
const pid = serverChild.pid;
|
|
867
|
-
try { serverChild.kill("SIGTERM"); } catch {}
|
|
868
|
-
setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 2000).unref();
|
|
869
886
|
}
|
|
870
887
|
if (tunnelChild) {
|
|
871
888
|
log("INFO", `Killing tunnel child (PID: ${tunnelChild.pid})`);
|
|
@@ -345,7 +345,11 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
345
345
|
if (newTabs.length === 0 && gridPanelCount > 1) {
|
|
346
346
|
const { [pid]: _, ...rest } = s.panels;
|
|
347
347
|
const newGrid = gridRemovePanel(s.grid, pid);
|
|
348
|
-
|
|
348
|
+
// Focus must land on a panel still in the grid — Object.keys(rest)
|
|
349
|
+
// can return keep-alive panels from other projects (off-grid).
|
|
350
|
+
const newFocused = s.focusedPanelId === pid
|
|
351
|
+
? (newGrid.flat()[0] ?? Object.keys(rest)[0]!)
|
|
352
|
+
: s.focusedPanelId;
|
|
349
353
|
return { panels: rest, grid: newGrid, focusedPanelId: newFocused };
|
|
350
354
|
}
|
|
351
355
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./vendor-markdown-0Mxgxy0L.js";import"./api-client-bbJLzRVE.js";import{L as e}from"./index-lVDR594A.js";export{e as useKeybindingsStore};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./vendor-markdown-0Mxgxy0L.js";import"./api-client-bbJLzRVE.js";import{W as e}from"./index-lVDR594A.js";export{e as useNotificationStore};
|