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/LICENSE +21 -21
- package/README.md +414 -398
- package/README.zh-CN.md +359 -359
- package/bin/pi-web-ui.mjs +51 -3
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/pi-web-ui-task.xml +71 -71
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +16 -1
- package/dist/server/index.js +70 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-Cbunl4-1.js → index-Dmb4daY8.js} +47 -47
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +14 -14
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=
|
|
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>
|
package/deploy/pi-web-ui.service
CHANGED
|
@@ -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:
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.8.
|
|
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",
|