@co0ontty/wand 2.3.1 → 2.3.2
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.json +3 -3
- package/dist/cli.js +50 -6
- package/dist/relaunch.d.ts +3 -2
- package/dist/relaunch.js +5 -3
- package/dist/structured-session-manager.js +59 -15
- package/dist/tui/attach.js +8 -27
- package/dist/tui/commands.js +129 -38
- package/dist/types.d.ts +1 -1
- package/dist/web-ui/content/scripts.js +29 -29
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "2.3.
|
|
2
|
+
"commit": "066cdba4a30243d0950ceeb4988754f198bec6db",
|
|
3
|
+
"builtAt": "2026-07-04T05:33:22.797Z",
|
|
4
|
+
"version": "2.3.2",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { request as httpsRequest } from "node:https";
|
|
|
7
7
|
import net from "node:net";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
10
|
-
import { isPidAlive,
|
|
10
|
+
import { isPidAlive, readPidfile, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
|
|
11
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
12
|
async function main() {
|
|
13
13
|
const args = process.argv.slice(2);
|
|
@@ -392,9 +392,29 @@ async function handlePortInUse(config, configPath, useTui) {
|
|
|
392
392
|
return true;
|
|
393
393
|
}
|
|
394
394
|
async function discoverAttachableInstance(configPath) {
|
|
395
|
-
const live =
|
|
396
|
-
if (live)
|
|
397
|
-
|
|
395
|
+
const live = readPidfile(configPath);
|
|
396
|
+
if (live && live.pid !== process.pid) {
|
|
397
|
+
if (!isPidAlive(live.pid)) {
|
|
398
|
+
removePidfile(configPath);
|
|
399
|
+
removeSocketFile(configPath);
|
|
400
|
+
}
|
|
401
|
+
else if (live.socket && existsSync(live.socket)) {
|
|
402
|
+
const snapshot = await readIpcSnapshot(live.socket);
|
|
403
|
+
if (snapshot &&
|
|
404
|
+
snapshot.header.pid === live.pid &&
|
|
405
|
+
snapshot.header.configPath === configPath) {
|
|
406
|
+
return pidInfoFromSnapshot(snapshot);
|
|
407
|
+
}
|
|
408
|
+
if (!pidCommandLooksLikeWand(live.pid, configPath)) {
|
|
409
|
+
removePidfile(configPath);
|
|
410
|
+
removeSocketFile(configPath);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (!pidCommandLooksLikeWand(live.pid, configPath)) {
|
|
414
|
+
removePidfile(configPath);
|
|
415
|
+
removeSocketFile(configPath);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
398
418
|
const sockPath = socketPath(configPath);
|
|
399
419
|
if (!sockPath || !existsSync(sockPath))
|
|
400
420
|
return null;
|
|
@@ -404,8 +424,13 @@ async function discoverAttachableInstance(configPath) {
|
|
|
404
424
|
const pid = snapshot.header.pid;
|
|
405
425
|
if (!isPidAlive(pid))
|
|
406
426
|
return null;
|
|
427
|
+
if (snapshot.header.configPath !== configPath)
|
|
428
|
+
return null;
|
|
429
|
+
return pidInfoFromSnapshot(snapshot);
|
|
430
|
+
}
|
|
431
|
+
function pidInfoFromSnapshot(snapshot) {
|
|
407
432
|
return {
|
|
408
|
-
pid,
|
|
433
|
+
pid: snapshot.header.pid,
|
|
409
434
|
version: snapshot.header.version,
|
|
410
435
|
startedAt: snapshot.header.startedAtMs,
|
|
411
436
|
url: snapshot.header.url,
|
|
@@ -413,9 +438,28 @@ async function discoverAttachableInstance(configPath) {
|
|
|
413
438
|
bindAddr: snapshot.header.bindAddr,
|
|
414
439
|
configPath: snapshot.header.configPath,
|
|
415
440
|
dbPath: snapshot.header.dbPath,
|
|
416
|
-
socket:
|
|
441
|
+
socket: socketPath(snapshot.header.configPath),
|
|
417
442
|
};
|
|
418
443
|
}
|
|
444
|
+
function pidCommandLooksLikeWand(pid, configPath) {
|
|
445
|
+
try {
|
|
446
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
447
|
+
encoding: "utf8",
|
|
448
|
+
timeout: 1000,
|
|
449
|
+
});
|
|
450
|
+
if (result.status !== 0)
|
|
451
|
+
return false;
|
|
452
|
+
const command = result.stdout || "";
|
|
453
|
+
return (command.includes(configPath) &&
|
|
454
|
+
(/\bwand\b/.test(command) ||
|
|
455
|
+
command.includes("/wand ") ||
|
|
456
|
+
command.includes("/cli.js web") ||
|
|
457
|
+
command.includes("src/cli.ts web")));
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
419
463
|
function readIpcSnapshot(sockPath) {
|
|
420
464
|
return new Promise((resolve) => {
|
|
421
465
|
const sock = net.createConnection({ path: sockPath });
|
package/dist/relaunch.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export interface RelaunchPlan {
|
|
|
18
18
|
/**
|
|
19
19
|
* 计算重启方式。
|
|
20
20
|
*
|
|
21
|
-
* - systemd
|
|
22
|
-
* (更新自修复后可能刚被重写的)ExecStart
|
|
21
|
+
* - systemd / launchd 托管且已装服务 → "exit-only":仅退出,由 unit/plist 里
|
|
22
|
+
* (更新自修复后可能刚被重写的)ExecStart/ProgramArguments 重新拉起,
|
|
23
|
+
* 避免 spawn 抢 pidfile 的竞态。
|
|
23
24
|
* - 否则 → "spawn":bin 优先用刚装好的全局 CLI(更新后能跑到新版),回退 argv[1]。
|
|
24
25
|
*/
|
|
25
26
|
export declare function computeRelaunch(opts: {
|
package/dist/relaunch.js
CHANGED
|
@@ -2,13 +2,15 @@ import process from "node:process";
|
|
|
2
2
|
/**
|
|
3
3
|
* 计算重启方式。
|
|
4
4
|
*
|
|
5
|
-
* - systemd
|
|
6
|
-
* (更新自修复后可能刚被重写的)ExecStart
|
|
5
|
+
* - systemd / launchd 托管且已装服务 → "exit-only":仅退出,由 unit/plist 里
|
|
6
|
+
* (更新自修复后可能刚被重写的)ExecStart/ProgramArguments 重新拉起,
|
|
7
|
+
* 避免 spawn 抢 pidfile 的竞态。
|
|
7
8
|
* - 否则 → "spawn":bin 优先用刚装好的全局 CLI(更新后能跑到新版),回退 argv[1]。
|
|
8
9
|
*/
|
|
9
10
|
export function computeRelaunch(opts) {
|
|
10
11
|
const managedBySystemd = !!process.env.INVOCATION_ID;
|
|
11
|
-
|
|
12
|
+
const managedByLaunchd = process.platform === "darwin" && process.env.XPC_SERVICE_NAME === "com.wand.web";
|
|
13
|
+
if ((managedBySystemd || managedByLaunchd) && opts.serviceInstalled) {
|
|
12
14
|
return { mode: "exit-only" };
|
|
13
15
|
}
|
|
14
16
|
const bin = opts.globalCli ?? process.argv[1] ?? "";
|
|
@@ -65,7 +65,7 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
|
65
65
|
switch (effort) {
|
|
66
66
|
case "standard": return "low";
|
|
67
67
|
case "deep": return "medium";
|
|
68
|
-
case "max": return "
|
|
68
|
+
case "max": return "xhigh";
|
|
69
69
|
case "off": return "minimal";
|
|
70
70
|
default: return null;
|
|
71
71
|
}
|
|
@@ -1128,7 +1128,7 @@ export class StructuredSessionManager {
|
|
|
1128
1128
|
if (modelChoice && modelChoice !== "default") {
|
|
1129
1129
|
args.push("--model", modelChoice);
|
|
1130
1130
|
}
|
|
1131
|
-
// 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max →
|
|
1131
|
+
// 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max → xhigh)
|
|
1132
1132
|
// Newer Codex CLI versions removed the old dedicated exec flag, but still
|
|
1133
1133
|
// accept config overrides through `-c`.
|
|
1134
1134
|
const reasoningEffort = thinkingEffortToCodexReasoningEffort(session.thinkingEffort);
|
|
@@ -2765,12 +2765,16 @@ export class StructuredSessionManager {
|
|
|
2765
2765
|
const aggregatedOutput = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
2766
2766
|
const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
|
|
2767
2767
|
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2768
|
+
const input = { command, status };
|
|
2769
|
+
if (exitCode !== null)
|
|
2770
|
+
input.exit_code = exitCode;
|
|
2768
2771
|
if (!completed) {
|
|
2769
2772
|
return [{
|
|
2770
2773
|
type: "tool_use",
|
|
2771
2774
|
id,
|
|
2772
2775
|
name: "Bash",
|
|
2773
|
-
|
|
2776
|
+
description: "running",
|
|
2777
|
+
input,
|
|
2774
2778
|
}];
|
|
2775
2779
|
}
|
|
2776
2780
|
// codex 的 status 可能是 declined(sandbox 拒了命令)/ failed(执行失败)—
|
|
@@ -2780,12 +2784,21 @@ export class StructuredSessionManager {
|
|
|
2780
2784
|
const fallbackText = status === "declined"
|
|
2781
2785
|
? "command declined by sandbox"
|
|
2782
2786
|
: (exitCode === null ? "" : `exit_code: ${exitCode}`);
|
|
2783
|
-
return [
|
|
2787
|
+
return [
|
|
2788
|
+
{
|
|
2789
|
+
type: "tool_use",
|
|
2790
|
+
id,
|
|
2791
|
+
name: "Bash",
|
|
2792
|
+
description: exitCode === null ? status : `${status} · exit ${exitCode}`,
|
|
2793
|
+
input,
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2784
2796
|
type: "tool_result",
|
|
2785
2797
|
tool_use_id: id,
|
|
2786
2798
|
content: aggregatedOutput || fallbackText,
|
|
2787
2799
|
is_error: isError,
|
|
2788
|
-
}
|
|
2800
|
+
},
|
|
2801
|
+
];
|
|
2789
2802
|
}
|
|
2790
2803
|
if (type === "file_change") {
|
|
2791
2804
|
// 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
|
|
@@ -2806,16 +2819,16 @@ export class StructuredSessionManager {
|
|
|
2806
2819
|
let input;
|
|
2807
2820
|
if (kind === "add") {
|
|
2808
2821
|
toolName = "Write";
|
|
2809
|
-
input = { file_path: path, content: "" };
|
|
2822
|
+
input = { file_path: path, content: "", kind, status };
|
|
2810
2823
|
}
|
|
2811
2824
|
else if (kind === "delete") {
|
|
2812
2825
|
// 复用 Bash 终端卡,rm 语义直观
|
|
2813
2826
|
toolName = "Bash";
|
|
2814
|
-
input = { command: `rm ${path}`, description: `delete ${path}`, status };
|
|
2827
|
+
input = { command: `rm ${path}`, description: `delete ${path}`, kind, status };
|
|
2815
2828
|
}
|
|
2816
2829
|
else {
|
|
2817
2830
|
toolName = "Edit";
|
|
2818
|
-
input = { file_path: path, old_string: "", new_string: "" };
|
|
2831
|
+
input = { file_path: path, old_string: "", new_string: "", kind, status };
|
|
2819
2832
|
}
|
|
2820
2833
|
if (!completed) {
|
|
2821
2834
|
blocks.push({ type: "tool_use", id: subId, name: toolName, input });
|
|
@@ -2839,12 +2852,14 @@ export class StructuredSessionManager {
|
|
|
2839
2852
|
const errObj = item.error && typeof item.error === "object" ? item.error : null;
|
|
2840
2853
|
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2841
2854
|
const isError = !!errObj || status === "failed";
|
|
2855
|
+
const input = { ...args, status };
|
|
2842
2856
|
if (!completed) {
|
|
2843
2857
|
return [{
|
|
2844
2858
|
type: "tool_use",
|
|
2845
2859
|
id,
|
|
2846
2860
|
name: `${server}__${tool}`,
|
|
2847
|
-
|
|
2861
|
+
description: status,
|
|
2862
|
+
input,
|
|
2848
2863
|
}];
|
|
2849
2864
|
}
|
|
2850
2865
|
let resultText = "";
|
|
@@ -2856,29 +2871,58 @@ export class StructuredSessionManager {
|
|
|
2856
2871
|
const inner = this.extractCodexText(resultRec.content);
|
|
2857
2872
|
resultText = inner || JSON.stringify(resultRec).slice(0, 4096);
|
|
2858
2873
|
}
|
|
2859
|
-
return [
|
|
2874
|
+
return [
|
|
2875
|
+
{
|
|
2876
|
+
type: "tool_use",
|
|
2877
|
+
id,
|
|
2878
|
+
name: `${server}__${tool}`,
|
|
2879
|
+
description: status,
|
|
2880
|
+
input,
|
|
2881
|
+
},
|
|
2882
|
+
{
|
|
2860
2883
|
type: "tool_result",
|
|
2861
2884
|
tool_use_id: id,
|
|
2862
2885
|
content: resultText,
|
|
2863
2886
|
is_error: isError,
|
|
2864
|
-
}
|
|
2887
|
+
},
|
|
2888
|
+
];
|
|
2865
2889
|
}
|
|
2866
2890
|
if (type === "web_search") {
|
|
2867
2891
|
const query = typeof item.query === "string" ? item.query : "";
|
|
2892
|
+
const action = item.action && typeof item.action === "object" ? item.action : null;
|
|
2893
|
+
const actionType = action && typeof action.type === "string" ? action.type : "";
|
|
2894
|
+
const queries = action && Array.isArray(action.queries)
|
|
2895
|
+
? action.queries.filter((v) => typeof v === "string")
|
|
2896
|
+
: [];
|
|
2897
|
+
const input = { query };
|
|
2898
|
+
if (actionType)
|
|
2899
|
+
input.action = actionType;
|
|
2900
|
+
if (queries.length > 0)
|
|
2901
|
+
input.queries = queries;
|
|
2868
2902
|
if (!completed) {
|
|
2869
2903
|
return [{
|
|
2870
2904
|
type: "tool_use",
|
|
2871
2905
|
id,
|
|
2872
2906
|
name: "WebSearch",
|
|
2873
|
-
|
|
2907
|
+
description: actionType || "searching",
|
|
2908
|
+
input,
|
|
2874
2909
|
}];
|
|
2875
2910
|
}
|
|
2876
|
-
return [
|
|
2911
|
+
return [
|
|
2912
|
+
{
|
|
2913
|
+
type: "tool_use",
|
|
2914
|
+
id,
|
|
2915
|
+
name: "WebSearch",
|
|
2916
|
+
description: actionType || "completed",
|
|
2917
|
+
input,
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2877
2920
|
type: "tool_result",
|
|
2878
2921
|
tool_use_id: id,
|
|
2879
2922
|
// codex 不在 exec 流里回 search 结果,这里给个占位让 UI 卡片完成态。
|
|
2880
|
-
content: query ? `query: ${query}` : "",
|
|
2881
|
-
}
|
|
2923
|
+
content: queries.length > 0 ? queries.map((q) => `query: ${q}`).join("\n") : (query ? `query: ${query}` : ""),
|
|
2924
|
+
},
|
|
2925
|
+
];
|
|
2882
2926
|
}
|
|
2883
2927
|
if (type === "collab_tool_call") {
|
|
2884
2928
|
// codex 的子-agent 编排(spawn_agent / send_input / wait / close_agent)。
|
package/dist/tui/attach.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* 日志面板:因为日志在主进程里,attach 端没法直接看;这里改成"活动流"——
|
|
6
6
|
* 监听 snapshot 差分,把会话起止 / 总数变化打到 log 面板。
|
|
7
7
|
*/
|
|
8
|
-
import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, uninstallService, } from "./commands.js";
|
|
9
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, serviceRestart, uninstallService, } from "./commands.js";
|
|
10
9
|
import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
|
|
11
10
|
import { IpcClient } from "./ipc-client.js";
|
|
12
11
|
import { buildLayout } from "./layout.js";
|
|
@@ -159,36 +158,18 @@ export function startAttachTui(deps) {
|
|
|
159
158
|
void handleUninstallService(); });
|
|
160
159
|
async function handleRestart() {
|
|
161
160
|
const installed = safeServiceInstalled();
|
|
162
|
-
if (installed
|
|
161
|
+
if (installed) {
|
|
163
162
|
const ok = await layout.confirm({
|
|
164
163
|
title: "重启 wand 服务",
|
|
165
|
-
body: "
|
|
164
|
+
body: "将重启已安装的 wand systemd / launchd 服务。当前 attach 会话会随主进程重启短暂断开。",
|
|
166
165
|
});
|
|
167
166
|
if (!ok)
|
|
168
167
|
return;
|
|
169
|
-
layout.showToast("
|
|
170
|
-
const r =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
else {
|
|
175
|
-
layout.showToast(`systemctl 失败 (exit ${r.status})`, "error", 4000);
|
|
176
|
-
layout.showDetail("systemctl 输出", (r.stdout || "") + "\n" + (r.stderr || ""));
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (installed && process.platform === "darwin") {
|
|
181
|
-
const ok = await layout.confirm({
|
|
182
|
-
title: "重启 wand 服务",
|
|
183
|
-
body: "将依次 launchctl unload / load com.wand.web。",
|
|
184
|
-
});
|
|
185
|
-
if (!ok)
|
|
186
|
-
return;
|
|
187
|
-
const plist = `${process.env.HOME}/Library/LaunchAgents/com.wand.web.plist`;
|
|
188
|
-
const u = spawnSync("launchctl", ["unload", plist], { encoding: "utf8" });
|
|
189
|
-
const l = spawnSync("launchctl", ["load", plist], { encoding: "utf8" });
|
|
190
|
-
const ok2 = u.status === 0 && l.status === 0;
|
|
191
|
-
layout.showToast(ok2 ? "已请求 launchd 重启" : "launchctl 调用失败", ok2 ? "success" : "error", 3500);
|
|
168
|
+
layout.showToast("正在请求服务重启…", "info", 3000);
|
|
169
|
+
const r = await runOffMicrotask(() => serviceRestart());
|
|
170
|
+
layout.showToast(r.ok ? "已请求重启,IPC 会自动重连" : r.message, r.ok ? "success" : "error", 4000);
|
|
171
|
+
if (r.detail)
|
|
172
|
+
layout.showDetail(r.ok ? "服务重启输出" : "服务重启失败", r.detail);
|
|
192
173
|
return;
|
|
193
174
|
}
|
|
194
175
|
// 没注册成服务:尝试通过 IPC 让主进程自我退出,再由用户手动重启
|
package/dist/tui/commands.js
CHANGED
|
@@ -156,6 +156,8 @@ function clipboardCandidates() {
|
|
|
156
156
|
{ cmd: "xsel", args: ["--clipboard", "--input"] },
|
|
157
157
|
];
|
|
158
158
|
}
|
|
159
|
+
// ─── 系统服务(systemd system / user / launchd) ─────────────────────────
|
|
160
|
+
const LAUNCHD_LABEL = "com.wand.web";
|
|
159
161
|
export const DEFAULT_SERVICE_SCOPE = "system";
|
|
160
162
|
/** 当前 process 是不是 root(POSIX)。Windows 永远返回 false。 */
|
|
161
163
|
function isRoot() {
|
|
@@ -314,26 +316,35 @@ function launchdStatus(scope) {
|
|
|
314
316
|
platform: "darwin",
|
|
315
317
|
};
|
|
316
318
|
}
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
319
|
+
const target = launchdTarget(scope);
|
|
320
|
+
const printed = spawnSync("launchctl", ["print", target], { encoding: "utf8", timeout: 10_000 });
|
|
321
|
+
if (printed.status !== 0) {
|
|
320
322
|
return {
|
|
321
323
|
installed: true,
|
|
322
324
|
state: "inactive",
|
|
323
|
-
description: `[${scope}]
|
|
324
|
-
raw:
|
|
325
|
+
description: `[${scope}] installed 但未 bootstrap · ${target}`,
|
|
326
|
+
raw: ((printed.stdout || "") + "\n" + (printed.stderr || "")).trim(),
|
|
325
327
|
platform: "darwin",
|
|
326
328
|
};
|
|
327
329
|
}
|
|
328
|
-
const text =
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
330
|
+
const text = printed.stdout || "";
|
|
331
|
+
const state = matchLaunchdField(text, "state") || "loaded";
|
|
332
|
+
const pid = Number(matchLaunchdField(text, "pid") || "0");
|
|
333
|
+
const lastExit = matchLaunchdField(text, "last exit code");
|
|
334
|
+
const normalized = pid > 0 || state === "running"
|
|
335
|
+
? "active"
|
|
336
|
+
: state === "failed"
|
|
337
|
+
? "failed"
|
|
338
|
+
: "inactive";
|
|
339
|
+
const tail = pid > 0
|
|
340
|
+
? ` · PID ${pid}`
|
|
341
|
+
: lastExit
|
|
342
|
+
? ` · last exit ${lastExit}`
|
|
343
|
+
: "";
|
|
344
|
+
const desc = `[${scope}] ${state}${tail} · ${target}`;
|
|
334
345
|
return {
|
|
335
346
|
installed: true,
|
|
336
|
-
state:
|
|
347
|
+
state: normalized,
|
|
337
348
|
description: desc,
|
|
338
349
|
raw: text,
|
|
339
350
|
platform: "darwin",
|
|
@@ -361,39 +372,114 @@ function launchctlLoad(scope) {
|
|
|
361
372
|
const plist = servicePathFor(scope);
|
|
362
373
|
if (!existsSync(plist))
|
|
363
374
|
return { ok: false, message: `未安装 (${plist} 不存在)` };
|
|
364
|
-
|
|
365
|
-
if (r.status === 0)
|
|
366
|
-
return { ok: true, message: `已 launchctl load (${scope})` };
|
|
367
|
-
return {
|
|
368
|
-
ok: false,
|
|
369
|
-
message: `launchctl load 失败 (exit ${r.status})`,
|
|
370
|
-
detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
|
|
371
|
-
};
|
|
375
|
+
return launchdBootstrap(scope, plist, `已启动 launchd ${scope}`);
|
|
372
376
|
}
|
|
373
377
|
function launchctlUnload(scope) {
|
|
374
378
|
const plist = servicePathFor(scope);
|
|
375
379
|
if (!existsSync(plist))
|
|
376
380
|
return { ok: false, message: `未安装 (${plist} 不存在)` };
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
return launchdBootout(scope, plist, `已停止 launchd ${scope}`);
|
|
382
|
+
}
|
|
383
|
+
function launchdRestart(scope) {
|
|
384
|
+
const target = launchdTarget(scope);
|
|
385
|
+
const kicked = spawnSync("launchctl", ["kickstart", "-k", target], { encoding: "utf8", timeout: 10_000 });
|
|
386
|
+
if (kicked.status === 0)
|
|
387
|
+
return { ok: true, message: `已重启 launchd ${scope}: ${target}` };
|
|
388
|
+
const started = launchctlLoad(scope);
|
|
389
|
+
if (started.ok)
|
|
390
|
+
return { ok: true, message: `已 bootstrap 并启动 launchd ${scope}: ${target}` };
|
|
380
391
|
return {
|
|
381
392
|
ok: false,
|
|
382
|
-
message: `
|
|
383
|
-
detail:
|
|
393
|
+
message: `launchd 重启失败 (${target})`,
|
|
394
|
+
detail: [
|
|
395
|
+
`kickstart: ${formatSpawnResult(kicked)}`,
|
|
396
|
+
`bootstrap: ${started.message}`,
|
|
397
|
+
started.detail ?? "",
|
|
398
|
+
].filter(Boolean).join("\n"),
|
|
384
399
|
};
|
|
385
400
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
401
|
+
function launchdDomain(scope) {
|
|
402
|
+
return scope === "user" ? `gui/${currentUid()}` : "system";
|
|
403
|
+
}
|
|
404
|
+
function launchdTarget(scope) {
|
|
405
|
+
return `${launchdDomain(scope)}/${LAUNCHD_LABEL}`;
|
|
406
|
+
}
|
|
407
|
+
function currentUid() {
|
|
408
|
+
const fn = process.getuid;
|
|
409
|
+
if (typeof fn === "function") {
|
|
410
|
+
try {
|
|
411
|
+
return fn.call(process);
|
|
412
|
+
}
|
|
413
|
+
catch { /* fall through */ }
|
|
414
|
+
}
|
|
415
|
+
const id = spawnSync("id", ["-u"], { encoding: "utf8", timeout: 3000 });
|
|
416
|
+
const uid = Number((id.stdout || "").trim());
|
|
417
|
+
return Number.isInteger(uid) && uid >= 0 ? uid : 0;
|
|
418
|
+
}
|
|
419
|
+
function matchLaunchdField(text, field) {
|
|
420
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
+
const match = text.match(new RegExp(`^\\s*${escaped} = (.+)$`, "m"));
|
|
422
|
+
return match?.[1]?.trim() ?? null;
|
|
423
|
+
}
|
|
424
|
+
function launchdBootstrap(scope, plist, successMessage) {
|
|
425
|
+
const domain = launchdDomain(scope);
|
|
426
|
+
const target = launchdTarget(scope);
|
|
427
|
+
const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
428
|
+
const bootstrap = spawnSync("launchctl", ["bootstrap", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
429
|
+
const enable = spawnSync("launchctl", ["enable", target], { encoding: "utf8", timeout: 10_000 });
|
|
430
|
+
const kickstart = spawnSync("launchctl", ["kickstart", "-k", target], { encoding: "utf8", timeout: 10_000 });
|
|
431
|
+
const detail = [
|
|
432
|
+
`target: ${target}`,
|
|
433
|
+
`bootout: ${formatSpawnResult(bootout)}`,
|
|
434
|
+
`bootstrap: ${formatSpawnResult(bootstrap)}`,
|
|
435
|
+
`enable: ${formatSpawnResult(enable)}`,
|
|
436
|
+
`kickstart: ${formatSpawnResult(kickstart)}`,
|
|
437
|
+
].join("\n");
|
|
438
|
+
if (bootstrap.status !== 0 && !launchdIsAlreadyBootstrapped(bootstrap)) {
|
|
439
|
+
return { ok: false, message: `launchctl bootstrap 失败 (${target})`, detail };
|
|
440
|
+
}
|
|
441
|
+
if (enable.status !== 0) {
|
|
442
|
+
return { ok: false, message: `launchctl enable 失败 (${target})`, detail };
|
|
443
|
+
}
|
|
444
|
+
if (kickstart.status !== 0) {
|
|
445
|
+
const status = launchdStatus(scope);
|
|
446
|
+
if (status.state !== "active") {
|
|
447
|
+
return { ok: false, message: `launchctl kickstart 失败 (${target})`, detail };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return { ok: true, message: successMessage, detail };
|
|
451
|
+
}
|
|
452
|
+
function launchdBootout(scope, plist, successMessage) {
|
|
453
|
+
const domain = launchdDomain(scope);
|
|
454
|
+
const target = launchdTarget(scope);
|
|
455
|
+
const bootout = spawnSync("launchctl", ["bootout", domain, plist], { encoding: "utf8", timeout: 10_000 });
|
|
456
|
+
if (bootout.status === 0 || launchdIsNotBootstrapped(bootout)) {
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
message: successMessage,
|
|
460
|
+
detail: `target: ${target}\nbootout: ${formatSpawnResult(bootout)}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
391
463
|
return {
|
|
392
464
|
ok: false,
|
|
393
|
-
message:
|
|
394
|
-
detail:
|
|
465
|
+
message: `launchctl bootout 失败 (${target})`,
|
|
466
|
+
detail: formatSpawnResult(bootout),
|
|
395
467
|
};
|
|
396
468
|
}
|
|
469
|
+
function launchdIsAlreadyBootstrapped(result) {
|
|
470
|
+
const text = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
471
|
+
return /already bootstrapped|service already loaded|Bootstrap failed:\s*5/i.test(text);
|
|
472
|
+
}
|
|
473
|
+
function launchdIsNotBootstrapped(result) {
|
|
474
|
+
const text = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
475
|
+
return /No such process|service is not loaded|Bootstrap failed:\s*3/i.test(text);
|
|
476
|
+
}
|
|
477
|
+
function formatSpawnResult(result) {
|
|
478
|
+
const text = ((result.stdout || "") + "\n" + (result.stderr || "")).trim();
|
|
479
|
+
return result.status === 0
|
|
480
|
+
? "ok"
|
|
481
|
+
: `failed (${text || `exit ${result.status}`})`;
|
|
482
|
+
}
|
|
397
483
|
function unsupported() {
|
|
398
484
|
return { ok: false, message: `当前平台 ${process.platform} 不支持服务管理` };
|
|
399
485
|
}
|
|
@@ -714,17 +800,18 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
714
800
|
catch (err) {
|
|
715
801
|
return { ok: false, message: `写入 plist 失败: ${getErrorMessage(err)}` };
|
|
716
802
|
}
|
|
717
|
-
const
|
|
718
|
-
if (
|
|
803
|
+
const started = launchdBootstrap(scope, plistPath, `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`);
|
|
804
|
+
if (!started.ok) {
|
|
719
805
|
return {
|
|
720
806
|
ok: false,
|
|
721
|
-
message: "已写入 plist,但 launchctl
|
|
722
|
-
detail:
|
|
807
|
+
message: "已写入 plist,但 launchctl 启动失败",
|
|
808
|
+
detail: started.detail,
|
|
723
809
|
};
|
|
724
810
|
}
|
|
725
811
|
return {
|
|
726
812
|
ok: true,
|
|
727
813
|
message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}`,
|
|
814
|
+
detail: started.detail,
|
|
728
815
|
};
|
|
729
816
|
}
|
|
730
817
|
function uninstallLaunchdService(scope) {
|
|
@@ -732,7 +819,8 @@ function uninstallLaunchdService(scope) {
|
|
|
732
819
|
if (!existsSync(plistPath)) {
|
|
733
820
|
return { ok: false, message: `未检测到已安装的 launchd ${scope} 服务` };
|
|
734
821
|
}
|
|
735
|
-
const
|
|
822
|
+
const stopped = launchdBootout(scope, plistPath, `已停止 launchd ${scope}`);
|
|
823
|
+
const disabled = spawnSync("launchctl", ["disable", launchdTarget(scope)], { encoding: "utf8", timeout: 10_000 });
|
|
736
824
|
try {
|
|
737
825
|
unlinkSync(plistPath);
|
|
738
826
|
}
|
|
@@ -742,7 +830,10 @@ function uninstallLaunchdService(scope) {
|
|
|
742
830
|
return {
|
|
743
831
|
ok: true,
|
|
744
832
|
message: `已卸载 launchd ${scope === "user" ? "用户代理" : "系统守护"}`,
|
|
745
|
-
detail:
|
|
833
|
+
detail: [
|
|
834
|
+
stopped.detail ?? stopped.message,
|
|
835
|
+
`disable: ${formatSpawnResult(disabled)}`,
|
|
836
|
+
].join("\n"),
|
|
746
837
|
};
|
|
747
838
|
}
|
|
748
839
|
// ─── 工具 ────────────────────────────────────────────────────────────────
|
package/dist/types.d.ts
CHANGED
|
@@ -441,7 +441,7 @@ export interface SessionSnapshot {
|
|
|
441
441
|
* - off: 不覆盖默认思考深度(SDK: 不传 thinking;Claude CLI: auto/default;Codex: model_reasoning_effort minimal)
|
|
442
442
|
* - standard: 标准(SDK: budget 4096;Claude CLI: low;Codex: low)
|
|
443
443
|
* - deep: 深度(SDK: budget 16000;Claude CLI: medium;Codex: medium)
|
|
444
|
-
* - max: 最深(SDK: budget 31999;Claude CLI: max;Codex:
|
|
444
|
+
* - max: 最深(SDK: budget 31999;Claude CLI: max;Codex: xhigh)
|
|
445
445
|
*/
|
|
446
446
|
thinkingEffort?: "off" | "standard" | "deep" | "max" | null;
|
|
447
447
|
/** 当前 PTY 列宽,由最近一次 resize 决定。前端用它来判断本端 fit 是否需要校准。 */
|