pi-web-ui 0.2.10 → 0.2.12

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
@@ -420,6 +420,12 @@ function serviceEnv(port, cwd, dataDir) {
420
420
  // Interactive Windows tasks inherit the user's PATH; only systemd/launchd
421
421
  // run with a minimal environment that needs an explicit PATH.
422
422
  if (!isWin) env.PATH = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
423
+ // Same for the locale: launchd/systemd drop LANG/LC_ALL, and a C-locale
424
+ // shell garbles multibyte input in the terminal (UTF-8 continuation
425
+ // bytes 0x80–0x9F rendered as C1 control chars). Bake the installing
426
+ // shell's locale into the service env so spawned terminals are UTF-8.
427
+ if (!isWin && process.env.LANG) env.LANG = process.env.LANG;
428
+ if (!isWin && process.env.LC_ALL) env.LC_ALL = process.env.LC_ALL;
423
429
  if (dataDir) env.PI_WEB_DATA_DIR = dataDir;
424
430
  return env;
425
431
  }
@@ -13,10 +13,11 @@
13
13
  * `${pwd}` inside cwd/command resolves to the agent session's current working
14
14
  * directory (the same directory the agent operates in — see set_cwd).
15
15
  */
16
- import { existsSync } from "node:fs";
16
+ import { chmodSync, existsSync, readdirSync, statSync } from "node:fs";
17
17
  import { mkdir, readFile, writeFile } from "node:fs/promises";
18
+ import { createRequire } from "node:module";
18
19
  import { homedir } from "node:os";
19
- import { isAbsolute, join, resolve } from "node:path";
20
+ import { dirname, isAbsolute, join, resolve } from "node:path";
20
21
  import { spawn } from "node-pty";
21
22
  /** Location of the command list for a project: <workspaceRoot>/.pi/commands.json */
22
23
  export function commandsFilePath(workspaceRoot) {
@@ -112,6 +113,82 @@ const SHELL = isWindows
112
113
  : process.env.SHELL || "bash";
113
114
  /** `-i` is POSIX-only; cmd.exe / powershell.exe start interactively on their own. */
114
115
  const SHELL_ARGS = isWindows ? [] : ["-i"];
116
+ /**
117
+ * Environment for spawned shells. System services (launchd/systemd) run with
118
+ * no locale variables, which puts the shell in the C locale: its line editor
119
+ * then renders UTF-8 continuation bytes 0x80–0x9F as C1 control characters
120
+ * (e.g. `�<0091><0098>` for 员), garbling Chinese input in the terminal.
121
+ * Default a UTF-8 locale so multibyte text round-trips.
122
+ */
123
+ function shellEnv() {
124
+ const env = { ...process.env, TERM: "xterm-256color" };
125
+ if (!env.LANG && !env.LC_ALL)
126
+ env.LANG = "en_US.UTF-8";
127
+ return env;
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // spawn-helper permission repair (node-pty macOS prebuilds)
131
+ // ---------------------------------------------------------------------------
132
+ // node-pty 1.1.0 publishes its macOS prebuilds with `spawn-helper` lacking the
133
+ // execute bit (mode 0644 in the npm tarball), so posix_spawn fails with EACCES
134
+ // and node-pty throws the generic "posix_spawnp failed". Locally-built
135
+ // copies (build/Release) are fine; every `npm install` that picks the prebuild
136
+ // — e.g. `npm i -g pi-web-ui`, which is what system-service installs run — is
137
+ // broken until the bit is restored. Self-heal at startup, best-effort.
138
+ const require = createRequire(import.meta.url);
139
+ /** Absolute paths of every node-pty spawn-helper this install can exec. */
140
+ function spawnHelperPaths() {
141
+ try {
142
+ // require.resolve("node-pty") → <pkg>/lib/index.js → package root is two up.
143
+ const pkgDir = dirname(dirname(require.resolve("node-pty")));
144
+ const out = [];
145
+ const built = join(pkgDir, "build", "Release", "spawn-helper");
146
+ if (existsSync(built))
147
+ out.push(built);
148
+ const prebuildsDir = join(pkgDir, "prebuilds");
149
+ if (existsSync(prebuildsDir)) {
150
+ for (const entry of readdirSync(prebuildsDir)) {
151
+ const p = join(prebuildsDir, entry, "spawn-helper");
152
+ if (existsSync(p))
153
+ out.push(p);
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ }
162
+ /** Restore the +x bit on node-pty's spawn-helper binaries (idempotent). */
163
+ function repairSpawnHelperPermissions() {
164
+ if (isWindows)
165
+ return;
166
+ for (const p of spawnHelperPaths()) {
167
+ try {
168
+ if ((statSync(p).mode & 0o111) === 0)
169
+ chmodSync(p, 0o755);
170
+ }
171
+ catch {
172
+ // best-effort; a read-only node_modules just keeps the old failure
173
+ }
174
+ }
175
+ }
176
+ repairSpawnHelperPermissions();
177
+ /** Path of a still-broken helper, for the error hint ("" when none). */
178
+ function brokenSpawnHelper() {
179
+ if (isWindows)
180
+ return "";
181
+ for (const p of spawnHelperPaths()) {
182
+ try {
183
+ if ((statSync(p).mode & 0o111) === 0)
184
+ return p;
185
+ }
186
+ catch {
187
+ // ignore
188
+ }
189
+ }
190
+ return "";
191
+ }
115
192
  /**
116
193
  * Owns one or more PTYs for a client. All output is forwarded as
117
194
  * `terminal_output` messages through the provided emit (broadcast to every
@@ -188,11 +265,14 @@ export class TerminalManager {
188
265
  cols: Math.max(2, Math.floor(cols) || 80),
189
266
  rows: Math.max(2, Math.floor(rows) || 24),
190
267
  cwd: abs,
191
- env: { ...process.env, TERM: "xterm-256color" },
268
+ env: shellEnv(),
192
269
  });
193
270
  }
194
271
  catch (err) {
195
- this.fail(id, `启动终端失败:${err.message}`);
272
+ const helper = brokenSpawnHelper();
273
+ this.fail(id, helper
274
+ ? `启动终端失败:${err.message}(node-pty 的 spawn-helper 缺少执行权限,请运行:chmod +x "${helper}")`
275
+ : `启动终端失败:${err.message}`);
196
276
  return false;
197
277
  }
198
278
  const entry = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",