@co0ontty/wand 1.48.1 → 1.49.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.
- package/dist/build-info.json +3 -3
- package/dist/npm-update-utils.d.ts +19 -0
- package/dist/npm-update-utils.js +89 -3
- package/dist/server.js +40 -121
- package/dist/tui/attach.js +8 -6
- package/dist/tui/commands.d.ts +8 -4
- package/dist/tui/commands.js +39 -19
- package/dist/tui/index.js +8 -6
- package/dist/web-ui/content/scripts.js +30 -30
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +2 -2
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-06-
|
|
4
|
-
"version": "1.
|
|
2
|
+
"commit": "6066201e006fdce04120be0b3e0add7d68770375",
|
|
3
|
+
"builtAt": "2026-06-02T01:47:00.843Z",
|
|
4
|
+
"version": "1.49.0",
|
|
5
5
|
"channel": "stable"
|
|
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
|
*/
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -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
|
|
295
|
-
//
|
|
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
|
-
|
|
53
|
+
const packageUpdateCache = new Map();
|
|
54
|
+
async function checkLatestPackageVersion(channel, forceRefresh = false) {
|
|
56
55
|
const now = Date.now();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
1476
|
-
const getUpdateChannel = () => storage.getConfigValue("updateChannel")
|
|
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
|
-
|
|
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
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
2213
|
+
await npmInstallGlobal(info.installSpec, 120000);
|
|
2295
2214
|
// 镜像 install.sh:装完用全局安装刷新服务 unit(ExecStart/PATH),重启才会跑到新版。
|
|
2296
2215
|
const repair = repairServiceUnitAfterUpdate(configPath);
|
|
2297
2216
|
if (repair.scope)
|
package/dist/tui/attach.js
CHANGED
|
@@ -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
|
|
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:
|
|
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);
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
/**
|
package/dist/tui/commands.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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);
|