@co0ontty/wand 1.41.4 → 1.43.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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "7c55e89bdac5b0ba4588b4dc43a7dd2ab5f812ea",
3
- "builtAt": "2026-05-31T01:09:26.532Z",
4
- "version": "1.41.4",
2
+ "commit": "a2fcf61e1ef6825cb1767b63fb9593e8f6e4ecea",
3
+ "builtAt": "2026-05-31T02:16:53.049Z",
4
+ "version": "1.43.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/server.js CHANGED
@@ -25,6 +25,7 @@ import { installPackageGloballyAsync, resolveGlobalWandCli } from "./npm-update-
25
25
  import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
26
26
  import { computeRelaunch } from "./relaunch.js";
27
27
  import { isServiceInstalled } from "./tui/commands.js";
28
+ import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
28
29
  import { registerUploadRoutes } from "./upload-routes.js";
29
30
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
30
31
  import { resolveDatabasePath, WandStorage } from "./storage.js";
@@ -1569,31 +1570,40 @@ export async function startServer(config, configPath) {
1569
1570
  installSpec = `${PKG_NAME}@latest`;
1570
1571
  targetLabel = onBetaBuild ? "最新正式版" : latest;
1571
1572
  }
1572
- // beta 走 git 安装(clone + 装依赖),比 registry tarball 慢,给足超时。
1573
- const logLines = [];
1574
- try {
1575
- await installPackageGloballyAsync(installSpec, 300000, (line) => {
1576
- logLines.push(line);
1577
- process.stdout.write(`${line}\n`);
1578
- });
1573
+ if (!canUseDetachedUpdateHelper()) {
1574
+ res.status(500).json({ error: "当前平台暂不支持 Web 异步更新,请在终端运行 install.sh 更新。" });
1575
+ return;
1579
1576
  }
1580
- catch (e) {
1581
- res.status(500).json({
1582
- error: getErrorMessage(e, "更新失败。"),
1583
- detail: logLines.join("\n").slice(-2000),
1584
- });
1577
+ const helper = startDetachedUpdateHelper({
1578
+ installSpec,
1579
+ configPath,
1580
+ parentPid: process.pid,
1581
+ cliArgs: process.argv.slice(2),
1582
+ cwd: process.cwd(),
1583
+ env: process.env,
1584
+ timeoutMs: 300000,
1585
+ });
1586
+ if (!helper.started) {
1587
+ res.status(500).json({ error: helper.message, detail: `script=${helper.scriptPath}\nlog=${helper.logPath}` });
1585
1588
  return;
1586
1589
  }
1587
- // 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),重启才会跑到新版。
1588
- const repair = repairServiceUnitAfterUpdate(configPath);
1589
- // 装包成功后告知前端可以发起重启;前端会随即调用 /api/restart 完成自动重启。
1590
+ process.stdout.write(`[wand] ${helper.message}\n`);
1591
+ wsManager.emitEvent({
1592
+ type: "notification",
1593
+ sessionId: "__system__",
1594
+ data: { kind: "auto-update-restart", current: PKG_VERSION, latest: targetLabel },
1595
+ });
1590
1596
  res.json({
1591
1597
  ok: true,
1592
- message: `已更新到 ${targetLabel}`,
1593
- restartRequired: true,
1598
+ message: `已开始更新到 ${targetLabel}`,
1599
+ restartRequired: false,
1600
+ detachedUpdate: true,
1594
1601
  version: targetLabel,
1595
- serviceRepair: repair.scope ? { repaired: repair.repaired, message: repair.message } : null,
1602
+ logPath: helper.logPath,
1596
1603
  });
1604
+ setTimeout(() => {
1605
+ shutdownForDetachedUpdate();
1606
+ }, 500);
1597
1607
  }
1598
1608
  catch (error) {
1599
1609
  res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
@@ -2187,6 +2197,18 @@ export async function startServer(config, configPath) {
2187
2197
  // Force exit after 5s if graceful shutdown stalls
2188
2198
  setTimeout(() => process.exit(0), 5000);
2189
2199
  }
2200
+ function shutdownForDetachedUpdate() {
2201
+ try {
2202
+ wss.clients.forEach((client) => client.close());
2203
+ }
2204
+ catch {
2205
+ /* noop */
2206
+ }
2207
+ server.close(() => {
2208
+ process.exit(0);
2209
+ });
2210
+ setTimeout(() => process.exit(0), 3000);
2211
+ }
2190
2212
  app.post("/api/restart", async (_req, res) => {
2191
2213
  res.json({ ok: true, message: "服务正在重启..." });
2192
2214
  wsManager.emitEvent({
@@ -0,0 +1,18 @@
1
+ export interface DetachedUpdateOptions {
2
+ installSpec: string;
3
+ configPath: string;
4
+ parentPid: number;
5
+ cliArgs: string[];
6
+ cwd: string;
7
+ env: NodeJS.ProcessEnv;
8
+ timeoutMs?: number;
9
+ }
10
+ export interface DetachedUpdateResult {
11
+ started: boolean;
12
+ scriptPath: string;
13
+ logPath: string;
14
+ pid?: number;
15
+ message: string;
16
+ }
17
+ export declare function startDetachedUpdateHelper(opts: DetachedUpdateOptions): DetachedUpdateResult;
18
+ export declare function canUseDetachedUpdateHelper(): boolean;
@@ -0,0 +1,238 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { detectInstalledScope } from "./tui/commands.js";
7
+ function shellQuote(value) {
8
+ return `'${value.replace(/'/g, `'\\''`)}'`;
9
+ }
10
+ function shellArray(values) {
11
+ return values.map((value) => shellQuote(value)).join(" ");
12
+ }
13
+ function boolShell(value) {
14
+ return value ? "1" : "0";
15
+ }
16
+ function resolveBashPath() {
17
+ return existsSync("/bin/bash") ? "/bin/bash" : "bash";
18
+ }
19
+ function detectServiceScope() {
20
+ try {
21
+ return detectInstalledScope();
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function buildHelperScript(opts, logPath, serviceScope) {
28
+ const isSystemdManaged = !!process.env.INVOCATION_ID;
29
+ const isLaunchdManaged = !!process.env.LAUNCHD_SOCKET || !!process.env.XPC_SERVICE_NAME;
30
+ const shouldStartService = serviceScope !== null;
31
+ const shouldRestartStandalone = !shouldStartService;
32
+ const npmCache = opts.env.npm_config_cache || path.join(os.homedir(), ".npm");
33
+ const timeoutSec = Math.max(30, Math.ceil((opts.timeoutMs ?? 300_000) / 1000));
34
+ const cliArgArray = opts.cliArgs;
35
+ return `#!/usr/bin/env bash
36
+ set -u
37
+ LOG=${shellQuote(logPath)}
38
+ exec >>"$LOG" 2>&1
39
+ echo "[wand-update] started at $(date -Is)"
40
+ echo "[wand-update] parent pid: ${opts.parentPid}"
41
+ echo "[wand-update] install spec: ${opts.installSpec}"
42
+
43
+ export PATH=${shellQuote(opts.env.PATH || process.env.PATH || "")}
44
+ export HOME=${shellQuote(opts.env.HOME || os.homedir())}
45
+ export npm_config_cache=${shellQuote(npmCache)}
46
+ CONFIG_PATH=${shellQuote(opts.configPath)}
47
+ INSTALL_SPEC=${shellQuote(opts.installSpec)}
48
+ PARENT_PID=${opts.parentPid}
49
+ SERVICE_SCOPE=${shellQuote(serviceScope ?? "")}
50
+ SHOULD_START_SERVICE=${boolShell(shouldStartService)}
51
+ SHOULD_RESTART_STANDALONE=${boolShell(shouldRestartStandalone)}
52
+ TIMEOUT_SEC=${timeoutSec}
53
+ CLI_ARGS=(${shellArray(cliArgArray)})
54
+
55
+ run() {
56
+ echo "+ $*"
57
+ "$@"
58
+ }
59
+
60
+ run_best_effort() {
61
+ echo "+ $*"
62
+ "$@" || true
63
+ }
64
+
65
+ wait_for_parent_exit() {
66
+ local pid="$1"
67
+ local i=0
68
+ while kill -0 "$pid" 2>/dev/null; do
69
+ i=$((i + 1))
70
+ if [ "$i" -ge 120 ]; then
71
+ echo "[wand-update] parent still alive after 60s; continuing"
72
+ return 0
73
+ fi
74
+ sleep 0.5
75
+ done
76
+ }
77
+
78
+ clean_leftovers() {
79
+ local npm_root
80
+ npm_root="$(npm root -g 2>/dev/null || true)"
81
+ if [ -n "$npm_root" ] && [ -d "$npm_root/@co0ontty" ]; then
82
+ find "$npm_root/@co0ontty" -maxdepth 1 -name ".wand-*" -type d -print -exec rm -rf {} + 2>/dev/null || true
83
+ fi
84
+ }
85
+
86
+ npm_install_wand() {
87
+ if command -v timeout >/dev/null 2>&1; then
88
+ run timeout "$TIMEOUT_SEC" npm install -g "$INSTALL_SPEC"
89
+ else
90
+ run npm install -g "$INSTALL_SPEC"
91
+ fi
92
+ }
93
+
94
+ npm_install_wand_force() {
95
+ if command -v timeout >/dev/null 2>&1; then
96
+ run timeout "$TIMEOUT_SEC" npm install -g --force "$INSTALL_SPEC"
97
+ else
98
+ run npm install -g --force "$INSTALL_SPEC"
99
+ fi
100
+ }
101
+
102
+ restart_or_start_service() {
103
+ if [ "$SHOULD_START_SERVICE" = "1" ]; then
104
+ if [ "$SERVICE_SCOPE" = "user" ]; then
105
+ run_best_effort wand service:install --user -c "$CONFIG_PATH"
106
+ run_best_effort systemctl --user restart wand.service
107
+ else
108
+ run_best_effort wand service:install -c "$CONFIG_PATH"
109
+ run_best_effort systemctl restart wand.service
110
+ fi
111
+ return
112
+ fi
113
+
114
+ if [ "$SHOULD_RESTART_STANDALONE" = "1" ]; then
115
+ echo "[wand-update] starting standalone process"
116
+ cd ${shellQuote(opts.cwd)}
117
+ local global_cli
118
+ global_cli="$(npm root -g 2>/dev/null)/@co0ontty/wand/dist/cli.js"
119
+ if [ -f "$global_cli" ]; then
120
+ nohup ${shellQuote(process.execPath)} "$global_cli" "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
121
+ else
122
+ nohup wand "$\{CLI_ARGS[@]}" >>"$LOG" 2>&1 &
123
+ fi
124
+ fi
125
+ }
126
+
127
+ main() {
128
+ if [ "$SERVICE_SCOPE" = "user" ]; then
129
+ run_best_effort systemctl --user stop wand.service
130
+ elif [ "$SERVICE_SCOPE" = "system" ]; then
131
+ run_best_effort systemctl stop wand.service
132
+ fi
133
+
134
+ wait_for_parent_exit "$PARENT_PID"
135
+ clean_leftovers
136
+
137
+ echo "[wand-update] installing"
138
+ if ! npm_install_wand; then
139
+ echo "[wand-update] first install failed; retrying with uninstall + force install"
140
+ clean_leftovers
141
+ run_best_effort npm uninstall -g @co0ontty/wand
142
+ clean_leftovers
143
+ npm_install_wand_force
144
+ fi
145
+
146
+ run wand init -c "$CONFIG_PATH"
147
+ restart_or_start_service
148
+ echo "[wand-update] completed at $(date -Is)"
149
+ }
150
+
151
+ main
152
+ `;
153
+ }
154
+ function spawnDetached(scriptPath, _logPath, env, serviceScope) {
155
+ const baseEnv = { ...env };
156
+ const trySpawn = (cmd, args, method) => {
157
+ try {
158
+ const child = spawn(cmd, args, {
159
+ detached: true,
160
+ stdio: "ignore",
161
+ env: baseEnv,
162
+ });
163
+ child.unref();
164
+ return { pid: child.pid, method };
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ };
170
+ if (process.platform === "linux") {
171
+ const unitName = `wand-update-${process.pid}-${Date.now()}`;
172
+ const bashPath = resolveBashPath();
173
+ const runTransient = (args, method) => {
174
+ const res = spawnSync("systemd-run", args, {
175
+ encoding: "utf8",
176
+ timeout: 5000,
177
+ env: baseEnv,
178
+ stdio: "ignore",
179
+ });
180
+ return res.status === 0 ? { method } : null;
181
+ };
182
+ const userService = runTransient(["--user", `--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run --user") ??
183
+ runTransient(["--user", `--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run --user");
184
+ if (userService)
185
+ return userService;
186
+ const systemService = runTransient([`--unit=${unitName}`, "--quiet", "--collect", bashPath, scriptPath], "systemd-run") ??
187
+ runTransient([`--unit=${unitName}`, "--quiet", bashPath, scriptPath], "systemd-run");
188
+ if (systemService)
189
+ return systemService;
190
+ if (serviceScope) {
191
+ return null;
192
+ }
193
+ const setsid = spawnSync("setsid", ["--version"], { encoding: "utf8", timeout: 2000 });
194
+ if (setsid.status === 0) {
195
+ const result = trySpawn("setsid", [bashPath, scriptPath], "setsid");
196
+ if (result)
197
+ return result;
198
+ }
199
+ }
200
+ if (process.platform === "darwin") {
201
+ const result = trySpawn("nohup", [resolveBashPath(), scriptPath], "nohup");
202
+ if (result)
203
+ return result;
204
+ }
205
+ return trySpawn(resolveBashPath(), [scriptPath], "bash detached");
206
+ }
207
+ export function startDetachedUpdateHelper(opts) {
208
+ const dir = mkdtempSync(path.join(os.tmpdir(), "wand-update-"));
209
+ const scriptPath = path.join(dir, "update.sh");
210
+ const logPath = path.join(dir, "update.log");
211
+ const serviceScope = detectServiceScope();
212
+ writeFileSync(scriptPath, buildHelperScript(opts, logPath, serviceScope), { encoding: "utf8", mode: 0o700 });
213
+ chmodSync(scriptPath, 0o700);
214
+ const spawned = spawnDetached(scriptPath, logPath, opts.env, serviceScope);
215
+ if (!spawned) {
216
+ return {
217
+ started: false,
218
+ scriptPath,
219
+ logPath,
220
+ message: "无法启动独立更新 helper。",
221
+ };
222
+ }
223
+ return {
224
+ started: true,
225
+ scriptPath,
226
+ logPath,
227
+ pid: spawned.pid,
228
+ message: `独立更新 helper 已启动 (${spawned.method})。日志: ${logPath}`,
229
+ };
230
+ }
231
+ export function canUseDetachedUpdateHelper() {
232
+ if (process.platform === "win32")
233
+ return false;
234
+ if (existsSync("/bin/bash"))
235
+ return true;
236
+ const res = spawnSync("bash", ["--version"], { encoding: "utf8", timeout: 2000 });
237
+ return res.status === 0;
238
+ }