pi-web-ui 0.8.3 → 0.8.5

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 CHANGED
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * pi-web-ui CLI.
4
4
  *
5
- * pi-web-ui 启动生产服务器(前台,Ctrl+C 停止)
5
+ * pi-web-ui 启动生产服务器(前台,Ctrl+C 停止,自动打开浏览器)
6
6
  * pi-web-ui --port 9000 --cwd /path 同上,覆盖端口 / 工作目录 / 数据目录
7
+ * pi-web-ui --no-browser 启动但不自动打开浏览器
7
8
  * pi-web-ui --version | --help
8
9
  * pi-web-ui server install [选项] 安装系统服务(开机自启)并启动
9
10
  * pi-web-ui server uninstall [选项] 卸载系统服务
@@ -21,6 +22,7 @@
21
22
  * PI_CODING_AGENT_DIR。
22
23
  */
23
24
  import { spawnSync } from "node:child_process";
25
+ import { get as httpGet } from "node:http";
24
26
  import {
25
27
  existsSync,
26
28
  mkdirSync,
@@ -47,8 +49,9 @@ try {
47
49
  const HELP = `pi-web-ui v${pkg.version} — web chat for the pi coding agent
48
50
 
49
51
  用法:
50
- pi-web-ui 启动服务器(前台,Ctrl+C 停止)
52
+ pi-web-ui 启动服务器(前台,Ctrl+C 停止,自动打开浏览器)
51
53
  pi-web-ui --port 9000 --cwd /path 启动并指定端口 / 工作目录 / 数据目录
54
+ pi-web-ui --no-browser 启动但不自动打开浏览器
52
55
  pi-web-ui server install [选项] 安装系统服务(开机自启)并启动
53
56
  pi-web-ui server uninstall [选项] 卸载系统服务
54
57
  pi-web-ui server start|stop|restart|status [选项]
@@ -109,6 +112,7 @@ function parseFlags(argv) {
109
112
  dataDir: undefined,
110
113
  name: undefined,
111
114
  print: false,
115
+ noBrowser: false,
112
116
  help: false,
113
117
  };
114
118
  const positionals = [];
@@ -141,6 +145,9 @@ function parseFlags(argv) {
141
145
  case "--print":
142
146
  opts.print = true;
143
147
  break;
148
+ case "--no-browser":
149
+ opts.noBrowser = true;
150
+ break;
144
151
  case "--help":
145
152
  case "-h":
146
153
  opts.help = true;
@@ -157,11 +164,52 @@ function parseFlags(argv) {
157
164
  // 前台启动
158
165
  // ---------------------------------------------------------------------------
159
166
 
167
+ /** Open a URL in the OS default browser; failures are ignored (best-effort). */
168
+ function openBrowser(url) {
169
+ if (isMac) {
170
+ spawnSync("open", [url], { stdio: "ignore" });
171
+ } else if (isWin) {
172
+ spawnSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
173
+ } else {
174
+ spawnSync("xdg-open", [url], { stdio: "ignore" });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Poll `url` until the server answers (or ~15s pass), then open it in the
180
+ * default browser. The foreground server runs in this process, so the first
181
+ * HTTP response is the "listening" signal; if the server crashes meanwhile
182
+ * (e.g. port already taken), the pending timers die with the process and
183
+ * nothing is opened.
184
+ */
185
+ function openBrowserWhenUp(url) {
186
+ const deadline = Date.now() + 15_000;
187
+ const attempt = () => {
188
+ const req = httpGet(url, (res) => {
189
+ res.resume();
190
+ console.log(` 🌐 已自动打开浏览器:${url}(--no-browser 可关闭)`);
191
+ openBrowser(url);
192
+ });
193
+ req.setTimeout(1000, () => {
194
+ req.destroy();
195
+ if (Date.now() < deadline) setTimeout(attempt, 150);
196
+ });
197
+ req.on("error", () => {
198
+ if (Date.now() < deadline) setTimeout(attempt, 150);
199
+ });
200
+ };
201
+ attempt();
202
+ }
203
+
160
204
  async function startForeground(opts) {
161
205
  if (opts.port) process.env.PORT = opts.port;
162
206
  if (opts.cwd) process.env.PI_WEB_CWD = resolve(opts.cwd);
163
207
  if (opts.dataDir) process.env.PI_WEB_DATA_DIR = resolve(opts.dataDir);
208
+ const url = `http://localhost:${String(
209
+ opts.port ?? process.env.PORT ?? "8787",
210
+ )}`;
164
211
  await import(pathToFileURL(SERVER_ENTRY).href);
212
+ if (!opts.noBrowser) openBrowserWhenUp(url);
165
213
  }
166
214
 
167
215
  // ---------------------------------------------------------------------------
@@ -394,7 +442,7 @@ User=${process.env.SUDO_USER ?? userInfo().username}
394
442
  WorkingDirectory=${cwd}
395
443
  ${envLines}
396
444
  ExecStart=${JSON.stringify(NODE)} ${JSON.stringify(SERVER_ENTRY)}
397
- Restart=on-failure
445
+ Restart=always
398
446
  RestartSec=5
399
447
 
400
448
  [Install]
@@ -1,48 +1,48 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <!--
4
- pi-web-ui launchd agent — macOS auto-start at login.
5
-
6
- Install:
7
- npm i -g pi-web-ui
8
- cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
9
- # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD for your setup
10
- launchctl load ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
11
- # (or: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist)
12
-
13
- Find the pi-web-ui binary path with: which pi-web-ui
14
- -->
15
- <plist version="1.0">
16
- <dict>
17
- <key>Label</key>
18
- <string>com.xingshuyin.pi-web-ui</string>
19
-
20
- <key>ProgramArguments</key>
21
- <array>
22
- <string>/usr/local/bin/pi-web-ui</string>
23
- </array>
24
-
25
- <key>RunAtLoad</key>
26
- <true/>
27
-
28
- <!-- Restart if it crashes -->
29
- <key>KeepAlive</key>
30
- <true/>
31
-
32
- <key>WorkingDirectory</key>
33
- <string>/Users/YOUR_USER</string>
34
-
35
- <key>EnvironmentVariables</key>
36
- <dict>
37
- <key>PORT</key>
38
- <string>8787</string>
39
- <key>PI_WEB_CWD</key>
40
- <string>/Users/YOUR_USER</string>
41
- </dict>
42
-
43
- <key>StandardOutPath</key>
44
- <string>/tmp/pi-web-ui.log</string>
45
- <key>StandardErrorPath</key>
46
- <string>/tmp/pi-web-ui.err</string>
47
- </dict>
48
- </plist>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ pi-web-ui launchd agent — macOS auto-start at login.
5
+
6
+ Install:
7
+ npm i -g pi-web-ui
8
+ cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
9
+ # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD for your setup
10
+ launchctl load ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
11
+ # (or: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist)
12
+
13
+ Find the pi-web-ui binary path with: which pi-web-ui
14
+ -->
15
+ <plist version="1.0">
16
+ <dict>
17
+ <key>Label</key>
18
+ <string>com.xingshuyin.pi-web-ui</string>
19
+
20
+ <key>ProgramArguments</key>
21
+ <array>
22
+ <string>/usr/local/bin/pi-web-ui</string>
23
+ </array>
24
+
25
+ <key>RunAtLoad</key>
26
+ <true/>
27
+
28
+ <!-- Restart if it crashes -->
29
+ <key>KeepAlive</key>
30
+ <true/>
31
+
32
+ <key>WorkingDirectory</key>
33
+ <string>/Users/YOUR_USER</string>
34
+
35
+ <key>EnvironmentVariables</key>
36
+ <dict>
37
+ <key>PORT</key>
38
+ <string>8787</string>
39
+ <key>PI_WEB_CWD</key>
40
+ <string>/Users/YOUR_USER</string>
41
+ </dict>
42
+
43
+ <key>StandardOutPath</key>
44
+ <string>/tmp/pi-web-ui.log</string>
45
+ <key>StandardErrorPath</key>
46
+ <string>/tmp/pi-web-ui.err</string>
47
+ </dict>
48
+ </plist>
@@ -1,71 +1,71 @@
1
- <?xml version="1.0" encoding="UTF-16"?>
2
- <!--
3
- pi-web-ui Task Scheduler task — Windows auto-start at logon.
4
-
5
- The easy way (no admin needed, generates everything for you):
6
- npm i -g pi-web-ui
7
- pi-web-ui server install --port 8787 --cwd C:\path\to\project
8
- pi-web-ui server status | restart | stop | uninstall
9
-
10
- Manual install with this template (edit the paths below first):
11
- schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
12
- schtasks /Run /TN "pi-web-ui"
13
-
14
- Notes:
15
- - The task runs the PowerShell launcher the CLI generates at
16
- %APPDATA%\pi-web-ui\pi-web-ui.ps1 with -WindowStyle Hidden, so the
17
- server runs with no black console window (nothing to accidentally
18
- close/kill). The ps1 sets PORT/PI_WEB_CWD, cd's to the workspace,
19
- launches node, and appends output to %USERPROFILE%\pi-web-ui.log.
20
- Preview both generated files with: pi-web-ui server install --print
21
- - Save this file as UTF-16 LE (schtasks requires it; the CLI does this
22
- automatically when it writes the task XML).
23
- - LogonTrigger = starts when you log in, same as a launchd user agent.
24
- For boot-start without login, use Docker instead (see README).
25
- -->
26
- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
27
- <RegistrationInfo>
28
- <Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
29
- </RegistrationInfo>
30
- <Triggers>
31
- <LogonTrigger>
32
- <Enabled>true</Enabled>
33
- </LogonTrigger>
34
- </Triggers>
35
- <Principals>
36
- <Principal id="Author">
37
- <LogonType>InteractiveToken</LogonType>
38
- <RunLevel>LeastPrivilege</RunLevel>
39
- </Principal>
40
- </Principals>
41
- <Settings>
42
- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
43
- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
44
- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
45
- <AllowHardTerminate>true</AllowHardTerminate>
46
- <StartWhenAvailable>false</StartWhenAvailable>
47
- <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
48
- <IdleSettings>
49
- <StopOnIdleEnd>false</StopOnIdleEnd>
50
- <RestartOnIdle>false</RestartOnIdle>
51
- </IdleSettings>
52
- <AllowStartOnDemand>true</AllowStartOnDemand>
53
- <Enabled>true</Enabled>
54
- <Hidden>false</Hidden>
55
- <RunOnlyIfIdle>false</RunOnlyIfIdle>
56
- <WakeToRun>false</WakeToRun>
57
- <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
58
- <Priority>7</Priority>
59
- <RestartOnFailure>
60
- <Interval>PT1M</Interval>
61
- <Count>3</Count>
62
- </RestartOnFailure>
63
- </Settings>
64
- <Actions Context="Author">
65
- <Exec>
66
- <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
67
- <Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%APPDATA%\pi-web-ui\pi-web-ui.ps1"</Arguments>
68
- <WorkingDirectory>%USERPROFILE%</WorkingDirectory>
69
- </Exec>
70
- </Actions>
71
- </Task>
1
+ <?xml version="1.0" encoding="UTF-16"?>
2
+ <!--
3
+ pi-web-ui Task Scheduler task — Windows auto-start at logon.
4
+
5
+ The easy way (no admin needed, generates everything for you):
6
+ npm i -g pi-web-ui
7
+ pi-web-ui server install --port 8787 --cwd C:\path\to\project
8
+ pi-web-ui server status | restart | stop | uninstall
9
+
10
+ Manual install with this template (edit the paths below first):
11
+ schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
12
+ schtasks /Run /TN "pi-web-ui"
13
+
14
+ Notes:
15
+ - The task runs the PowerShell launcher the CLI generates at
16
+ %APPDATA%\pi-web-ui\pi-web-ui.ps1 with -WindowStyle Hidden, so the
17
+ server runs with no black console window (nothing to accidentally
18
+ close/kill). The ps1 sets PORT/PI_WEB_CWD, cd's to the workspace,
19
+ launches node, and appends output to %USERPROFILE%\pi-web-ui.log.
20
+ Preview both generated files with: pi-web-ui server install --print
21
+ - Save this file as UTF-16 LE (schtasks requires it; the CLI does this
22
+ automatically when it writes the task XML).
23
+ - LogonTrigger = starts when you log in, same as a launchd user agent.
24
+ For boot-start without login, use Docker instead (see README).
25
+ -->
26
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
27
+ <RegistrationInfo>
28
+ <Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
29
+ </RegistrationInfo>
30
+ <Triggers>
31
+ <LogonTrigger>
32
+ <Enabled>true</Enabled>
33
+ </LogonTrigger>
34
+ </Triggers>
35
+ <Principals>
36
+ <Principal id="Author">
37
+ <LogonType>InteractiveToken</LogonType>
38
+ <RunLevel>LeastPrivilege</RunLevel>
39
+ </Principal>
40
+ </Principals>
41
+ <Settings>
42
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
43
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
44
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
45
+ <AllowHardTerminate>true</AllowHardTerminate>
46
+ <StartWhenAvailable>false</StartWhenAvailable>
47
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
48
+ <IdleSettings>
49
+ <StopOnIdleEnd>false</StopOnIdleEnd>
50
+ <RestartOnIdle>false</RestartOnIdle>
51
+ </IdleSettings>
52
+ <AllowStartOnDemand>true</AllowStartOnDemand>
53
+ <Enabled>true</Enabled>
54
+ <Hidden>false</Hidden>
55
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
56
+ <WakeToRun>false</WakeToRun>
57
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
58
+ <Priority>7</Priority>
59
+ <RestartOnFailure>
60
+ <Interval>PT1M</Interval>
61
+ <Count>3</Count>
62
+ </RestartOnFailure>
63
+ </Settings>
64
+ <Actions Context="Author">
65
+ <Exec>
66
+ <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
67
+ <Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%APPDATA%\pi-web-ui\pi-web-ui.ps1"</Arguments>
68
+ <WorkingDirectory>%USERPROFILE%</WorkingDirectory>
69
+ </Exec>
70
+ </Actions>
71
+ </Task>
@@ -1,31 +1,31 @@
1
- # pi-web-ui systemd unit — Linux auto-start on boot.
2
- #
3
- # Install (adjust User/WorkingDirectory/Environment to taste):
4
- # sudo npm i -g pi-web-ui
5
- # sudo cp deploy/pi-web-ui.service /etc/systemd/system/
6
- # sudo systemctl daemon-reload
7
- # sudo systemctl enable --now pi-web-ui
8
- # sudo systemctl status pi-web-ui
9
- #
10
- # Logs: journalctl -u pi-web-ui -f
11
-
12
- [Unit]
13
- Description=pi-web-ui — web chat for the pi coding agent
14
- After=network.target
15
-
16
- [Service]
17
- Type=simple
18
- # Run as an unprivileged user (change to your user, e.g. yourname)
19
- User=YOUR_USER
20
- # The workspace the agent operates in (read/edit/bash/write)
21
- WorkingDirectory=/home/YOUR_USER
22
- Environment=PORT=8787
23
- # Point at your pi config dir if it's not the default ~/.pi/agent
24
- #Environment=PI_CODING_AGENT_DIR=/home/YOUR_USER/.pi/agent
25
- ExecStart=/usr/bin/pi-web-ui
26
- Restart=on-failure
27
- RestartSec=5
28
- # npm global bin may live elsewhere (nvm, etc.) — find with: which pi-web-ui
29
-
30
- [Install]
31
- WantedBy=multi-user.target
1
+ # pi-web-ui systemd unit — Linux auto-start on boot.
2
+ #
3
+ # Install (adjust User/WorkingDirectory/Environment to taste):
4
+ # sudo npm i -g pi-web-ui
5
+ # sudo cp deploy/pi-web-ui.service /etc/systemd/system/
6
+ # sudo systemctl daemon-reload
7
+ # sudo systemctl enable --now pi-web-ui
8
+ # sudo systemctl status pi-web-ui
9
+ #
10
+ # Logs: journalctl -u pi-web-ui -f
11
+
12
+ [Unit]
13
+ Description=pi-web-ui — web chat for the pi coding agent
14
+ After=network.target
15
+
16
+ [Service]
17
+ Type=simple
18
+ # Run as an unprivileged user (change to your user, e.g. yourname)
19
+ User=YOUR_USER
20
+ # The workspace the agent operates in (read/edit/bash/write)
21
+ WorkingDirectory=/home/YOUR_USER
22
+ Environment=PORT=8787
23
+ # Point at your pi config dir if it's not the default ~/.pi/agent
24
+ #Environment=PI_CODING_AGENT_DIR=/home/YOUR_USER/.pi/agent
25
+ ExecStart=/usr/bin/pi-web-ui
26
+ Restart=on-failure
27
+ RestartSec=5
28
+ # npm global bin may live elsewhere (nvm, etc.) — find with: which pi-web-ui
29
+
30
+ [Install]
31
+ WantedBy=multi-user.target
@@ -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: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
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). */
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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",