@co0ontty/wand 4.63.0 → 4.64.0-beta.g3402a64
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/dist/build-info.d.ts +18 -0
- package/dist/build-info.js +13 -0
- package/dist/build-info.json +4 -4
- package/dist/config.js +6 -11
- package/dist/env-utils.d.ts +5 -0
- package/dist/env-utils.js +4 -0
- package/dist/git-quick-commit.js +7 -11
- package/dist/ios-ota.js +8 -29
- package/dist/layout-tree.d.ts +6 -0
- package/dist/layout-tree.js +9 -0
- package/dist/missions.js +2 -2
- package/dist/models.js +2 -5
- package/dist/path-repair.js +2 -1
- package/dist/process-manager.js +10 -20
- package/dist/provider-cli-updater.js +3 -6
- package/dist/pty-shell-launch.js +3 -5
- package/dist/server-app-connect.js +11 -46
- package/dist/server-github-routes.js +12 -11
- package/dist/server-local-preview-routes.js +6 -1
- package/dist/server-request.d.ts +4 -2
- package/dist/server-request.js +25 -4
- package/dist/server-session-routes.js +14 -78
- package/dist/server-settings-routes.d.ts +2 -6
- package/dist/server-settings-routes.js +3 -8
- package/dist/server-task-routes.d.ts +9 -6
- package/dist/server-task-routes.js +50 -43
- package/dist/server-update-routes.d.ts +2 -5
- package/dist/server-update-routes.js +42 -9
- package/dist/server-workspace-routes.js +4 -12
- package/dist/server.js +33 -99
- package/dist/session-ai-context.js +6 -38
- package/dist/session-provider.d.ts +16 -0
- package/dist/session-provider.js +49 -0
- package/dist/session-registry.d.ts +10 -0
- package/dist/session-registry.js +41 -14
- package/dist/session-topic.js +3 -11
- package/dist/shell-quote.d.ts +9 -0
- package/dist/shell-quote.js +13 -0
- package/dist/signal-utils.d.ts +10 -0
- package/dist/signal-utils.js +38 -0
- package/dist/storage.js +17 -55
- package/dist/structured-client-protocol.js +4 -8
- package/dist/structured-codex-protocol.js +1 -6
- package/dist/structured-content.d.ts +2 -0
- package/dist/structured-content.js +6 -0
- package/dist/structured-exec-host.d.ts +0 -2
- package/dist/structured-exec-host.js +2 -11
- package/dist/structured-exec-pump.js +2 -12
- package/dist/structured-grok-adapter.js +1 -5
- package/dist/structured-opencode-adapter.js +1 -5
- package/dist/structured-pi-adapter.js +9 -11
- package/dist/structured-provider-common.d.ts +11 -3
- package/dist/structured-provider-common.js +16 -23
- package/dist/structured-qoder-adapter.js +3 -8
- package/dist/structured-session-manager.js +6 -17
- package/dist/task-title.js +4 -12
- package/dist/task-types.d.ts +23 -1
- package/dist/task-types.js +26 -0
- package/dist/terminal-daemon-protocol.d.ts +0 -6
- package/dist/terminal-daemon-server.js +1 -9
- package/dist/text-utils.d.ts +7 -0
- package/dist/text-utils.js +15 -0
- package/dist/tui/attach.js +17 -93
- package/dist/tui/commands.d.ts +2 -0
- package/dist/tui/commands.js +3 -2
- package/dist/tui/index.js +28 -115
- package/dist/tui/runtime-utils.d.ts +7 -0
- package/dist/tui/runtime-utils.js +33 -0
- package/dist/tui/service-actions.d.ts +31 -0
- package/dist/tui/service-actions.js +86 -0
- package/dist/tui/snapshot.js +1 -8
- package/dist/update-helper.js +6 -35
- package/dist/web-ui/content/scripts.js +397 -497
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +7210 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +4 -0
- package/dist/web-ui/provider-identity.js +9 -0
- package/dist/web-ui/styles.js +24 -7
- package/dist/workspace-binding.d.ts +2 -0
- package/dist/workspace-binding.js +4 -3
- 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,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
|
+
}
|
package/dist/tui/attach.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* 日志面板:因为日志在主进程里,attach 端没法直接看;这里改成"活动流"——
|
|
6
6
|
* 监听 snapshot 差分,把会话起止 / 总数变化打到 log 面板。
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
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
|
-
}
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -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;
|
package/dist/tui/commands.js
CHANGED
|
@@ -607,7 +607,8 @@ function hasLiveTerminalDaemon(configPath) {
|
|
|
607
607
|
const pid = readTerminalDaemonPid(configPath);
|
|
608
608
|
return pid !== null && isPidAlive(pid);
|
|
609
609
|
}
|
|
610
|
-
|
|
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 =
|
|
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 {
|
|
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
|
|
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: "
|
|
180
|
+
title: "重启 wand",
|
|
181
|
+
body: "将派生新进程并退出当前进程,活跃会话会因 PTY 中断而中止,是否继续?",
|
|
242
182
|
});
|
|
243
183
|
if (!ok)
|
|
244
184
|
return;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
package/dist/tui/snapshot.js
CHANGED
|
@@ -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
|
-
}
|