pi-web-ui 0.8.6 → 0.8.7

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.
@@ -988,6 +988,41 @@ export class ClientSession {
988
988
  });
989
989
  }
990
990
  }
991
+ /**
992
+ * After `npm i -g`, confirm the on-disk package this process serves from
993
+ * actually changed to the new version and is complete. Windows npm updates
994
+ * can fail partway (locked files / Defender / npm rollback) and leave the
995
+ * global install without its bin links — restarting into that is a silent
996
+ * crash (web/dist missing + `pi-web-ui` no longer on PATH). Returns null
997
+ * when OK, else a human-readable problem description.
998
+ */
999
+ static verifyGlobalInstall() {
1000
+ try {
1001
+ const here = dirname(fileURLToPath(import.meta.url));
1002
+ const pkgRoot = resolve(here, "..", "..");
1003
+ const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
1004
+ if (!pkg.version || pkg.version === ClientSession.currentAppVersion()) {
1005
+ return `安装目录版本未变化(${pkg.version ?? "未知"})`;
1006
+ }
1007
+ if (!existsSync(join(pkgRoot, "web", "dist", "index.html"))) {
1008
+ return "web/dist/index.html 缺失(前端产物未安装完整)";
1009
+ }
1010
+ if (!existsSync(join(pkgRoot, "bin", "pi-web-ui.mjs"))) {
1011
+ return "bin/pi-web-ui.mjs 缺失";
1012
+ }
1013
+ if (process.platform === "win32") {
1014
+ const prefix = dirname(process.execPath);
1015
+ const hasShim = existsSync(join(prefix, "pi-web-ui.cmd")) ||
1016
+ existsSync(join(prefix, "pi-web-ui.ps1"));
1017
+ if (!hasShim)
1018
+ return "pi-web-ui 命令入口(bin 链接)未生成";
1019
+ }
1020
+ return null;
1021
+ }
1022
+ catch (err) {
1023
+ return `读取安装目录失败:${err.message}`;
1024
+ }
1025
+ }
991
1026
  /** npm i -g pi-web-ui@latest — the new code only takes effect after a restart. */
992
1027
  async updateApp() {
993
1028
  try {
@@ -997,34 +1032,50 @@ export class ClientSession {
997
1032
  text: "正在更新 pi-web-ui(npm i -g pi-web-ui@latest)…",
998
1033
  });
999
1034
  const { code, out } = await this.runAsync("npm", ["i", "-g", "pi-web-ui@latest"], 180_000);
1000
- if (code === 0) {
1001
- this.pendingRestart = true;
1035
+ if (code !== 0) {
1002
1036
  this.emit({
1003
1037
  type: "update_result",
1004
- ok: true,
1005
- detail: out.slice(0, 400),
1038
+ ok: false,
1039
+ detail: `npm i 失败(${code ?? "timeout"}):${out.slice(0, 400)}`,
1006
1040
  });
1007
- const autoRestart = this.onUpdateReady?.() ?? false;
1008
1041
  this.emit({
1009
1042
  type: "notice",
1010
- level: "info",
1011
- text: autoRestart
1012
- ? "✅ 已更新 pi-web-ui,正在自动重启…"
1013
- : "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
1043
+ level: "error",
1044
+ text: `更新 pi-web-ui 失败(${code ?? "timeout"}):${out.slice(0, 300)}`,
1014
1045
  });
1046
+ return;
1015
1047
  }
1016
- else {
1048
+ // npm reported success, but on Windows the replacement can be partial
1049
+ // (locked files, rollback) — restarting into a broken install is a
1050
+ // crash with no hint. Verify before handing over.
1051
+ const problem = ClientSession.verifyGlobalInstall();
1052
+ if (problem) {
1017
1053
  this.emit({
1018
1054
  type: "update_result",
1019
1055
  ok: false,
1020
- detail: `npm i 失败(${code ?? "timeout"}):${out.slice(0, 400)}`,
1056
+ detail: `npm i 成功但安装不完整(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复后再重启服务。`,
1021
1057
  });
1022
1058
  this.emit({
1023
1059
  type: "notice",
1024
1060
  level: "error",
1025
- text: `更新 pi-web-ui 失败(${code ?? "timeout"}):${out.slice(0, 300)}`,
1061
+ text: `更新未完整生效(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复`,
1026
1062
  });
1063
+ return;
1027
1064
  }
1065
+ this.pendingRestart = true;
1066
+ this.emit({
1067
+ type: "update_result",
1068
+ ok: true,
1069
+ detail: out.slice(0, 400),
1070
+ });
1071
+ const autoRestart = this.onUpdateReady?.() ?? false;
1072
+ this.emit({
1073
+ type: "notice",
1074
+ level: "info",
1075
+ text: autoRestart
1076
+ ? "✅ 已更新 pi-web-ui,正在自动重启…"
1077
+ : "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
1078
+ });
1028
1079
  }
1029
1080
  catch (err) {
1030
1081
  this.emit({
@@ -89,13 +89,31 @@ app.get("/api/file", async (req, res) => {
89
89
  // from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
90
90
  const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <pkg>/server
91
91
  const pkgRoot = resolve(here, "..", "..");
92
+ /** Set in the env of the replacement child spawned by a self-update restart. */
93
+ const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
92
94
  const webDist = join(pkgRoot, "web", "dist");
93
95
  if (existsSync(webDist)) {
94
96
  app.use(express.static(webDist));
95
97
  app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
96
- res.sendFile(join(webDist, "index.html"));
98
+ // Callback form: a failed stat here (npm i -g is mid-replacement of the
99
+ // package dir) responds 503 instead of crashing the request pipeline
100
+ // with an unhandled ENOENT stack trace.
101
+ res.sendFile(join(webDist, "index.html"), (err) => {
102
+ if (err && !res.headersSent) {
103
+ res.status(503).send("正在更新 pi-web-ui,请稍后刷新…");
104
+ }
105
+ });
97
106
  });
98
107
  }
108
+ else if (process.env[RESTART_CHILD_ENV]) {
109
+ // Auto-restart replacement of a self-update whose npm install did not
110
+ // complete (Windows: locked files / rollback can leave the global package
111
+ // without web/dist). Fail loudly with a repair hint instead of serving a
112
+ // UI-less 404 with no explanation.
113
+ console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" +
114
+ " 请手动执行 npm i -g pi-web-ui@latest 修复后重新启动。");
115
+ process.exit(1);
116
+ }
99
117
  const httpServer = createServer(app);
100
118
  const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
101
119
  // Heartbeat: lets clients detect half-open connections (server killed without
@@ -120,7 +138,6 @@ join(DATA_DIR, "client-state.json"));
120
138
  // foreground runs get a replacement child that waits for our port to free.
121
139
  // Docker containers can't self-restart (the orchestrator owns that), so they
122
140
  // keep the manual-restart notice.
123
- const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
124
141
  function scheduleUpdateRestart() {
125
142
  const isLaunchd = process.platform === "darwin" && process.ppid === 1;
126
143
  const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
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",