pi-webapp 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -8,7 +8,24 @@
8
8
 
9
9
  ## Install and open
10
10
 
11
- Requires Node.js 22.19 or later and Pi 0.87.1 or later.
11
+ Requires Node.js 22.19 or later. To open the web interface directly:
12
+
13
+ ```bash
14
+ npx pi-webapp
15
+ ```
16
+
17
+ This starts Pi in the background and opens the default browser on a local graphical desktop, leaving the terminal free. If Pi is missing, the launcher installs the official `@earendil-works/pi-coding-agent` package with npm. An existing Pi installation must be version 0.87.1 or later. The launcher does not replace an older installation automatically. Running the launcher again reopens the existing server. Remote or headless sessions print the address instead; set `PI_WEBAPP_AUTO_OPEN=0` to disable automatic opening.
18
+
19
+ Use `npx pi-webapp status` to show the address and `npx pi-webapp stop` to stop the background service. After a global install, use `pi-webapp status` and `pi-webapp stop`.
20
+
21
+ You can also install the launcher globally:
22
+
23
+ ```bash
24
+ npm install -g pi-webapp
25
+ pi-webapp
26
+ ```
27
+
28
+ To use the extension inside an existing Pi terminal, the original workflow remains available:
12
29
 
13
30
  ```bash
14
31
  pi install npm:pi-webapp
@@ -23,11 +40,11 @@ In Pi, enter `/web` to open the interface in your default browser. Pi also print
23
40
  - Send messages, stop a run, start a new Pi session, and change the current model or thinking level.
24
41
  - Browse workspaces and saved sessions. Add a workspace with the native directory picker when available, or use the built-in directory browser over SSH.
25
42
  - Attach up to 20 files of 20 MB each by selecting, dropping, or pasting them. Images show previews and are also sent as Pi image content.
26
- - Manage appearance, Pi packages, extensions, skills, and models from the settings panel. Provider credentials remain in Pi's local authentication store and are never returned to the browser.
43
+ - Manage appearance, Pi packages, extensions, skills, and models from the settings panel. The Models page can add models, edit display names, context windows, output limits, and input types, and restore built-in defaults; changes are saved to Pi's `models.json`. Browser sign-in opens the provider's authorization page automatically; manual code entry remains available when a callback cannot reach Pi. Stored provider credentials can also be removed there. Credentials stay in Pi's local `auth.json` and are never returned to the browser.
27
44
 
28
- The Pi terminal session that opened `/web` stays synchronized with the browser. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They do not load extensions again in the same process.
45
+ If you enter `/web` manually in a Pi terminal, that terminal session stays synchronized with the browser. With the launcher, Pi runs in the background. Sessions opened in other workspaces use the Pi SDK and load that workspace's skills and project context. They do not load extensions again in the same process.
29
46
 
30
- Workspace records and attachments stay under Pi's existing `pi-web/` data directory for compatibility with earlier releases. Removing a workspace from the sidebar does not delete its files or sessions.
47
+ On startup, the app discovers workspaces from Pi's saved sessions, so conversations from other directories appear in the sidebar. Workspace records and attachments stay under Pi's existing `pi-web/` data directory. Removing a workspace from the sidebar does not delete its files or sessions; you can add it again manually.
31
48
 
32
49
  ## Develop locally
33
50
 
@@ -36,10 +53,12 @@ npm install
36
53
  npm run check
37
54
  npm run test
38
55
  npm run build
39
- pi -e .
56
+ npm run start
57
+ npm run status
58
+ npm run stop
40
59
  ```
41
60
 
42
- After editing the extension, rebuild it, then enter `/reload` and `/web` in Pi. Refreshing an old browser page alone does not replace the running bridge.
61
+ `npm run start` builds the project, starts Pi in the background, and opens the web interface. After editing the extension, run `npm run stop` followed by `npm run start`. If you develop with `pi -e .`, enter `/reload` and `/web` in Pi after rebuilding. Refreshing an old browser page alone does not replace the running bridge.
43
62
 
44
63
  The source lives in `extension/`, `shared/`, and `web/src/`. The npm package contains only the bundled, minified JavaScript extension, built web assets, cover image, and the English and Chinese READMEs. No TypeScript source or source maps are published. Minification makes the shipped code harder to read; it does not encrypt JavaScript.
45
64
 
package/README.zh-CN.md CHANGED
@@ -8,7 +8,24 @@
8
8
 
9
9
  ## 安装与打开
10
10
 
11
- 需要 Node.js 22.19 或更新版本,以及 Pi 0.87.1 或更新版本。
11
+ 需要 Node.js 22.19 或更新版本。推荐直接运行:
12
+
13
+ ```bash
14
+ npx pi-webapp
15
+ ```
16
+
17
+ 该命令会在后台启动 Pi,并在本地图形桌面用默认浏览器打开网页,终端可继续使用。若未找到 Pi,会自动通过 npm 安装官方 `@earendil-works/pi-coding-agent`。已有 Pi 需要 0.87.1 或更新版本;启动器不会自动覆盖旧版本。再次运行启动命令会重新打开已有服务。远程或无图形界面时只显示地址;设置 `PI_WEBAPP_AUTO_OPEN=0` 可关闭自动打开。
18
+
19
+ 可用 `npx pi-webapp status` 查看地址,用 `npx pi-webapp stop` 停止后台服务。全局安装后可直接运行 `pi-webapp status` 和 `pi-webapp stop`。
20
+
21
+ 也可以将启动器安装为全局命令:
22
+
23
+ ```bash
24
+ npm install -g pi-webapp
25
+ pi-webapp
26
+ ```
27
+
28
+ 如果希望在已有 Pi 终端中使用,仍可按原方式安装扩展:
12
29
 
13
30
  ```bash
14
31
  pi install npm:pi-webapp
@@ -23,11 +40,11 @@ pi
23
40
  - 发送消息、停止运行、启动新的 Pi 会话,以及切换当前模型或思考级别。
24
41
  - 浏览工作区和已保存的会话。环境支持时可通过系统目录选择器添加工作区;通过 SSH 使用时,也可使用内置目录浏览器。
25
42
  - 通过选择、拖放或粘贴附加文件,最多 20 个,每个不超过 20 MB。图片会显示预览,并作为 Pi 图片内容发送。
26
- - 在设置面板中管理外观、Pi 软件包、扩展、技能和模型。模型提供商的凭据保留在 Pi 本地的身份验证存储中,不会返回给浏览器。
43
+ - 在设置面板中管理外观、Pi 软件包、扩展、技能和模型。模型页可添加模型、编辑显示名称、上下文窗口、最大输出 token 数和输入类型,或恢复内置模型的默认参数;配置写入 Pi 的 `models.json`。网页登录会自动打开提供方授权页;当回调无法到达 Pi 时,仍可手动粘贴授权码。已保存的提供方认证可在这里移除。凭据保留在 Pi 本地的 `auth.json`,不会返回给浏览器。
27
44
 
28
- 打开 `/web` 的 Pi 终端会话会与浏览器保持同步。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;同一进程中不会再次加载扩展。
45
+ 如果通过 Pi 终端手动输入 `/web`,该终端会话会与浏览器保持同步。使用启动器时,Pi 在后台运行。在其他工作区打开的会话使用 Pi SDK,并加载该工作区的技能和项目上下文;同一进程中不会再次加载扩展。
29
46
 
30
- 工作区记录和附件继续存放在 Pi 现有的 `pi-web/` 数据目录下,以兼容早期版本。从侧边栏移除工作区不会删除其文件或会话。
47
+ 启动时会从 Pi 已保存的会话中发现工作区,因此先前在其他目录使用 Pi 的会话也会出现在侧边栏。工作区记录和附件继续存放在 Pi 现有的 `pi-web/` 数据目录下。从侧边栏移除工作区不会删除其文件或会话;移除后如需再次显示,可以手动添加。
31
48
 
32
49
  ## 本地开发
33
50
 
@@ -36,10 +53,12 @@ npm install
36
53
  npm run check
37
54
  npm run test
38
55
  npm run build
39
- pi -e .
56
+ npm run start
57
+ npm run status
58
+ npm run stop
40
59
  ```
41
60
 
42
- 修改扩展后,需要重新构建扩展,再在 Pi 中依次输入 `/reload` 和 `/web`。仅刷新旧的浏览器页面不会替换正在运行的连接桥接代码。
61
+ `npm run start` 会构建项目、在后台启动 Pi 并打开 Web 界面。修改扩展后,运行 `npm run stop`,再运行 `npm run start`;如果使用 `pi -e .` 开发,则在 Pi 中依次输入 `/reload` 和 `/web`。仅刷新旧的浏览器页面不会替换正在运行的连接桥接代码。
43
62
 
44
63
  源代码位于 `extension/`、`shared/` 和 `web/src/`。npm 软件包仅包含打包并压缩后的 JavaScript 扩展、构建后的 Web 资源、封面图片,以及英文和中文 README;不会发布 TypeScript 源码或 source map。代码压缩会增加阅读发布版本代码的难度,但不会对 JavaScript 加密。
45
64
 
@@ -0,0 +1,2 @@
1
+ export type OpenPageRunner = (command: string, args: readonly string[]) => Promise<void>;
2
+ export declare function openWebPage(url: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, run?: OpenPageRunner): Promise<boolean>;
@@ -0,0 +1,18 @@
1
+ import { execFile } from "node:child_process";
2
+
3
+ const runOpenCommand = (command, args) => new Promise((resolve, reject) => {
4
+ execFile(command, [...args], { windowsHide: true }, (error) => {
5
+ if (error) reject(error);
6
+ else resolve();
7
+ });
8
+ });
9
+
10
+ /** Open on the host desktop only when this process has a local graphical session. */
11
+ export async function openWebPage(url, platform = process.platform, env = process.env, run = runOpenCommand) {
12
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) return false;
13
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
14
+ const command = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : platform === "win32" ? "explorer.exe" : null;
15
+ if (command === null) return false;
16
+ await run(command, [url]);
17
+ return true;
18
+ }
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+ import { spawn as spawnNode } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { closeSync, existsSync, openSync } from "node:fs";
5
+ import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import spawn from "cross-spawn";
10
+ import { openWebPage } from "./open-web-page.mjs";
11
+
12
+ const minimum = [0, 87, 1];
13
+ const launcher = fileURLToPath(import.meta.url);
14
+ const extension = fileURLToPath(new URL("../dist/extension.js", import.meta.url));
15
+ const agentDir = resolve((process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")).replace(/^~(?=$|[\\/])/, homedir()));
16
+ const statePath = join(agentDir, "pi-web", "launcher.json");
17
+ const stopPath = join(agentDir, "pi-web", "launcher.stop");
18
+ const logPath = join(agentDir, "pi-web", "launcher.log");
19
+
20
+ function versionParts(value) {
21
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
22
+ return match ? match.slice(1).map(Number) : null;
23
+ }
24
+
25
+ function compatible(parts, required) {
26
+ for (let i = 0; i < 3; i++) {
27
+ if (parts[i] !== required[i]) return parts[i] > required[i];
28
+ }
29
+ return true;
30
+ }
31
+
32
+ function probe(command) {
33
+ const result = spawn.sync(command, ["--version"], { encoding: "utf8" });
34
+ if (result.error?.code === "ENOENT") return null;
35
+ if (result.error || result.status !== 0) {
36
+ throw new Error(`无法运行 Pi:${result.error?.message || result.stderr?.trim() || command}`);
37
+ }
38
+ const version = result.stdout.trim();
39
+ const parts = versionParts(version);
40
+ if (!parts || !compatible(parts, minimum)) {
41
+ throw new Error(`Pi 版本 ${version || "未知"} 不受支持,需要 0.87.1 或更新版本。请更新 Pi 后重试。`);
42
+ }
43
+ return command;
44
+ }
45
+
46
+ function installPi() {
47
+ console.error("未找到 Pi,正在通过 npm 安装 @earendil-works/pi-coding-agent …");
48
+ const result = spawn.sync("npm", ["install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"], { stdio: "inherit" });
49
+ if (result.error || result.status !== 0) {
50
+ throw new Error(`Pi 安装失败。请手动运行 npm install -g --ignore-scripts @earendil-works/pi-coding-agent${result.error ? `(${result.error.message})` : ""}`);
51
+ }
52
+ const inPath = probe("pi");
53
+ if (inPath) return inPath;
54
+ const prefix = spawn.sync("npm", ["prefix", "-g"], { encoding: "utf8" });
55
+ if (prefix.status === 0) {
56
+ const installed = process.platform === "win32" ? join(prefix.stdout.trim(), "pi.cmd") : join(prefix.stdout.trim(), "bin", "pi");
57
+ if (existsSync(installed)) return probe(installed);
58
+ }
59
+ throw new Error("Pi 已安装,但找不到 pi 命令。请将 npm 全局可执行目录加入 PATH 后重试。");
60
+ }
61
+
62
+ function alive(pid) {
63
+ if (!Number.isInteger(pid) || pid <= 0) return false;
64
+ try { process.kill(pid, 0); return true; }
65
+ catch (error) { return error?.code === "EPERM"; }
66
+ }
67
+
68
+ async function activeState() {
69
+ let state;
70
+ for (let attempt = 0; attempt < 3; attempt++) {
71
+ try { state = JSON.parse(await readFile(statePath, "utf8")); break; }
72
+ catch (error) {
73
+ if (error?.code === "ENOENT") return null;
74
+ if (!(error instanceof SyntaxError)) throw error;
75
+ if (attempt < 2) { await new Promise((resolveWait) => setTimeout(resolveWait, 50)); continue; }
76
+ const age = Date.now() - (await stat(statePath)).mtimeMs;
77
+ if (age < 5000) return { starting: true };
78
+ await unlink(statePath).catch(() => {});
79
+ return null;
80
+ }
81
+ }
82
+ if (alive(state?.pid)) return state;
83
+ await unlink(statePath).catch(() => {});
84
+ return null;
85
+ }
86
+
87
+ async function saveState(state) {
88
+ const temporary = `${statePath}.${randomUUID()}.tmp`;
89
+ try {
90
+ await writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
91
+ await rename(temporary, statePath);
92
+ } finally { await unlink(temporary).catch(() => {}); }
93
+ }
94
+
95
+ function notifyParent(message) {
96
+ if (process.connected) process.send?.(message);
97
+ }
98
+
99
+ async function serve(pi, args) {
100
+ const nonce = randomUUID();
101
+ await mkdir(dirname(statePath), { recursive: true });
102
+ let claimed = false;
103
+ let lock;
104
+ try {
105
+ lock = await open(statePath, "wx", 0o600);
106
+ claimed = true;
107
+ await lock.writeFile(JSON.stringify({ pid: process.pid, nonce, cwd: process.cwd(), starting: true }));
108
+ } catch (error) {
109
+ if (claimed) await unlink(statePath).catch(() => {});
110
+ notifyParent({ type: "error", message: error?.code === "EEXIST" ? "pi-webapp 已在启动或运行" : String(error) });
111
+ return;
112
+ } finally { await lock?.close().catch(() => {}); }
113
+
114
+ const child = spawn(pi, ["--mode", "rpc", "-e", extension, ...args], {
115
+ cwd: process.cwd(), env: { ...process.env, PI_WEBAPP_AUTO_OPEN: "0" }, stdio: ["pipe", "pipe", "pipe"],
116
+ });
117
+ child.stderr?.pipe(process.stderr, { end: false });
118
+ let ready = false;
119
+ let opening = false;
120
+ let failed = false;
121
+ let closed = false;
122
+ let stateWrite = Promise.resolve();
123
+ let stopping = false;
124
+ let buffer = "";
125
+ let killTimer;
126
+ const shutdown = () => {
127
+ if (stopping) return;
128
+ stopping = true;
129
+ child.stdin?.end();
130
+ killTimer = setTimeout(() => child.kill("SIGTERM"), 5000);
131
+ killTimer.unref();
132
+ };
133
+ const fail = (message) => {
134
+ if (failed || ready) return;
135
+ failed = true;
136
+ notifyParent({ type: "error", message });
137
+ shutdown();
138
+ };
139
+ const startupTimer = setTimeout(() => fail(`Pi 启动超时。日志:${logPath}`), 60000);
140
+ const stopTimer = setInterval(async () => {
141
+ try { if ((await readFile(stopPath, "utf8")).trim() === nonce) shutdown(); }
142
+ catch { /* No stop request. */ }
143
+ }, 400);
144
+ process.on("SIGTERM", shutdown);
145
+ process.on("SIGINT", shutdown);
146
+ child.stdout?.on("data", (chunk) => {
147
+ if (ready || opening || failed) return;
148
+ buffer += chunk.toString("utf8");
149
+ if (buffer.length > 8 * 1024 * 1024) { fail("Pi RPC 输出超出预期大小"); return; }
150
+ for (let index; (index = buffer.indexOf("\n")) !== -1;) {
151
+ const line = buffer.slice(0, index);
152
+ buffer = buffer.slice(index + 1);
153
+ let record;
154
+ try { record = JSON.parse(line); } catch { continue; }
155
+ if (record?.type === "response" && record.id === "list-commands") {
156
+ if (record.success !== true || !Array.isArray(record.data?.commands)) {
157
+ fail("无法读取 Pi 扩展命令列表");
158
+ return;
159
+ }
160
+ const command = record.data.commands.find((item) => item?.source === "extension"
161
+ && item.sourceInfo?.path === extension && /^web(?::\d+)?$/.test(item.name));
162
+ if (!command) {
163
+ fail("Pi 没有加载当前项目的 Web 扩展");
164
+ return;
165
+ }
166
+ child.stdin?.write(JSON.stringify({ id: "open-web", type: "prompt", message: `/${command.name}` }) + "\n");
167
+ continue;
168
+ }
169
+ if (record?.type === "response" && record.id === "open-web" && record.success === false) {
170
+ fail(`Pi 拒绝打开网页:${record.error ?? "未知错误"}`);
171
+ return;
172
+ }
173
+ if (record?.type !== "extension_ui_request" || record.method !== "notify" || typeof record.message !== "string" || !record.message.startsWith("pi-webapp: ")) continue;
174
+ let url;
175
+ try { url = new URL(record.message.slice("pi-webapp: ".length)); }
176
+ catch { continue; }
177
+ if (url.hostname !== "127.0.0.1" || !url.hash) continue;
178
+ opening = true;
179
+ stateWrite = saveState({ pid: process.pid, piPid: child.pid, nonce, cwd: process.cwd(), url: url.href })
180
+ .then(() => {
181
+ if (closed || failed) return;
182
+ ready = true;
183
+ clearTimeout(startupTimer);
184
+ notifyParent({ type: "ready", pid: process.pid, url: url.href });
185
+ })
186
+ .catch((error) => fail(`无法保存启动状态:${error.message}`));
187
+ return;
188
+ }
189
+ });
190
+ child.stdin?.on("error", (error) => fail(`无法向 Pi 发送启动命令:${error.message}`));
191
+ child.stdin?.write(JSON.stringify({ id: "list-commands", type: "get_commands" }) + "\n");
192
+ await new Promise((resolveExit) => {
193
+ child.once("error", (error) => { fail(`无法启动 Pi:${error.message}`); resolveExit(); });
194
+ child.once("close", (code) => {
195
+ closed = true;
196
+ if (!ready) fail(`Pi 提前退出(代码 ${code ?? "未知"})。日志:${logPath}`);
197
+ resolveExit();
198
+ });
199
+ });
200
+ await stateWrite;
201
+ clearTimeout(startupTimer);
202
+ clearInterval(stopTimer);
203
+ clearTimeout(killTimer);
204
+ try { if ((await readFile(stopPath, "utf8")).trim() === nonce) await unlink(stopPath); } catch { /* Already removed. */ }
205
+ if (claimed) {
206
+ try { if (JSON.parse(await readFile(statePath, "utf8")).nonce === nonce) await unlink(statePath); }
207
+ catch { /* Already removed. */ }
208
+ }
209
+ }
210
+
211
+ async function start(args) {
212
+ const existing = await activeState();
213
+ if (existing) {
214
+ console.log(existing.url ? `pi-webapp 已在后台运行:${existing.url}` : "pi-webapp 正在启动");
215
+ if (existing.url) await openPageIfLocal(existing.url);
216
+ return;
217
+ }
218
+ const nodeVersion = versionParts(process.versions.node);
219
+ if (!nodeVersion || !compatible(nodeVersion, [22, 19, 0])) throw new Error("需要 Node.js 22.19 或更新版本。");
220
+ if (!existsSync(extension)) throw new Error("找不到构建后的扩展。请先运行 npm run build。");
221
+ if (args.some((arg) => ["--mode", "--print", "-p"].includes(arg))) throw new Error("后台启动不支持覆盖 Pi 的运行模式");
222
+ const pi = probe("pi") ?? installPi();
223
+ await mkdir(dirname(statePath), { recursive: true });
224
+ const logFd = openSync(logPath, "a", 0o600);
225
+ let worker;
226
+ try {
227
+ worker = spawnNode(process.execPath, [launcher, "__serve", pi, ...args], {
228
+ detached: true, cwd: process.cwd(), env: process.env, stdio: ["ignore", logFd, logFd, "ipc"],
229
+ });
230
+ } finally { closeSync(logFd); }
231
+ let ready;
232
+ try {
233
+ ready = await new Promise((resolveReady, rejectReady) => {
234
+ const timer = setTimeout(() => rejectReady(new Error(`后台启动超时。日志:${logPath}`)), 65000);
235
+ worker.once("message", (message) => {
236
+ clearTimeout(timer);
237
+ if (message?.type === "ready") resolveReady(message);
238
+ else rejectReady(new Error(message?.message ?? "后台启动失败"));
239
+ });
240
+ worker.once("error", rejectReady);
241
+ worker.once("exit", (code) => rejectReady(new Error(`后台进程退出(代码 ${code ?? "未知"})。日志:${logPath}`)));
242
+ });
243
+ } finally {
244
+ if (worker.connected) worker.disconnect();
245
+ worker.unref();
246
+ }
247
+ console.log(`pi-webapp 已在后台运行(PID ${ready.pid}):${ready.url}`);
248
+ console.log("停止服务:pi-webapp stop(源码目录中可运行 npm run stop)");
249
+ await openPageIfLocal(ready.url);
250
+ }
251
+
252
+ async function openPageIfLocal(url) {
253
+ if (process.env.PI_WEBAPP_AUTO_OPEN === "0") return;
254
+ try {
255
+ if (!await openWebPage(url)) console.log("当前没有本地图形桌面,请手动打开上方地址。");
256
+ } catch {
257
+ console.error("无法自动打开浏览器,请手动打开上方地址。");
258
+ }
259
+ }
260
+
261
+ async function stop() {
262
+ const state = await activeState();
263
+ if (!state) { console.log("pi-webapp 未运行"); return; }
264
+ if (!state.nonce) throw new Error("pi-webapp 正在启动,请稍后再停止");
265
+ await writeFile(stopPath, state.nonce, { mode: 0o600 });
266
+ for (let attempt = 0; attempt < 120; attempt++) {
267
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
268
+ const current = await activeState();
269
+ if (!current || current.nonce !== state.nonce) { console.log("pi-webapp 已停止"); return; }
270
+ }
271
+ throw new Error(`停止超时。后台进程 PID:${state.pid};日志:${logPath}`);
272
+ }
273
+
274
+ async function main() {
275
+ const [argument, ...rest] = process.argv.slice(2);
276
+ if (argument === "__serve") { await serve(rest[0], rest.slice(1)); return; }
277
+ if (argument === "stop" || argument === "status") {
278
+ if (rest.length) throw new Error(`用法:pi-webapp ${argument}`);
279
+ if (argument === "stop") await stop();
280
+ else {
281
+ const state = await activeState();
282
+ console.log(state ? `pi-webapp ${state.url ? `正在运行:${state.url}` : "正在启动"}` : "pi-webapp 未运行");
283
+ }
284
+ return;
285
+ }
286
+ if (argument === "--help" || argument === "-h") { console.log("用法:pi-webapp [start|status|stop] [Pi 参数]"); return; }
287
+ if (argument && argument !== "start" && !argument.startsWith("-")) throw new Error("用法:pi-webapp [start|status|stop] [Pi 参数]");
288
+ await start(argument === "start" || !argument ? rest : [argument, ...rest]);
289
+ }
290
+
291
+ main().catch((error) => {
292
+ console.error(`pi-webapp: ${error instanceof Error ? error.message : String(error)}`);
293
+ process.exitCode = 1;
294
+ });