pi-web-ui 0.8.2 → 0.8.4
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/bin/pi-web-ui.mjs +1 -1
- package/dist/server/agent-service.js +16 -1
- package/dist/server/index.js +70 -0
- package/dist/server/terminals.js +7 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-BmacHhe9.js → index-CqRE1kpZ.js} +2 -2
- package/web/dist/assets/{index-BOlcuvo2.css → index-Ton8iALZ.css} +1 -1
- package/web/dist/index.html +2 -2
package/bin/pi-web-ui.mjs
CHANGED
|
@@ -946,6 +946,11 @@ export class ClientSession {
|
|
|
946
946
|
}
|
|
947
947
|
/** True once updateApp succeeded — the process must restart to run new code. */
|
|
948
948
|
pendingRestart = false;
|
|
949
|
+
/**
|
|
950
|
+
* Set by index.ts: called after a successful self-update; returns whether
|
|
951
|
+
* the process is going to restart itself (so the notice can say so).
|
|
952
|
+
*/
|
|
953
|
+
onUpdateReady = undefined;
|
|
949
954
|
/** Ask the npm registry for the latest pi-web-ui version and report it. */
|
|
950
955
|
async checkUpdate() {
|
|
951
956
|
const current = ClientSession.currentAppVersion();
|
|
@@ -993,10 +998,13 @@ export class ClientSession {
|
|
|
993
998
|
ok: true,
|
|
994
999
|
detail: out.slice(0, 400),
|
|
995
1000
|
});
|
|
1001
|
+
const autoRestart = this.onUpdateReady?.() ?? false;
|
|
996
1002
|
this.emit({
|
|
997
1003
|
type: "notice",
|
|
998
1004
|
level: "info",
|
|
999
|
-
text:
|
|
1005
|
+
text: autoRestart
|
|
1006
|
+
? "✅ 已更新 pi-web-ui,正在自动重启…"
|
|
1007
|
+
: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
|
|
1000
1008
|
});
|
|
1001
1009
|
}
|
|
1002
1010
|
else {
|
|
@@ -2350,6 +2358,11 @@ export class AgentService {
|
|
|
2350
2358
|
clients = new Map();
|
|
2351
2359
|
pending = new Map();
|
|
2352
2360
|
stateStore;
|
|
2361
|
+
/**
|
|
2362
|
+
* Set by index.ts: called by a client session after a successful
|
|
2363
|
+
* self-update; returns whether the process will restart itself.
|
|
2364
|
+
*/
|
|
2365
|
+
onUpdateReady = undefined;
|
|
2353
2366
|
constructor(cwd, sessionDirRoot, stateFile) {
|
|
2354
2367
|
this.cwd = cwd;
|
|
2355
2368
|
this.sessionDirRoot = sessionDirRoot;
|
|
@@ -2395,6 +2408,8 @@ export class AgentService {
|
|
|
2395
2408
|
}
|
|
2396
2409
|
}
|
|
2397
2410
|
cs.attachSink(send);
|
|
2411
|
+
// Forward the update hook (set once by index.ts) to every session.
|
|
2412
|
+
cs.onUpdateReady = this.onUpdateReady;
|
|
2398
2413
|
return cs;
|
|
2399
2414
|
}
|
|
2400
2415
|
/** Remove a socket from a client's broadcast set (called on socket close). */
|
package/dist/server/index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import { stat } from "node:fs/promises";
|
|
17
17
|
import { createServer } from "node:http";
|
|
18
|
+
import { createConnection } from "node:net";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
18
20
|
import { basename, dirname, join, resolve } from "node:path";
|
|
19
21
|
import { fileURLToPath } from "node:url";
|
|
20
22
|
import { randomUUID } from "node:crypto";
|
|
@@ -109,6 +111,51 @@ const heartbeatTimer = setInterval(() => {
|
|
|
109
111
|
const service = new AgentService(CWD, SESSION_DIR_ROOT,
|
|
110
112
|
// Per-client persisted UI state: last-used workspace + recent projects.
|
|
111
113
|
join(DATA_DIR, "client-state.json"));
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Self-update auto-restart
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// npm i -g writes new code to disk but the running process keeps the old
|
|
118
|
+
// code in memory — so a successful in-app update hands the process over:
|
|
119
|
+
// macOS launchd (KeepAlive) and systemd (Restart) relaunch us on exit;
|
|
120
|
+
// foreground runs get a replacement child that waits for our port to free.
|
|
121
|
+
// Docker containers can't self-restart (the orchestrator owns that), so they
|
|
122
|
+
// keep the manual-restart notice.
|
|
123
|
+
const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
|
|
124
|
+
function scheduleUpdateRestart() {
|
|
125
|
+
const isLaunchd = process.platform === "darwin" && process.ppid === 1;
|
|
126
|
+
const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
|
|
127
|
+
const inDocker = existsSync("/.dockerenv");
|
|
128
|
+
if (isLaunchd || isSystemd || inDocker) {
|
|
129
|
+
// Supervisors relaunch on exit; Docker restarts externally. Nothing to
|
|
130
|
+
// spawn — just exit after the notice has flushed.
|
|
131
|
+
if (isLaunchd || isSystemd) {
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
console.log("update applied — auto-restarting…");
|
|
134
|
+
if (isSystemd) {
|
|
135
|
+
// Non-zero exit: legacy units use Restart=on-failure.
|
|
136
|
+
process.exit(3);
|
|
137
|
+
}
|
|
138
|
+
void shutdown();
|
|
139
|
+
}, 1500);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
// Foreground / Windows: spawn a replacement from the updated install and
|
|
145
|
+
// exit. Same stdio (logs keep flowing), same args/env (port, cwd, data
|
|
146
|
+
// dir…); the child waits for this port to free before binding.
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
console.log("update applied — spawning replacement…");
|
|
149
|
+
spawn(process.execPath, process.argv.slice(1), {
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
env: { ...process.env, [RESTART_CHILD_ENV]: "1" },
|
|
152
|
+
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
153
|
+
});
|
|
154
|
+
void shutdown();
|
|
155
|
+
}, 1500);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
service.onUpdateReady = scheduleUpdateRestart;
|
|
112
159
|
wss.on("connection", (ws) => {
|
|
113
160
|
let clientId = null;
|
|
114
161
|
let closed = false;
|
|
@@ -279,6 +326,29 @@ wss.on("connection", (ws) => {
|
|
|
279
326
|
service.detach(clientId, send);
|
|
280
327
|
});
|
|
281
328
|
});
|
|
329
|
+
// When spawned by the old process as an auto-restart replacement, wait for
|
|
330
|
+
// the old instance to release the port before binding (it exits right after
|
|
331
|
+
// spawning us). Probe by attempting a connection: refused = free.
|
|
332
|
+
if (process.env[RESTART_CHILD_ENV] === "1") {
|
|
333
|
+
const deadline = Date.now() + 20_000;
|
|
334
|
+
const portFree = () => new Promise((resolve) => {
|
|
335
|
+
const sock = createConnection({ port: PORT, host: "127.0.0.1" });
|
|
336
|
+
sock.once("connect", () => {
|
|
337
|
+
sock.destroy();
|
|
338
|
+
resolve(false); // busy — old instance still up
|
|
339
|
+
});
|
|
340
|
+
sock.once("error", () => resolve(true)); // refused → free
|
|
341
|
+
sock.setTimeout(500, () => {
|
|
342
|
+
sock.destroy();
|
|
343
|
+
resolve(false);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
while (Date.now() < deadline) {
|
|
347
|
+
if (await portFree())
|
|
348
|
+
break;
|
|
349
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
282
352
|
httpServer.listen(PORT, () => {
|
|
283
353
|
console.log("");
|
|
284
354
|
console.log(" ⚡ pi-web-ui — web chat for the pi coding agent");
|
package/dist/server/terminals.js
CHANGED
|
@@ -137,7 +137,10 @@ function shellEnv() {
|
|
|
137
137
|
// and node-pty throws the generic "posix_spawnp failed". Locally-built
|
|
138
138
|
// copies (build/Release) are fine; every `npm install` that picks the prebuild
|
|
139
139
|
// — e.g. `npm i -g pi-web-ui`, which is what system-service installs run — is
|
|
140
|
-
// broken until the bit is restored. Self-heal at startup
|
|
140
|
+
// broken until the bit is restored. Self-heal at startup AND lazily before
|
|
141
|
+
// every spawn (an `npm i -g` while the server is running replaces the helper
|
|
142
|
+
// under the running process, so the startup-only repair misses it).
|
|
143
|
+
// Best-effort: a read-only node_modules just keeps the old failure.
|
|
141
144
|
const require = createRequire(import.meta.url);
|
|
142
145
|
/** Absolute paths of every node-pty spawn-helper this install can exec. */
|
|
143
146
|
function spawnHelperPaths() {
|
|
@@ -294,6 +297,9 @@ export class TerminalManager {
|
|
|
294
297
|
this.fail(id, `目录不存在:${abs}`);
|
|
295
298
|
return false;
|
|
296
299
|
}
|
|
300
|
+
// node-pty's spawn-helper may have lost its +x bit since the last repair
|
|
301
|
+
// (e.g. a global npm install replaced the helper while this server runs).
|
|
302
|
+
repairSpawnHelperPermissions();
|
|
297
303
|
let pty;
|
|
298
304
|
try {
|
|
299
305
|
pty = spawn(SHELL, SHELL_ARGS, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|