@co0ontty/wand 4.63.0 → 4.64.0

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.
Files changed (83) hide show
  1. package/dist/build-info.d.ts +18 -0
  2. package/dist/build-info.js +13 -0
  3. package/dist/build-info.json +3 -3
  4. package/dist/config.js +6 -11
  5. package/dist/env-utils.d.ts +5 -0
  6. package/dist/env-utils.js +4 -0
  7. package/dist/git-quick-commit.js +7 -11
  8. package/dist/ios-ota.js +8 -29
  9. package/dist/layout-tree.d.ts +6 -0
  10. package/dist/layout-tree.js +9 -0
  11. package/dist/missions.js +2 -2
  12. package/dist/models.js +2 -5
  13. package/dist/path-repair.js +2 -1
  14. package/dist/process-manager.js +10 -20
  15. package/dist/provider-cli-updater.js +3 -6
  16. package/dist/pty-shell-launch.js +3 -5
  17. package/dist/server-app-connect.js +11 -46
  18. package/dist/server-github-routes.js +12 -11
  19. package/dist/server-local-preview-routes.js +6 -1
  20. package/dist/server-request.d.ts +4 -2
  21. package/dist/server-request.js +25 -4
  22. package/dist/server-session-routes.js +14 -78
  23. package/dist/server-settings-routes.d.ts +2 -6
  24. package/dist/server-settings-routes.js +3 -8
  25. package/dist/server-task-routes.d.ts +9 -6
  26. package/dist/server-task-routes.js +50 -43
  27. package/dist/server-update-routes.d.ts +2 -5
  28. package/dist/server-update-routes.js +42 -9
  29. package/dist/server-workspace-routes.js +4 -12
  30. package/dist/server.js +33 -99
  31. package/dist/session-ai-context.js +6 -38
  32. package/dist/session-provider.d.ts +16 -0
  33. package/dist/session-provider.js +49 -0
  34. package/dist/session-registry.d.ts +10 -0
  35. package/dist/session-registry.js +41 -14
  36. package/dist/session-topic.js +3 -11
  37. package/dist/shell-quote.d.ts +9 -0
  38. package/dist/shell-quote.js +13 -0
  39. package/dist/signal-utils.d.ts +10 -0
  40. package/dist/signal-utils.js +38 -0
  41. package/dist/storage.js +17 -55
  42. package/dist/structured-client-protocol.js +4 -8
  43. package/dist/structured-codex-protocol.js +1 -6
  44. package/dist/structured-content.d.ts +2 -0
  45. package/dist/structured-content.js +6 -0
  46. package/dist/structured-exec-host.d.ts +0 -2
  47. package/dist/structured-exec-host.js +2 -11
  48. package/dist/structured-exec-pump.js +2 -12
  49. package/dist/structured-grok-adapter.js +1 -5
  50. package/dist/structured-opencode-adapter.js +1 -5
  51. package/dist/structured-pi-adapter.js +9 -11
  52. package/dist/structured-provider-common.d.ts +11 -3
  53. package/dist/structured-provider-common.js +16 -23
  54. package/dist/structured-qoder-adapter.js +3 -8
  55. package/dist/structured-session-manager.js +6 -17
  56. package/dist/task-title.js +4 -12
  57. package/dist/task-types.d.ts +23 -1
  58. package/dist/task-types.js +26 -0
  59. package/dist/terminal-daemon-protocol.d.ts +0 -6
  60. package/dist/terminal-daemon-server.js +1 -9
  61. package/dist/text-utils.d.ts +7 -0
  62. package/dist/text-utils.js +15 -0
  63. package/dist/tui/attach.js +17 -93
  64. package/dist/tui/commands.d.ts +2 -0
  65. package/dist/tui/commands.js +3 -2
  66. package/dist/tui/index.js +28 -115
  67. package/dist/tui/runtime-utils.d.ts +7 -0
  68. package/dist/tui/runtime-utils.js +33 -0
  69. package/dist/tui/service-actions.d.ts +31 -0
  70. package/dist/tui/service-actions.js +86 -0
  71. package/dist/tui/snapshot.js +1 -8
  72. package/dist/update-helper.js +6 -35
  73. package/dist/web-ui/content/scripts.js +397 -497
  74. package/dist/web-ui/content/styles.css +1 -1
  75. package/dist/web-ui/content/tailwind.css +7210 -0
  76. package/dist/web-ui/embedded-assets.js +2 -2
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +4 -0
  79. package/dist/web-ui/provider-identity.js +9 -0
  80. package/dist/web-ui/styles.js +24 -7
  81. package/dist/workspace-binding.d.ts +2 -0
  82. package/dist/workspace-binding.js +4 -3
  83. package/package.json +9 -12
@@ -1,4 +1,3 @@
1
- import type { StructuredRunState, StructuredSpawnRequest } from "./structured-exec-host.js";
2
1
  import type { TerminalSessionState, TerminalSpawnRequest } from "./terminal-host.js";
3
2
  export declare const TERMINAL_DAEMON_PROTOCOL_VERSION = 2;
4
3
  export interface TerminalDaemonPaths {
@@ -36,11 +35,6 @@ export type TerminalDaemonEvent = {
36
35
  signal?: number;
37
36
  stream?: "stdout" | "stderr";
38
37
  };
39
- export interface TerminalDaemonStructuredAttachPayload {
40
- state: StructuredRunState | null;
41
- }
42
- export interface TerminalDaemonStructuredSpawnParams extends StructuredSpawnRequest {
43
- }
44
38
  export interface TerminalDaemonCreateParams extends TerminalSpawnRequest {
45
39
  afterSeq?: number;
46
40
  }
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { StringDecoder } from "node:string_decoder";
7
7
  import pty from "node-pty";
8
8
  import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
9
+ import { signalNumberFromName } from "./signal-utils.js";
9
10
  import { STRUCTURED_RUN_LOG_MAX_CHARS, createUtf8TextDecoder, } from "./structured-exec-host.js";
10
11
  import { TERMINAL_DAEMON_PROTOCOL_VERSION, terminalDaemonPaths, } from "./terminal-daemon-protocol.js";
11
12
  import { appendTerminalChunkWindow, } from "./terminal-host.js";
@@ -463,15 +464,6 @@ function isProcessAlive(pid) {
463
464
  return false;
464
465
  }
465
466
  }
466
- function signalNumberFromName(signalName) {
467
- const table = {
468
- SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6,
469
- SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12,
470
- SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18,
471
- SIGSTOP: 19, SIGTSTP: 20,
472
- };
473
- return table[signalName] ?? 0;
474
- }
475
467
  function waitForProcessExit(pid) {
476
468
  return new Promise((resolve) => {
477
469
  const timer = setInterval(() => {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 文本裁剪:超长时优先在空格处断句,避免标题被截成半句话。
3
+ *
4
+ * 只有当空格出现在靠后的位置(≥ 55%)才按单词断,否则直接硬截,
5
+ * 防止「一个很长的英文单词 + 少量尾巴」的场景退化成只剩单词头。
6
+ */
7
+ export declare function clipAtWordBoundary(value: string, maxLength: number): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 文本裁剪:超长时优先在空格处断句,避免标题被截成半句话。
3
+ *
4
+ * 只有当空格出现在靠后的位置(≥ 55%)才按单词断,否则直接硬截,
5
+ * 防止「一个很长的英文单词 + 少量尾巴」的场景退化成只剩单词头。
6
+ */
7
+ export function clipAtWordBoundary(value, maxLength) {
8
+ if (value.length <= maxLength)
9
+ return value;
10
+ const sliced = value.slice(0, maxLength);
11
+ const lastSpace = sliced.lastIndexOf(" ");
12
+ if (lastSpace >= Math.floor(maxLength * 0.55))
13
+ return sliced.slice(0, lastSpace);
14
+ return sliced;
15
+ }
@@ -5,11 +5,12 @@
5
5
  * 日志面板:因为日志在主进程里,attach 端没法直接看;这里改成"活动流"——
6
6
  * 监听 snapshot 差分,把会话起止 / 总数变化打到 log 面板。
7
7
  */
8
- import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, serviceRestart, uninstallService, } from "./commands.js";
9
- import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
8
+ import { copyToClipboard, openInBrowser, serviceRestart, } from "./commands.js";
10
9
  import { IpcClient } from "./ipc-client.js";
11
10
  import { buildLayout } from "./layout.js";
12
11
  import { openServicePanel } from "./service-panel.js";
12
+ import { runOffMicrotask, safeServiceInstalled } from "./runtime-utils.js";
13
+ import { createServiceActions } from "./service-actions.js";
13
14
  const POLL_INTERVAL_MS = 1000;
14
15
  export function startAttachTui(deps) {
15
16
  const layout = buildLayout();
@@ -118,6 +119,17 @@ export function startAttachTui(deps) {
118
119
  }
119
120
  const pollTimer = setInterval(() => { void pollOnce(); }, POLL_INTERVAL_MS);
120
121
  pollTimer.unref?.();
122
+ const serviceActions = createServiceActions({
123
+ configPath: deps.configPath,
124
+ version: deps.pidInfo.version,
125
+ layout,
126
+ updateConfirmBody: ({ current, latest, channelLabel }) => `通道 ${channelLabel}:当前 ${current} → 最新 ${latest},立即升级?升级后请按 R 重启服务。`,
127
+ installServiceBody: (isRoot) => process.platform === "linux"
128
+ ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,可以 Ctrl+C 退出 TUI 后跑 sudo wand service:install。"}`
129
+ : process.platform === "darwin"
130
+ ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,退出 TUI 跑 sudo wand service:install。"}`
131
+ : "当前平台暂不支持。",
132
+ });
121
133
  // —— 键位 —— 服务面板打开时,屏幕级快捷键让位
122
134
  const idle = () => !layout.isServicePanelOpen();
123
135
  layout.screen.key(["q", "Q"], () => { if (idle())
@@ -139,7 +151,7 @@ export function startAttachTui(deps) {
139
151
  layout.screen.key(["S-r"], () => { if (idle())
140
152
  void handleRestart(); });
141
153
  layout.screen.key(["u", "U"], () => { if (idle())
142
- void handleUpdate(); });
154
+ void serviceActions.handleUpdate(); });
143
155
  layout.screen.key(["o", "O"], () => {
144
156
  if (!idle())
145
157
  return;
@@ -153,9 +165,9 @@ export function startAttachTui(deps) {
153
165
  layout.showToast(r.message, r.ok ? "success" : "error", 2500);
154
166
  });
155
167
  layout.screen.key(["s"], () => { if (idle())
156
- void handleInstallService(); });
168
+ void serviceActions.handleInstallService(); });
157
169
  layout.screen.key(["S-s"], () => { if (idle())
158
- void handleUninstallService(); });
170
+ void serviceActions.handleUninstallService(); });
159
171
  async function handleRestart() {
160
172
  const installed = safeServiceInstalled();
161
173
  if (installed) {
@@ -182,74 +194,6 @@ export function startAttachTui(deps) {
182
194
  const accepted = await client.shutdownDaemon();
183
195
  layout.showToast(accepted ? "已请求主进程退出" : "请求未被接受", accepted ? "success" : "warn", 3500);
184
196
  }
185
- async function handleUpdate() {
186
- layout.showToast("正在检查更新…", "info", 2000);
187
- const channel = await runOffMicrotask(() => readUpdateChannel(deps.configPath));
188
- const info = await runOffMicrotask(() => checkUpdate(deps.pidInfo.version, channel));
189
- if (!info.latest) {
190
- layout.showToast(info.channel === "beta" ? "无法读取 npm beta 版本" : "无法连接到 npm registry", "error", 3500);
191
- return;
192
- }
193
- if (!info.hasUpdate) {
194
- layout.showToast(info.channel === "beta" ? `已是最新 Beta 版本 (${info.current})` : `已是最新版本 (v${info.current})`, "success", 3000);
195
- return;
196
- }
197
- const channelLabel = info.channel === "beta" ? "Beta" : "正式版";
198
- const go = await layout.confirm({
199
- title: "发现新版本",
200
- body: `通道 ${channelLabel}:当前 ${info.current} → 最新 ${info.latest},立即升级?升级后请按 R 重启服务。`,
201
- yes: "回车 / y 安装",
202
- no: "Esc / n 取消",
203
- });
204
- if (!go)
205
- return;
206
- layout.showToast("正在执行 npm install -g …", "info", 5000);
207
- const r = await runOffMicrotask(() => installUpdate(info.channel));
208
- layout.showToast(r.message, r.ok ? "success" : "error", 5000);
209
- if (r.detail)
210
- layout.showDetail(r.ok ? "更新输出" : "更新失败", r.detail);
211
- if (r.ok) {
212
- // 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),再按 R 重启服务生效。
213
- const repair = await runOffMicrotask(() => repairServiceUnitAfterUpdate(deps.configPath));
214
- if (repair.scope)
215
- layout.showToast(repair.message, repair.repaired ? "success" : "warn", 4500);
216
- }
217
- }
218
- async function handleInstallService() {
219
- if (isServiceInstalled()) {
220
- layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
221
- return;
222
- }
223
- const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
224
- const body = process.platform === "linux"
225
- ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,可以 Ctrl+C 退出 TUI 后跑 sudo wand service:install。"}`
226
- : process.platform === "darwin"
227
- ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,退出 TUI 跑 sudo wand service:install。"}`
228
- : "当前平台暂不支持。";
229
- const ok = await layout.confirm({ title: "注册为系统服务", body });
230
- if (!ok)
231
- return;
232
- const r = await runOffMicrotask(() => installService({ configPath: deps.configPath }));
233
- layout.showToast(r.message, r.ok ? "success" : "error", 5000);
234
- if (r.detail)
235
- layout.showDetail(r.ok ? "服务安装详情" : "服务安装失败", r.detail);
236
- }
237
- async function handleUninstallService() {
238
- if (!isServiceInstalled()) {
239
- layout.showToast("当前未安装系统服务", "warn", 2500);
240
- return;
241
- }
242
- const ok = await layout.confirm({
243
- title: "卸载系统服务",
244
- body: "将禁用并删除 wand 的 systemd / launchd 配置,确认继续?",
245
- });
246
- if (!ok)
247
- return;
248
- const r = await runOffMicrotask(() => uninstallService());
249
- layout.showToast(r.message, r.ok ? "success" : "error", 4000);
250
- if (r.detail)
251
- layout.showDetail(r.ok ? "服务卸载详情" : "服务卸载失败", r.detail);
252
- }
253
197
  async function stop() {
254
198
  if (stopping || !active)
255
199
  return;
@@ -273,23 +217,3 @@ export function startAttachTui(deps) {
273
217
  }
274
218
  return { stop };
275
219
  }
276
- function safeServiceInstalled() {
277
- try {
278
- return isServiceInstalled();
279
- }
280
- catch {
281
- return false;
282
- }
283
- }
284
- function runOffMicrotask(fn) {
285
- return new Promise((resolve, reject) => {
286
- setImmediate(() => {
287
- try {
288
- resolve(fn());
289
- }
290
- catch (err) {
291
- reject(err);
292
- }
293
- });
294
- });
295
- }
@@ -90,3 +90,5 @@ export declare function serviceRestart(opts?: ServiceOpts): CommandResult;
90
90
  export declare function serviceLogs(lines?: number, opts?: ServiceOpts): CommandResult;
91
91
  export declare function installService(ctx: ServiceContext): CommandResult;
92
92
  export declare function uninstallService(opts?: ServiceOpts): CommandResult;
93
+ /** 读取已安装服务 unit / plist 的入口命令;未安装或平台不支持时返回 null。 */
94
+ export declare function readServiceEntrypoint(scope: ServiceScope): string | null;
@@ -607,7 +607,8 @@ function hasLiveTerminalDaemon(configPath) {
607
607
  const pid = readTerminalDaemonPid(configPath);
608
608
  return pid !== null && isPidAlive(pid);
609
609
  }
610
- function readServiceEntrypointForUpdate(scope) {
610
+ /** 读取已安装服务 unit / plist 的入口命令;未安装或平台不支持时返回 null。 */
611
+ export function readServiceEntrypoint(scope) {
611
612
  const servicePath = servicePathFor(scope);
612
613
  if (process.platform === "darwin") {
613
614
  const result = spawnSync("plutil", ["-extract", "ProgramArguments.1", "raw", "-o", "-", servicePath], { encoding: "utf8", timeout: 5_000 });
@@ -635,7 +636,7 @@ function checkInstalledServiceEntrypointsForUpdate() {
635
636
  return { ok: false, message: "npm 全局 wand shim 不存在,已取消更新以保护当前服务。" };
636
637
  }
637
638
  for (const scope of scopes) {
638
- const entrypoint = readServiceEntrypointForUpdate(scope);
639
+ const entrypoint = readServiceEntrypoint(scope);
639
640
  if (!entrypoint || path.resolve(entrypoint) !== path.resolve(stableBin)) {
640
641
  const command = scope === "system"
641
642
  ? `sudo ${stableBin} service:install`
package/dist/tui/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, restartSelf, uninstallService, } from "./commands.js";
2
- import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
1
+ import { copyToClipboard, openInBrowser, restartSelf, } from "./commands.js";
3
2
  import { buildLayout } from "./layout.js";
4
3
  import { installLogBus, restoreLogBus } from "./log-bus.js";
5
4
  import { formatSession, sortRows } from "./session-formatter.js";
6
5
  import { openServicePanel } from "./service-panel.js";
6
+ import { safeRss, safeServiceInstalled } from "./runtime-utils.js";
7
+ import { createServiceActions } from "./service-actions.js";
7
8
  /** 触发 sessions 列表重渲的事件类型。output 太频繁,不订阅。 */
8
9
  const SESSIONS_REFRESH_EVENTS = new Set(["status", "started", "ended", "task"]);
9
10
  const RELATIVE_TIME_TICK_MS = 5_000;
@@ -103,6 +104,18 @@ export function startTui(deps) {
103
104
  tickTimer.unref?.();
104
105
  // 服务面板打开时,屏幕级快捷键全部让位给面板自身按键。
105
106
  const idle = () => !layout.isServicePanelOpen();
107
+ const serviceActions = createServiceActions({
108
+ configPath: deps.configPath,
109
+ version: deps.version,
110
+ layout,
111
+ updateConfirmBody: ({ current, latest, channelLabel }) => `通道 ${channelLabel}:当前 ${current} → 最新 ${latest},是否立即升级?`,
112
+ installServiceBody: (isRoot) => process.platform === "linux"
113
+ ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}\n不想要 root?退出 TUI 跑 wand service:install --user (登出会被回收)。`
114
+ : process.platform === "darwin"
115
+ ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}`
116
+ : "当前平台暂不支持。",
117
+ onServiceChanged: refreshAll,
118
+ });
106
119
  // —— 基本键位 ——
107
120
  layout.screen.key(["q", "Q"], () => { if (idle())
108
121
  void stop("user"); });
@@ -132,66 +145,18 @@ export function startTui(deps) {
132
145
  layout.screen.key(["S-r"], () => { if (idle())
133
146
  void handleRestart(); });
134
147
  layout.screen.key(["u", "U"], () => { if (idle())
135
- void handleUpdate(); });
148
+ void serviceActions.handleUpdate(); });
136
149
  layout.screen.key(["o", "O"], () => { if (idle())
137
150
  handleOpenBrowser(); });
138
151
  layout.screen.key(["c", "C"], () => { if (idle())
139
152
  handleCopyUrl(); });
140
153
  layout.screen.key(["s"], () => { if (idle())
141
- void handleInstallService(); });
154
+ void serviceActions.handleInstallService(); });
142
155
  layout.screen.key(["S-s"], () => { if (idle())
143
- void handleUninstallService(); });
156
+ void serviceActions.handleUninstallService(); });
144
157
  // 首次渲染
145
158
  refreshAll();
146
159
  // —— 操作处理函数 ——
147
- async function handleRestart() {
148
- const ok = await layout.confirm({
149
- title: "重启 wand",
150
- body: "将派生新进程并退出当前进程,活跃会话会因 PTY 中断而中止,是否继续?",
151
- });
152
- if (!ok)
153
- return;
154
- layout.showToast("正在重启…", "info", 5000);
155
- // 让 toast 有时间渲染,再触发 restart
156
- setTimeout(() => {
157
- const r = restartSelf();
158
- if (!r.ok)
159
- layout.showToast(r.message, "error", 4000);
160
- }, 200);
161
- }
162
- async function handleUpdate() {
163
- layout.showToast("正在检查更新…", "info", 2000);
164
- const channel = await runOffMicrotask(() => readUpdateChannel(deps.configPath));
165
- const info = await runOffMicrotask(() => checkUpdate(deps.version, channel));
166
- if (!info.latest) {
167
- layout.showToast(info.channel === "beta" ? "无法读取 npm beta 版本" : "无法连接到 npm registry", "error", 3500);
168
- return;
169
- }
170
- if (!info.hasUpdate) {
171
- layout.showToast(info.channel === "beta" ? `已是最新 Beta 版本 (${info.current})` : `已是最新版本 (v${info.current})`, "success", 3000);
172
- return;
173
- }
174
- const channelLabel = info.channel === "beta" ? "Beta" : "正式版";
175
- const go = await layout.confirm({
176
- title: "发现新版本",
177
- body: `通道 ${channelLabel}:当前 ${info.current} → 最新 ${info.latest},是否立即升级?`,
178
- yes: "回车 / y 安装",
179
- no: "Esc / n 取消",
180
- });
181
- if (!go)
182
- return;
183
- layout.showToast("正在执行 npm install -g …", "info", 5000);
184
- const r = await runOffMicrotask(() => installUpdate(info.channel));
185
- layout.showToast(r.message, r.ok ? "success" : "error", 5000);
186
- if (r.detail)
187
- layout.showDetail(r.ok ? "更新输出" : "更新失败", r.detail);
188
- if (r.ok) {
189
- // 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),再按 R 重启生效。
190
- const repair = await runOffMicrotask(() => repairServiceUnitAfterUpdate(deps.configPath));
191
- if (repair.scope)
192
- layout.showToast(repair.message, repair.repaired ? "success" : "warn", 4500);
193
- }
194
- }
195
160
  function handleOpenBrowser() {
196
161
  const url = deps.urls[0]?.url;
197
162
  if (!url) {
@@ -210,43 +175,20 @@ export function startTui(deps) {
210
175
  const r = copyToClipboard(url);
211
176
  layout.showToast(r.message, r.ok ? "success" : "error", 2500);
212
177
  }
213
- async function handleInstallService() {
214
- if (isServiceInstalled()) {
215
- layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
216
- return;
217
- }
218
- // 默认装 system-wide。非 root 时 installService 会返回明确错误,toast 自然展示。
219
- const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
220
- const body = process.platform === "linux"
221
- ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}\n不想要 root?退出 TUI 跑 wand service:install --user (登出会被回收)。`
222
- : process.platform === "darwin"
223
- ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,请退出 TUI 后直接跑 sudo wand service:install。"}`
224
- : "当前平台暂不支持。";
225
- const ok = await layout.confirm({ title: "注册为系统服务", body });
226
- if (!ok)
227
- return;
228
- const r = await runOffMicrotask(() => installService({ configPath: deps.configPath }));
229
- layout.showToast(r.message, r.ok ? "success" : "error", 5000);
230
- if (r.detail)
231
- layout.showDetail(r.ok ? "服务安装详情" : "服务安装失败", r.detail);
232
- refreshAll();
233
- }
234
- async function handleUninstallService() {
235
- if (!isServiceInstalled()) {
236
- layout.showToast("当前未安装系统服务", "warn", 2500);
237
- return;
238
- }
178
+ async function handleRestart() {
239
179
  const ok = await layout.confirm({
240
- title: "卸载系统服务",
241
- body: "将禁用并删除 wand 的 systemd / launchd 配置,确认继续?",
180
+ title: "重启 wand",
181
+ body: "将派生新进程并退出当前进程,活跃会话会因 PTY 中断而中止,是否继续?",
242
182
  });
243
183
  if (!ok)
244
184
  return;
245
- const r = await runOffMicrotask(() => uninstallService());
246
- layout.showToast(r.message, r.ok ? "success" : "error", 4000);
247
- if (r.detail)
248
- layout.showDetail(r.ok ? "服务卸载详情" : "服务卸载失败", r.detail);
249
- refreshAll();
185
+ layout.showToast("正在重启…", "info", 5000);
186
+ // 让 toast 有时间渲染,再触发 restart
187
+ setTimeout(() => {
188
+ const r = restartSelf();
189
+ if (!r.ok)
190
+ layout.showToast(r.message, "error", 4000);
191
+ }, 200);
250
192
  }
251
193
  async function stop(reason) {
252
194
  if (stopping || !active)
@@ -284,32 +226,3 @@ function safeStructuredList(mgr) {
284
226
  return [];
285
227
  }
286
228
  }
287
- function safeRss() {
288
- try {
289
- return process.memoryUsage().rss;
290
- }
291
- catch {
292
- return 0;
293
- }
294
- }
295
- function safeServiceInstalled() {
296
- try {
297
- return isServiceInstalled();
298
- }
299
- catch {
300
- return false;
301
- }
302
- }
303
- /** 把同步阻塞操作放到下一 microtask,给 TUI 留出一帧把 toast 画出来。 */
304
- function runOffMicrotask(fn) {
305
- return new Promise((resolve, reject) => {
306
- setImmediate(() => {
307
- try {
308
- resolve(fn());
309
- }
310
- catch (err) {
311
- reject(err);
312
- }
313
- });
314
- });
315
- }
@@ -0,0 +1,7 @@
1
+ /** TUI 运行时探针与小工具,供本地 TUI / attach TUI / snapshot 共享。 */
2
+ /** 读取进程 RSS;失败时返回 0,避免探针异常影响渲染。 */
3
+ export declare function safeRss(): number;
4
+ /** 查询服务安装状态;失败时返回 false。 */
5
+ export declare function safeServiceInstalled(): boolean;
6
+ /** 把同步阻塞操作放到下一 microtask,给 TUI 留出一帧把 toast 画出来。 */
7
+ export declare function runOffMicrotask<T>(fn: () => T): Promise<T>;
@@ -0,0 +1,33 @@
1
+ /** TUI 运行时探针与小工具,供本地 TUI / attach TUI / snapshot 共享。 */
2
+ import { isServiceInstalled } from "./commands.js";
3
+ /** 读取进程 RSS;失败时返回 0,避免探针异常影响渲染。 */
4
+ export function safeRss() {
5
+ try {
6
+ return process.memoryUsage().rss;
7
+ }
8
+ catch {
9
+ return 0;
10
+ }
11
+ }
12
+ /** 查询服务安装状态;失败时返回 false。 */
13
+ export function safeServiceInstalled() {
14
+ try {
15
+ return isServiceInstalled();
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /** 把同步阻塞操作放到下一 microtask,给 TUI 留出一帧把 toast 画出来。 */
22
+ export function runOffMicrotask(fn) {
23
+ return new Promise((resolve, reject) => {
24
+ setImmediate(() => {
25
+ try {
26
+ resolve(fn());
27
+ }
28
+ catch (err) {
29
+ reject(err);
30
+ }
31
+ });
32
+ });
33
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 两种 TUI 模式(前台主进程 / attach)共用的"服务与升级"操作。
3
+ *
4
+ * 流程完全一致,只有两处随模式而异:
5
+ * - 升级确认弹窗文案(attach 模式要提醒按 R 重启服务)
6
+ * - 注册系统服务的说明正文(attach 模式没有本地会话列表,装卸后不刷新界面)
7
+ */
8
+ import type { LayoutHandle } from "./layout.js";
9
+ export interface UpdateConfirmContext {
10
+ current: string;
11
+ latest: string;
12
+ channelLabel: string;
13
+ }
14
+ export interface ServiceActionsOptions {
15
+ configPath: string;
16
+ /** 当前版本;attach 模式传主进程版本。 */
17
+ version: string;
18
+ layout: LayoutHandle;
19
+ /** 升级确认弹窗正文。 */
20
+ updateConfirmBody(context: UpdateConfirmContext): string;
21
+ /** 注册系统服务的说明正文,随平台 / 权限而变。 */
22
+ installServiceBody(isRoot: boolean): string;
23
+ /** 服务安装 / 卸载后刷新界面;attach 模式没有本地会话列表,可不传。 */
24
+ onServiceChanged?(): void;
25
+ }
26
+ export interface ServiceActions {
27
+ handleUpdate(): Promise<void>;
28
+ handleInstallService(): Promise<void>;
29
+ handleUninstallService(): Promise<void>;
30
+ }
31
+ export declare function createServiceActions(options: ServiceActionsOptions): ServiceActions;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 两种 TUI 模式(前台主进程 / attach)共用的"服务与升级"操作。
3
+ *
4
+ * 流程完全一致,只有两处随模式而异:
5
+ * - 升级确认弹窗文案(attach 模式要提醒按 R 重启服务)
6
+ * - 注册系统服务的说明正文(attach 模式没有本地会话列表,装卸后不刷新界面)
7
+ */
8
+ import { checkUpdate, installService, installUpdate, isServiceInstalled, readUpdateChannel, uninstallService, } from "./commands.js";
9
+ import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
10
+ import { runOffMicrotask } from "./runtime-utils.js";
11
+ export function createServiceActions(options) {
12
+ const { layout } = options;
13
+ async function handleUpdate() {
14
+ layout.showToast("正在检查更新…", "info", 2000);
15
+ const channel = await runOffMicrotask(() => readUpdateChannel(options.configPath));
16
+ const info = await runOffMicrotask(() => checkUpdate(options.version, channel));
17
+ if (!info.latest) {
18
+ layout.showToast(info.channel === "beta" ? "无法读取 npm beta 版本" : "无法连接到 npm registry", "error", 3500);
19
+ return;
20
+ }
21
+ if (!info.hasUpdate) {
22
+ layout.showToast(info.channel === "beta" ? `已是最新 Beta 版本 (${info.current})` : `已是最新版本 (v${info.current})`, "success", 3000);
23
+ return;
24
+ }
25
+ const go = await layout.confirm({
26
+ title: "发现新版本",
27
+ body: options.updateConfirmBody({
28
+ current: info.current,
29
+ latest: info.latest,
30
+ channelLabel: info.channel === "beta" ? "Beta" : "正式版",
31
+ }),
32
+ yes: "回车 / y 安装",
33
+ no: "Esc / n 取消",
34
+ });
35
+ if (!go)
36
+ return;
37
+ layout.showToast("正在执行 npm install -g …", "info", 5000);
38
+ const r = await runOffMicrotask(() => installUpdate(info.channel));
39
+ layout.showToast(r.message, r.ok ? "success" : "error", 5000);
40
+ if (r.detail)
41
+ layout.showDetail(r.ok ? "更新输出" : "更新失败", r.detail);
42
+ if (r.ok) {
43
+ // 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),再按 R 重启生效。
44
+ const repair = await runOffMicrotask(() => repairServiceUnitAfterUpdate(options.configPath));
45
+ if (repair.scope)
46
+ layout.showToast(repair.message, repair.repaired ? "success" : "warn", 4500);
47
+ }
48
+ }
49
+ async function handleInstallService() {
50
+ if (isServiceInstalled()) {
51
+ layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
52
+ return;
53
+ }
54
+ // 默认装 system-wide。非 root 时 installService 会返回明确错误,toast 自然展示。
55
+ const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
56
+ const ok = await layout.confirm({
57
+ title: "注册为系统服务",
58
+ body: options.installServiceBody(isRoot),
59
+ });
60
+ if (!ok)
61
+ return;
62
+ const r = await runOffMicrotask(() => installService({ configPath: options.configPath }));
63
+ layout.showToast(r.message, r.ok ? "success" : "error", 5000);
64
+ if (r.detail)
65
+ layout.showDetail(r.ok ? "服务安装详情" : "服务安装失败", r.detail);
66
+ options.onServiceChanged?.();
67
+ }
68
+ async function handleUninstallService() {
69
+ if (!isServiceInstalled()) {
70
+ layout.showToast("当前未安装系统服务", "warn", 2500);
71
+ return;
72
+ }
73
+ const ok = await layout.confirm({
74
+ title: "卸载系统服务",
75
+ body: "将禁用并删除 wand 的 systemd / launchd 配置,确认继续?",
76
+ });
77
+ if (!ok)
78
+ return;
79
+ const r = await runOffMicrotask(() => uninstallService());
80
+ layout.showToast(r.message, r.ok ? "success" : "error", 4000);
81
+ if (r.detail)
82
+ layout.showDetail(r.ok ? "服务卸载详情" : "服务卸载失败", r.detail);
83
+ options.onServiceChanged?.();
84
+ }
85
+ return { handleUpdate, handleInstallService, handleUninstallService };
86
+ }
@@ -5,6 +5,7 @@
5
5
  * processManager / structuredSessions(避免一次额外的对象拷贝)。
6
6
  */
7
7
  import { formatSession, sortRows } from "./session-formatter.js";
8
+ import { safeRss } from "./runtime-utils.js";
8
9
  export function buildSnapshotData(inputs) {
9
10
  const ptyList = inputs.processManager.listSlim();
10
11
  let structuredList = [];
@@ -48,11 +49,3 @@ export function buildSnapshotData(inputs) {
48
49
  sessions: sorted.map((s) => formatSession(s)),
49
50
  };
50
51
  }
51
- function safeRss() {
52
- try {
53
- return process.memoryUsage().rss;
54
- }
55
- catch {
56
- return 0;
57
- }
58
- }