@co0ontty/wand 1.48.1 → 1.49.0-beta.g6066201

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": "357b85bac85ea7570863c051def1badcea5d2b62",
3
- "builtAt": "2026-06-02T00:14:42.639Z",
4
- "version": "1.48.1",
5
- "channel": "stable"
2
+ "commit": "6066201e006fdce04120be0b3e0add7d68770375",
3
+ "builtAt": "2026-06-02T01:46:40.601Z",
4
+ "version": "1.49.0-beta.g6066201",
5
+ "channel": "beta"
6
6
  }
@@ -11,6 +11,25 @@
11
11
  * 我们的策略:安装前备份当前全局包,补齐 npm 子进程 PATH,清掉
12
12
  * `@co0ontty/.wand-*` 残留目录;失败时恢复备份,避免运行中的服务被半成品安装拆掉。
13
13
  */
14
+ export declare const PACKAGE_NAME = "@co0ontty/wand";
15
+ export type UpdateChannel = "stable" | "beta";
16
+ export interface PackageUpdateInfo {
17
+ channel: UpdateChannel;
18
+ current: string;
19
+ latest: string | null;
20
+ updateAvailable: boolean;
21
+ distTag: "latest" | "beta";
22
+ installSpec: string;
23
+ }
24
+ export declare function normalizeUpdateChannel(value: unknown): UpdateChannel;
25
+ export declare function getUpdateDistTag(channel: UpdateChannel): "latest" | "beta";
26
+ export declare function getInstallSpecForChannel(channel: UpdateChannel): string;
27
+ export declare function getStableTagVersion(version: string): string;
28
+ export declare function isBetaPackageVersion(version: string): boolean;
29
+ export declare function isPureStableVersion(version: string): boolean;
30
+ export declare function buildPackageUpdateInfo(currentVersion: string, channel: UpdateChannel, latestVersion: string | null): PackageUpdateInfo;
31
+ export declare function checkPackageUpdateAsync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): Promise<PackageUpdateInfo>;
32
+ export declare function checkPackageUpdateSync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): PackageUpdateInfo;
14
33
  /**
15
34
  * 解析当前 `npm root -g` 的目录。失败返回 null。
16
35
  */
@@ -18,13 +18,99 @@ import path from "node:path";
18
18
  import process from "node:process";
19
19
  import { promisify } from "node:util";
20
20
  import { whichSync } from "./path-repair.js";
21
+ import { compareSemver } from "./version-utils.js";
21
22
  const execFileAsync = promisify(execFile);
22
- const PACKAGE_NAME = "@co0ontty/wand";
23
+ export const PACKAGE_NAME = "@co0ontty/wand";
23
24
  const PACKAGE_SCOPE = "@co0ontty";
24
25
  const PACKAGE_BASENAME = "wand";
25
26
  const NPM_BIN = process.platform === "win32" ? "npm.cmd" : "npm";
26
27
  const COMMON_UNIX_PATHS = ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"];
27
28
  const INSTALL_MAX_BUFFER = 10 * 1024 * 1024;
29
+ const NPM_VIEW_TIMEOUT_MS = 15_000;
30
+ export function normalizeUpdateChannel(value) {
31
+ return value === "beta" ? "beta" : "stable";
32
+ }
33
+ export function getUpdateDistTag(channel) {
34
+ return channel === "beta" ? "beta" : "latest";
35
+ }
36
+ export function getInstallSpecForChannel(channel) {
37
+ return `${PACKAGE_NAME}@${getUpdateDistTag(channel)}`;
38
+ }
39
+ function cleanVersion(value) {
40
+ return value.trim().replace(/^v/, "");
41
+ }
42
+ export function getStableTagVersion(version) {
43
+ return cleanVersion(version).split("+")[0]?.split("-")[0] ?? cleanVersion(version);
44
+ }
45
+ export function isBetaPackageVersion(version) {
46
+ return /(?:^|-)beta(?:[.-]|$)/i.test(cleanVersion(version));
47
+ }
48
+ export function isPureStableVersion(version) {
49
+ return /^\d+\.\d+\.\d+$/.test(cleanVersion(version));
50
+ }
51
+ function computeUpdateAvailable(currentVersion, latestVersion, channel) {
52
+ if (!latestVersion)
53
+ return false;
54
+ const current = cleanVersion(currentVersion);
55
+ const latest = cleanVersion(latestVersion);
56
+ const currentTag = getStableTagVersion(current);
57
+ const latestTag = getStableTagVersion(latest);
58
+ const tagCompare = compareSemver(latestTag, currentTag);
59
+ if (channel === "beta") {
60
+ if (tagCompare < 0)
61
+ return false;
62
+ if (tagCompare > 0)
63
+ return true;
64
+ // Beta follows the beta npm dist-tag exactly. The suffix contains the short
65
+ // git SHA, so recency comes from the dist-tag pointer instead of semver order.
66
+ return latest !== current;
67
+ }
68
+ if (tagCompare < 0)
69
+ return false;
70
+ if (tagCompare > 0)
71
+ return true;
72
+ // Stable intentionally tracks only the pure tag version. If the current build
73
+ // is a beta with the same base tag, switching back to stable should reinstall
74
+ // the clean npm @latest package.
75
+ return current !== latestTag;
76
+ }
77
+ export function buildPackageUpdateInfo(currentVersion, channel, latestVersion) {
78
+ const latest = latestVersion?.trim() || null;
79
+ const stableLatest = channel === "stable" && latest ? getStableTagVersion(latest) : latest;
80
+ return {
81
+ channel,
82
+ current: cleanVersion(currentVersion),
83
+ latest: stableLatest,
84
+ updateAvailable: computeUpdateAvailable(currentVersion, stableLatest, channel),
85
+ distTag: getUpdateDistTag(channel),
86
+ installSpec: getInstallSpecForChannel(channel),
87
+ };
88
+ }
89
+ async function viewPackageVersionAsync(channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
90
+ try {
91
+ const { stdout } = await execFileAsync(NPM_BIN, ["view", getInstallSpecForChannel(channel), "version"], { timeout: timeoutMs, env: getChildEnv(), maxBuffer: INSTALL_MAX_BUFFER });
92
+ const version = String(stdout || "").trim();
93
+ return version || null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ function viewPackageVersionSync(channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
100
+ const res = runNpmSync(["view", getInstallSpecForChannel(channel), "version"], timeoutMs);
101
+ if (res.status !== 0 || !res.stdout)
102
+ return null;
103
+ const version = res.stdout.trim();
104
+ return version || null;
105
+ }
106
+ export async function checkPackageUpdateAsync(currentVersion, channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
107
+ const latest = await viewPackageVersionAsync(channel, timeoutMs);
108
+ return buildPackageUpdateInfo(currentVersion, channel, latest);
109
+ }
110
+ export function checkPackageUpdateSync(currentVersion, channel, timeoutMs = NPM_VIEW_TIMEOUT_MS) {
111
+ const latest = viewPackageVersionSync(channel, timeoutMs);
112
+ return buildPackageUpdateInfo(currentVersion, channel, latest);
113
+ }
28
114
  function getChildEnv() {
29
115
  const entries = [
30
116
  path.dirname(process.execPath),
@@ -291,8 +377,8 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
291
377
  }
292
378
  }
293
379
  // 终极兜底:uninstall + force install
294
- // 卸载用固定包名 PACKAGE_NAME,而不是从 install spec 反推:spec 可能是 git
295
- // 形式(`github:co0ontty/wand#beta`),用正则 strip @tag 反推会得到错误的卸载目标。
380
+ // 卸载用固定包名 PACKAGE_NAME,而不是从 install spec 反推:spec npm tag
381
+ // 或版本号时,用正则 strip @tag 反推会误伤 scoped package 名。
296
382
  try {
297
383
  await runNpmAsync(["uninstall", "-g", PACKAGE_NAME], timeoutMs);
298
384
  }
package/dist/server.js CHANGED
@@ -22,7 +22,7 @@ import { SessionLogger } from "./session-logger.js";
22
22
  import { StructuredSessionManager } from "./structured-session-manager.js";
23
23
  import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
24
24
  import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
25
- import { installPackageGloballyAsync, resolveGlobalWandCli } from "./npm-update-utils.js";
25
+ import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
26
26
  import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
27
27
  import { computeRelaunch } from "./relaunch.js";
28
28
  import { isServiceInstalled } from "./tui/commands.js";
@@ -47,29 +47,27 @@ const PKG_VERSION = PKG_JSON.version;
47
47
  const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=22.5.0";
48
48
  const PKG_REPO_URL = "https://github.com/co0ontty/wand";
49
49
  // ── Update check cache ──
50
- let cachedLatestVersion = null;
51
- let cacheTimestamp = 0;
52
50
  const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
53
51
  /** Cached update result broadcast to new clients on connect. */
54
52
  let cachedUpdateInfo = null;
55
- async function checkNpmLatestVersion(forceRefresh = false) {
53
+ const packageUpdateCache = new Map();
54
+ async function checkLatestPackageVersion(channel, forceRefresh = false) {
56
55
  const now = Date.now();
57
- if (forceRefresh || !cachedLatestVersion || (now - cacheTimestamp > CACHE_TTL_MS)) {
58
- try {
59
- const { stdout } = await execAsync(`npm view ${PKG_NAME} version`, { timeout: 15000 });
60
- cachedLatestVersion = stdout.trim();
61
- cacheTimestamp = now;
62
- }
63
- catch {
64
- cachedLatestVersion = null;
65
- }
56
+ const cached = packageUpdateCache.get(channel);
57
+ if (!forceRefresh && cached && now - cached.timestamp < CACHE_TTL_MS) {
58
+ return applyLocalBuildUpdateOverride(cached.info);
66
59
  }
67
- const latest = cachedLatestVersion || PKG_VERSION;
68
- return {
69
- current: PKG_VERSION,
70
- latest,
71
- updateAvailable: latest !== PKG_VERSION && compareSemver(latest, PKG_VERSION) > 0,
72
- };
60
+ const info = await checkPackageUpdateAsync(PKG_VERSION, channel);
61
+ if (info.latest) {
62
+ packageUpdateCache.set(channel, { info, timestamp: now });
63
+ }
64
+ return applyLocalBuildUpdateOverride(info);
65
+ }
66
+ function applyLocalBuildUpdateOverride(info) {
67
+ if (info.channel === "stable" && BUILD_INFO.channel === "beta" && info.latest) {
68
+ return { ...info, updateAvailable: true };
69
+ }
70
+ return info;
73
71
  }
74
72
  /** 读取 dist/build-info.json(由 scripts/stamp-build-info.js 在 build 时生成)。 */
75
73
  function readBuildInfo() {
@@ -89,54 +87,6 @@ function readBuildInfo() {
89
87
  }
90
88
  }
91
89
  const BUILD_INFO = readBuildInfo();
92
- // owner/repo(从 PKG_REPO_URL 派生),用于拼 beta 分支 raw 地址与 git 安装 spec。
93
- const PKG_REPO_SLUG = PKG_REPO_URL.replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
94
- const BETA_BRANCH = "beta";
95
- const BETA_BUILD_INFO_URL = `https://raw.githubusercontent.com/${PKG_REPO_SLUG}/${BETA_BRANCH}/dist/build-info.json`;
96
- const BETA_INSTALL_SPEC = `github:${PKG_REPO_SLUG}#${BETA_BRANCH}`;
97
- let cachedBetaInfo = null;
98
- let betaCacheTs = 0;
99
- /**
100
- * 通过 beta 分支的 dist/build-info.json 判定 beta 更新。
101
- * 比对本地构建 commit 与远端 commit(两者记录的都是「构建源自的 master commit」)。
102
- * 取不到远端 → updateAvailable=false(不冒进)。
103
- */
104
- async function checkBetaUpdate(forceRefresh = false) {
105
- const now = Date.now();
106
- if (!forceRefresh && cachedBetaInfo && now - betaCacheTs < CACHE_TTL_MS) {
107
- return cachedBetaInfo;
108
- }
109
- const localCommit = BUILD_INFO.commit;
110
- let remoteCommit = null;
111
- let remoteBuiltAt = null;
112
- try {
113
- const resp = await fetch(BETA_BUILD_INFO_URL, {
114
- headers: { "User-Agent": "wand-server", Accept: "application/json" },
115
- signal: AbortSignal.timeout(10000),
116
- });
117
- if (resp.ok) {
118
- const j = (await resp.json());
119
- remoteCommit = typeof j.commit === "string" && j.commit ? j.commit : null;
120
- remoteBuiltAt = typeof j.builtAt === "string" && j.builtAt ? j.builtAt : null;
121
- }
122
- }
123
- catch {
124
- /* 网络失败:保持 null,下面判定为无更新 */
125
- }
126
- const short = (c) => (c ? c.slice(0, 7) : "unknown");
127
- const info = {
128
- channel: "beta",
129
- current: short(localCommit),
130
- latest: short(remoteCommit),
131
- updateAvailable: !!remoteCommit && remoteCommit !== localCommit,
132
- localCommit,
133
- remoteCommit,
134
- remoteBuiltAt,
135
- };
136
- cachedBetaInfo = info;
137
- betaCacheTs = now;
138
- return info;
139
- }
140
90
  let cachedGitHubApk = null;
141
91
  let gitHubApkCacheTs = 0;
142
92
  const GITHUB_APK_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
@@ -1472,30 +1422,20 @@ export async function startServer(config, configPath) {
1472
1422
  process.stdout.write(`${line}\n`);
1473
1423
  });
1474
1424
  }
1475
- /** 当前更新通道:stable(npm @latest,semver)或 beta(GitHub beta 分支,按 commit)。 */
1476
- const getUpdateChannel = () => storage.getConfigValue("updateChannel") === "beta" ? "beta" : "stable";
1425
+ /** 当前更新通道:stable(npm @latest,纯 tag)或 beta(npm @beta,tag + commit 尾标)。 */
1426
+ const getUpdateChannel = () => normalizeUpdateChannel(storage.getConfigValue("updateChannel"));
1477
1427
  app.get("/api/check-update", async (_req, res) => {
1478
1428
  try {
1479
1429
  const channel = getUpdateChannel();
1480
- if (channel === "beta") {
1481
- const b = await checkBetaUpdate(true);
1482
- res.json({
1483
- channel: "beta",
1484
- current: b.current,
1485
- latest: b.latest,
1486
- updateAvailable: b.updateAvailable,
1487
- builtAt: b.remoteBuiltAt,
1488
- });
1489
- return;
1490
- }
1491
- const result = await checkNpmLatestVersion(true);
1492
- // 离开 beta 边界:当前跑的是 beta 构建、但通道切回 stable 时,强制提示可更新,
1493
- // 让用户能从 beta 装回干净的正式版(否则 semver 相等会卡在 beta 构建上)。
1494
- const onBetaBuild = BUILD_INFO.channel === "beta";
1430
+ const info = await checkLatestPackageVersion(channel, true);
1495
1431
  res.json({
1496
- channel: "stable",
1497
- ...result,
1498
- updateAvailable: result.updateAvailable || onBetaBuild,
1432
+ ...info,
1433
+ build: {
1434
+ commit: BUILD_INFO.commit,
1435
+ shortCommit: BUILD_INFO.commit ? BUILD_INFO.commit.slice(0, 7) : null,
1436
+ builtAt: BUILD_INFO.builtAt,
1437
+ channel: BUILD_INFO.channel,
1438
+ },
1499
1439
  });
1500
1440
  }
1501
1441
  catch (error) {
@@ -1511,33 +1451,22 @@ export async function startServer(config, configPath) {
1511
1451
  updateInFlight = true;
1512
1452
  try {
1513
1453
  const channel = getUpdateChannel();
1514
- let installSpec;
1515
- let targetLabel;
1516
- if (channel === "beta") {
1517
- const b = await checkBetaUpdate(true);
1518
- if (!b.updateAvailable) {
1519
- res.json({ ok: true, message: "已是最新 Beta 构建。" });
1520
- return;
1521
- }
1522
- installSpec = BETA_INSTALL_SPEC;
1523
- targetLabel = `Beta ${b.latest}`;
1454
+ const info = await checkLatestPackageVersion(channel, true);
1455
+ if (!info.latest) {
1456
+ res.status(502).json({ error: "无法连接到 npm registry。" });
1457
+ return;
1524
1458
  }
1525
- else {
1526
- const { updateAvailable, latest } = await checkNpmLatestVersion(true);
1527
- const onBetaBuild = BUILD_INFO.channel === "beta";
1528
- if (!updateAvailable && !onBetaBuild) {
1529
- res.json({ ok: true, message: "已经是最新版本。" });
1530
- return;
1531
- }
1532
- installSpec = `${PKG_NAME}@latest`;
1533
- targetLabel = onBetaBuild ? "最新正式版" : latest;
1459
+ if (!info.updateAvailable) {
1460
+ res.json({ ok: true, message: channel === "beta" ? "已是最新 Beta 版本。" : "已经是最新版本。" });
1461
+ return;
1534
1462
  }
1463
+ const targetLabel = info.latest;
1535
1464
  if (!canUseDetachedUpdateHelper()) {
1536
1465
  res.status(500).json({ error: "当前平台暂不支持 Web 异步更新,请在终端运行 install.sh 更新。" });
1537
1466
  return;
1538
1467
  }
1539
1468
  const helper = startDetachedUpdateHelper({
1540
- installSpec,
1469
+ installSpec: info.installSpec,
1541
1470
  configPath,
1542
1471
  parentPid: process.pid,
1543
1472
  cliArgs: process.argv.slice(2),
@@ -1553,7 +1482,7 @@ export async function startServer(config, configPath) {
1553
1482
  wsManager.emitEvent({
1554
1483
  type: "notification",
1555
1484
  sessionId: "__system__",
1556
- data: { kind: "auto-update-restart", current: PKG_VERSION, latest: targetLabel },
1485
+ data: { kind: "auto-update-restart", current: info.current, latest: targetLabel },
1557
1486
  });
1558
1487
  res.json({
1559
1488
  ok: true,
@@ -2258,19 +2187,9 @@ export async function startServer(config, configPath) {
2258
2187
  // ── Auto-update logic ──
2259
2188
  async function performAutoUpdate() {
2260
2189
  const channel = getUpdateChannel();
2261
- let info;
2262
- let updateSpec;
2263
- if (channel === "beta") {
2264
- const b = await checkBetaUpdate(true);
2265
- info = { current: b.current, latest: b.latest, updateAvailable: b.updateAvailable };
2266
- updateSpec = BETA_INSTALL_SPEC;
2267
- }
2268
- else {
2269
- info = await checkNpmLatestVersion(true);
2270
- updateSpec = `${PKG_NAME}@latest`;
2271
- }
2190
+ const info = await checkLatestPackageVersion(channel, true);
2272
2191
  cachedUpdateInfo = info;
2273
- if (!info.updateAvailable)
2192
+ if (!info.latest || !info.updateAvailable)
2274
2193
  return;
2275
2194
  const autoEnabled = storage.getConfigValue("autoUpdateWeb") === "true";
2276
2195
  if (!autoEnabled) {
@@ -2291,7 +2210,7 @@ export async function startServer(config, configPath) {
2291
2210
  data: { kind: "auto-update-start", current: info.current, latest: info.latest },
2292
2211
  });
2293
2212
  try {
2294
- await npmInstallGlobal(updateSpec, 120000);
2213
+ await npmInstallGlobal(info.installSpec, 120000);
2295
2214
  // 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),重启才会跑到新版。
2296
2215
  const repair = repairServiceUnitAfterUpdate(configPath);
2297
2216
  if (repair.scope)
@@ -5,7 +5,7 @@
5
5
  * 日志面板:因为日志在主进程里,attach 端没法直接看;这里改成"活动流"——
6
6
  * 监听 snapshot 差分,把会话起止 / 总数变化打到 log 面板。
7
7
  */
8
- import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, uninstallService, } from "./commands.js";
8
+ import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, uninstallService, } from "./commands.js";
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
11
11
  import { IpcClient } from "./ipc-client.js";
@@ -203,25 +203,27 @@ export function startAttachTui(deps) {
203
203
  }
204
204
  async function handleUpdate() {
205
205
  layout.showToast("正在检查更新…", "info", 2000);
206
- const info = await runOffMicrotask(() => checkUpdate(deps.pidInfo.version));
206
+ const channel = await runOffMicrotask(() => readUpdateChannel(deps.configPath));
207
+ const info = await runOffMicrotask(() => checkUpdate(deps.pidInfo.version, channel));
207
208
  if (!info.latest) {
208
- layout.showToast("无法连接到 npm registry", "error", 3500);
209
+ layout.showToast(info.channel === "beta" ? "无法读取 npm beta 版本" : "无法连接到 npm registry", "error", 3500);
209
210
  return;
210
211
  }
211
212
  if (!info.hasUpdate) {
212
- layout.showToast(`已是最新版本 (v${info.current})`, "success", 3000);
213
+ layout.showToast(info.channel === "beta" ? `已是最新 Beta 版本 (${info.current})` : `已是最新版本 (v${info.current})`, "success", 3000);
213
214
  return;
214
215
  }
216
+ const channelLabel = info.channel === "beta" ? "Beta" : "正式版";
215
217
  const go = await layout.confirm({
216
218
  title: "发现新版本",
217
- body: `当前 v${info.current} → 最新 v${info.latest},立即升级?升级后请按 R 重启服务。`,
219
+ body: `通道 ${channelLabel}:当前 ${info.current} → 最新 ${info.latest},立即升级?升级后请按 R 重启服务。`,
218
220
  yes: "回车 / y 安装",
219
221
  no: "Esc / n 取消",
220
222
  });
221
223
  if (!go)
222
224
  return;
223
225
  layout.showToast("正在执行 npm install -g …", "info", 5000);
224
- const r = await runOffMicrotask(() => installUpdate());
226
+ const r = await runOffMicrotask(() => installUpdate(info.channel));
225
227
  layout.showToast(r.message, r.ok ? "success" : "error", 5000);
226
228
  if (r.detail)
227
229
  layout.showDetail(r.ok ? "更新输出" : "更新失败", r.detail);
@@ -4,6 +4,7 @@
4
4
  * 所有命令统一返回 CommandResult,UI 层负责把结果渲染到 toast / 弹窗 / 日志。
5
5
  * 命令本身不直接写 stdout / stderr —— TUI 模式下 stderr 已经被 log-bus 劫持。
6
6
  */
7
+ import { type UpdateChannel } from "../npm-update-utils.js";
7
8
  export interface CommandResult {
8
9
  ok: boolean;
9
10
  /** 给用户看的简短状态行(一行)。 */
@@ -18,14 +19,17 @@ export interface CommandResult {
18
19
  */
19
20
  export declare function restartSelf(): CommandResult;
20
21
  export interface UpdateInfo {
22
+ channel: UpdateChannel;
21
23
  current: string;
22
24
  latest: string | null;
23
25
  hasUpdate: boolean;
26
+ installSpec: string;
24
27
  }
25
- /** 通过 npm view 拿到最新版本号。失败返回 latest=null。 */
26
- export declare function checkUpdate(currentVersion: string): UpdateInfo;
28
+ export declare function readUpdateChannel(configPath: string): UpdateChannel;
29
+ /** 通过 npm dist-tag 拿到当前通道最新版本号。失败返回 latest=null。 */
30
+ export declare function checkUpdate(currentVersion: string, channel?: UpdateChannel): UpdateInfo;
27
31
  /**
28
- * 执行 `npm install -g @co0ontty/wand@latest`。
32
+ * 执行 `npm install -g @co0ontty/wand@<dist-tag>`。
29
33
  *
30
34
  * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
31
35
  * 通过 npm-update-utils 自动处理 `.wand-XXXXXX` 残留目录和 ENOTEMPTY 回退,
@@ -33,7 +37,7 @@ export declare function checkUpdate(currentVersion: string): UpdateInfo;
33
37
  *
34
38
  * 返回 npm 输出供调试。
35
39
  */
36
- export declare function installUpdate(): CommandResult;
40
+ export declare function installUpdate(channel?: UpdateChannel): CommandResult;
37
41
  export declare function openInBrowser(url: string): CommandResult;
38
42
  export declare function copyToClipboard(text: string): CommandResult;
39
43
  /**
@@ -5,15 +5,15 @@
5
5
  * 命令本身不直接写 stdout / stderr —— TUI 模式下 stderr 已经被 log-bus 劫持。
6
6
  */
7
7
  import { spawn, spawnSync } from "node:child_process";
8
- import { compareSemver } from "../version-utils.js";
9
- import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
10
9
  import os from "node:os";
11
10
  import path from "node:path";
12
11
  import process from "node:process";
13
- import { installPackageGloballySync, resolveGlobalWandCli } from "../npm-update-utils.js";
12
+ import { fileURLToPath } from "node:url";
13
+ import { checkPackageUpdateSync, getInstallSpecForChannel, installPackageGloballySync, normalizeUpdateChannel, resolveGlobalWandCli, } from "../npm-update-utils.js";
14
14
  import { whichSync } from "../path-repair.js";
15
15
  import { computeRelaunch } from "../relaunch.js";
16
- const PACKAGE_NAME = "@co0ontty/wand";
16
+ import { ensureDatabaseFile, resolveDatabasePath, WandStorage } from "../storage.js";
17
17
  // ─── 重启 ────────────────────────────────────────────────────────────────
18
18
  /**
19
19
  * 重启当前进程。
@@ -48,24 +48,44 @@ export function restartSelf() {
48
48
  return { ok: false, message: `重启失败: ${errMsg(err)}` };
49
49
  }
50
50
  }
51
- /** 通过 npm view 拿到最新版本号。失败返回 latest=null。 */
52
- export function checkUpdate(currentVersion) {
53
- const res = spawnSync("npm", ["view", PACKAGE_NAME, "version"], {
54
- encoding: "utf8",
55
- timeout: 15_000,
56
- });
57
- if (res.status !== 0 || !res.stdout) {
58
- return { current: currentVersion, latest: null, hasUpdate: false };
51
+ // ─── 检查 / 安装更新 ────────────────────────────────────────────────────
52
+ const TUI_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
53
+ function readLocalBuildChannel() {
54
+ try {
55
+ const raw = readFileSync(path.resolve(TUI_MODULE_DIR, "..", "build-info.json"), "utf8");
56
+ const parsed = JSON.parse(raw);
57
+ return typeof parsed.channel === "string" ? parsed.channel : null;
58
+ }
59
+ catch {
60
+ return null;
59
61
  }
60
- const latest = res.stdout.trim();
62
+ }
63
+ export function readUpdateChannel(configPath) {
64
+ const dbPath = resolveDatabasePath(configPath);
65
+ ensureDatabaseFile(dbPath);
66
+ const storage = new WandStorage(dbPath);
67
+ try {
68
+ return normalizeUpdateChannel(storage.getConfigValue("updateChannel"));
69
+ }
70
+ finally {
71
+ storage.close();
72
+ }
73
+ }
74
+ /** 通过 npm dist-tag 拿到当前通道最新版本号。失败返回 latest=null。 */
75
+ export function checkUpdate(currentVersion, channel = "stable") {
76
+ const info = checkPackageUpdateSync(currentVersion, channel);
77
+ const updateAvailable = info.updateAvailable ||
78
+ (channel === "stable" && !!info.latest && readLocalBuildChannel() === "beta");
61
79
  return {
62
- current: currentVersion,
63
- latest,
64
- hasUpdate: compareSemver(latest, currentVersion) > 0,
80
+ channel: info.channel,
81
+ current: info.current,
82
+ latest: info.latest,
83
+ hasUpdate: updateAvailable,
84
+ installSpec: info.installSpec,
65
85
  };
66
86
  }
67
87
  /**
68
- * 执行 `npm install -g @co0ontty/wand@latest`。
88
+ * 执行 `npm install -g @co0ontty/wand@<dist-tag>`。
69
89
  *
70
90
  * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
71
91
  * 通过 npm-update-utils 自动处理 `.wand-XXXXXX` 残留目录和 ENOTEMPTY 回退,
@@ -73,8 +93,8 @@ export function checkUpdate(currentVersion) {
73
93
  *
74
94
  * 返回 npm 输出供调试。
75
95
  */
76
- export function installUpdate() {
77
- const res = installPackageGloballySync(`${PACKAGE_NAME}@latest`, 180_000);
96
+ export function installUpdate(channel = "stable") {
97
+ const res = installPackageGloballySync(getInstallSpecForChannel(channel), 180_000);
78
98
  const out = (res.stdout || "") + (res.stderr ? "\n" + res.stderr : "");
79
99
  const trail = res.attempts.length > 1
80
100
  ? `\n\n尝试过的命令:\n ${res.attempts.join("\n ")}`
package/dist/tui/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, restartSelf, uninstallService, } from "./commands.js";
1
+ import { checkUpdate, copyToClipboard, installService, installUpdate, isServiceInstalled, openInBrowser, readUpdateChannel, restartSelf, uninstallService, } from "./commands.js";
2
2
  import { repairServiceUnitAfterUpdate } from "../service-self-repair.js";
3
3
  import { buildLayout } from "./layout.js";
4
4
  import { installLogBus, restoreLogBus } from "./log-bus.js";
@@ -161,25 +161,27 @@ export function startTui(deps) {
161
161
  }
162
162
  async function handleUpdate() {
163
163
  layout.showToast("正在检查更新…", "info", 2000);
164
- const info = await runOffMicrotask(() => checkUpdate(deps.version));
164
+ const channel = await runOffMicrotask(() => readUpdateChannel(deps.configPath));
165
+ const info = await runOffMicrotask(() => checkUpdate(deps.version, channel));
165
166
  if (!info.latest) {
166
- layout.showToast("无法连接到 npm registry", "error", 3500);
167
+ layout.showToast(info.channel === "beta" ? "无法读取 npm beta 版本" : "无法连接到 npm registry", "error", 3500);
167
168
  return;
168
169
  }
169
170
  if (!info.hasUpdate) {
170
- layout.showToast(`已是最新版本 (v${info.current})`, "success", 3000);
171
+ layout.showToast(info.channel === "beta" ? `已是最新 Beta 版本 (${info.current})` : `已是最新版本 (v${info.current})`, "success", 3000);
171
172
  return;
172
173
  }
174
+ const channelLabel = info.channel === "beta" ? "Beta" : "正式版";
173
175
  const go = await layout.confirm({
174
176
  title: "发现新版本",
175
- body: `当前 v${info.current} → 最新 v${info.latest},是否立即升级?`,
177
+ body: `通道 ${channelLabel}:当前 ${info.current} → 最新 ${info.latest},是否立即升级?`,
176
178
  yes: "回车 / y 安装",
177
179
  no: "Esc / n 取消",
178
180
  });
179
181
  if (!go)
180
182
  return;
181
183
  layout.showToast("正在执行 npm install -g …", "info", 5000);
182
- const r = await runOffMicrotask(() => installUpdate());
184
+ const r = await runOffMicrotask(() => installUpdate(info.channel));
183
185
  layout.showToast(r.message, r.ok ? "success" : "error", 5000);
184
186
  if (r.detail)
185
187
  layout.showDetail(r.ok ? "更新输出" : "更新失败", r.detail);