@co0ontty/wand 1.1.7 → 1.2.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/claude-pty-bridge.js +3 -6
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +19 -192
- package/dist/pty-text-utils.d.ts +2 -0
- package/dist/pty-text-utils.js +20 -0
- package/dist/resume-policy.d.ts +80 -0
- package/dist/resume-policy.js +178 -0
- package/dist/server-session-routes.d.ts +6 -0
- package/dist/server-session-routes.js +359 -0
- package/dist/server.js +20 -331
- package/dist/web-ui/content/scripts.js +435 -45
- package/dist/web-ui/content/styles.css +143 -18
- package/dist/ws-broadcast.d.ts +1 -1
- package/package.json +3 -2
package/dist/server.js
CHANGED
|
@@ -21,6 +21,8 @@ const PKG_REPO_URL = "https://github.com/co0ontty/wand";
|
|
|
21
21
|
let cachedLatestVersion = null;
|
|
22
22
|
let cacheTimestamp = 0;
|
|
23
23
|
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
24
|
+
/** Cached update result broadcast to new clients on connect. */
|
|
25
|
+
let cachedUpdateInfo = null;
|
|
24
26
|
async function checkNpmLatestVersion(forceRefresh = false) {
|
|
25
27
|
const now = Date.now();
|
|
26
28
|
if (forceRefresh || !cachedLatestVersion || (now - cacheTimestamp > CACHE_TTL_MS)) {
|
|
@@ -54,11 +56,11 @@ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
|
54
56
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
55
57
|
import { ensureCertificates } from "./cert.js";
|
|
56
58
|
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
57
|
-
import { ProcessManager
|
|
59
|
+
import { ProcessManager } from "./process-manager.js";
|
|
60
|
+
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
61
|
+
import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
58
62
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
59
63
|
import { renderApp } from "./web-ui/index.js";
|
|
60
|
-
import { parseMessages } from "./message-parser.js";
|
|
61
|
-
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
62
64
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
63
65
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
64
66
|
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
@@ -66,35 +68,6 @@ import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./mi
|
|
|
66
68
|
function getErrorMessage(error, fallback) {
|
|
67
69
|
return error instanceof Error ? error.message : fallback;
|
|
68
70
|
}
|
|
69
|
-
function getInputErrorResponse(error, sessionId) {
|
|
70
|
-
if (error instanceof SessionInputError) {
|
|
71
|
-
const statusCode = error.code === "SESSION_NOT_FOUND" ? 404 : 409;
|
|
72
|
-
return {
|
|
73
|
-
statusCode,
|
|
74
|
-
payload: {
|
|
75
|
-
error: error.message,
|
|
76
|
-
errorCode: error.code,
|
|
77
|
-
sessionId,
|
|
78
|
-
sessionStatus: error.sessionStatus ?? null,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
statusCode: 400,
|
|
84
|
-
payload: {
|
|
85
|
-
error: getErrorMessage(error, "会话已结束,请启动新会话。"),
|
|
86
|
-
errorCode: "INPUT_SEND_FAILED",
|
|
87
|
-
sessionId,
|
|
88
|
-
sessionStatus: null,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function getInputDebugMeta(error) {
|
|
93
|
-
if (error instanceof Error) {
|
|
94
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
95
|
-
}
|
|
96
|
-
return { error };
|
|
97
|
-
}
|
|
98
71
|
// ── Git helpers ──
|
|
99
72
|
async function isGitRepo(dirPath) {
|
|
100
73
|
try {
|
|
@@ -384,6 +357,9 @@ export async function startServer(config, configPath) {
|
|
|
384
357
|
defaultMode: config.defaultMode,
|
|
385
358
|
defaultCwd: config.defaultCwd,
|
|
386
359
|
commandPresets: config.commandPresets,
|
|
360
|
+
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
361
|
+
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
362
|
+
currentVersion: PKG_VERSION,
|
|
387
363
|
});
|
|
388
364
|
});
|
|
389
365
|
// ── Settings endpoints ──
|
|
@@ -504,152 +480,8 @@ export async function startServer(config, configPath) {
|
|
|
504
480
|
updateInFlight = false;
|
|
505
481
|
}
|
|
506
482
|
});
|
|
507
|
-
app
|
|
508
|
-
|
|
509
|
-
});
|
|
510
|
-
app.get("/api/claude-history", (_req, res) => {
|
|
511
|
-
try {
|
|
512
|
-
const sessions = processes.listClaudeHistorySessions();
|
|
513
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
514
|
-
const filtered = hidden.size > 0
|
|
515
|
-
? sessions.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
|
|
516
|
-
: sessions;
|
|
517
|
-
res.json(filtered);
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
res.status(500).json({ error: getErrorMessage(error, "无法扫描 Claude 历史会话。") });
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
|
|
524
|
-
const claudeSessionId = req.params.claudeSessionId?.trim();
|
|
525
|
-
if (!claudeSessionId) {
|
|
526
|
-
res.status(400).json({ error: "会话 ID 不能为空。" });
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
const session = processes.listClaudeHistorySessions()
|
|
530
|
-
.find((s) => s.claudeSessionId === claudeSessionId);
|
|
531
|
-
if (session) {
|
|
532
|
-
processes.deleteClaudeHistoryFiles([{ claudeSessionId, cwd: session.cwd }]);
|
|
533
|
-
removeFromHiddenClaudeSessionIds(storage, [claudeSessionId]);
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
537
|
-
if (!hidden.has(claudeSessionId)) {
|
|
538
|
-
hidden.add(claudeSessionId);
|
|
539
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
res.json({ ok: true });
|
|
543
|
-
});
|
|
544
|
-
app.delete("/api/claude-history", (req, res) => {
|
|
545
|
-
const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
|
|
546
|
-
if (!cwd) {
|
|
547
|
-
res.status(400).json({ error: "目录不能为空。" });
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
const sessions = processes.listClaudeHistorySessions();
|
|
552
|
-
const toDelete = [];
|
|
553
|
-
for (const session of sessions) {
|
|
554
|
-
if (session.claudeSessionId && session.cwd === cwd) {
|
|
555
|
-
toDelete.push({ claudeSessionId: session.claudeSessionId, cwd: session.cwd });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
559
|
-
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
560
|
-
res.json({ ok: true, deleted });
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
app.post("/api/claude-history/batch-delete", express.json(), (req, res) => {
|
|
567
|
-
const claudeSessionIds = Array.isArray(req.body?.claudeSessionIds)
|
|
568
|
-
? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
569
|
-
: [];
|
|
570
|
-
if (claudeSessionIds.length === 0) {
|
|
571
|
-
res.status(400).json({ error: "至少提供一个历史会话 ID。" });
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
try {
|
|
575
|
-
const allSessions = processes.listClaudeHistorySessions();
|
|
576
|
-
const sessionMap = new Map();
|
|
577
|
-
for (const s of allSessions) {
|
|
578
|
-
if (s.claudeSessionId)
|
|
579
|
-
sessionMap.set(s.claudeSessionId, s.cwd);
|
|
580
|
-
}
|
|
581
|
-
const toDelete = [];
|
|
582
|
-
const toHide = [];
|
|
583
|
-
for (const id of claudeSessionIds) {
|
|
584
|
-
const cwd = sessionMap.get(id);
|
|
585
|
-
if (cwd) {
|
|
586
|
-
toDelete.push({ claudeSessionId: id, cwd });
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
toHide.push(id);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
593
|
-
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
594
|
-
if (toHide.length > 0) {
|
|
595
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
596
|
-
let added = 0;
|
|
597
|
-
for (const id of toHide) {
|
|
598
|
-
if (!hidden.has(id)) {
|
|
599
|
-
hidden.add(id);
|
|
600
|
-
added++;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (added > 0)
|
|
604
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
605
|
-
}
|
|
606
|
-
res.json({ ok: true, deleted: deleted + toHide.length });
|
|
607
|
-
}
|
|
608
|
-
catch (error) {
|
|
609
|
-
res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
|
|
613
|
-
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
614
|
-
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
615
|
-
: [];
|
|
616
|
-
if (sessionIds.length === 0) {
|
|
617
|
-
res.status(400).json({ error: "至少提供一个会话 ID。" });
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
let deleted = 0;
|
|
621
|
-
const failed = [];
|
|
622
|
-
for (const sessionId of sessionIds) {
|
|
623
|
-
try {
|
|
624
|
-
processes.delete(sessionId);
|
|
625
|
-
deleted += 1;
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
failed.push(sessionId);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (deleted === 0 && failed.length > 0) {
|
|
632
|
-
res.status(400).json({ error: "无法批量删除会话。", failed });
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
res.json({ ok: true, deleted, failed });
|
|
636
|
-
});
|
|
637
|
-
app.get("/api/sessions/:id", (req, res) => {
|
|
638
|
-
const snapshot = processes.get(req.params.id);
|
|
639
|
-
if (!snapshot) {
|
|
640
|
-
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (req.query.format === "chat") {
|
|
644
|
-
const messages = snapshot.messages && snapshot.messages.length > 0
|
|
645
|
-
? snapshot.messages
|
|
646
|
-
: parseMessages(snapshot.output);
|
|
647
|
-
res.json({ ...snapshot, messages });
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
res.json(snapshot);
|
|
651
|
-
}
|
|
652
|
-
});
|
|
483
|
+
registerSessionRoutes(app, processes, storage, config.defaultMode);
|
|
484
|
+
registerClaudeHistoryRoutes(app, processes, storage);
|
|
653
485
|
// ── Path suggestion ──
|
|
654
486
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
655
487
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
@@ -977,156 +809,6 @@ export async function startServer(config, configPath) {
|
|
|
977
809
|
res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
|
|
978
810
|
}
|
|
979
811
|
});
|
|
980
|
-
app.post("/api/sessions/:id/resume", (req, res) => {
|
|
981
|
-
const sessionId = req.params.id;
|
|
982
|
-
const body = req.body;
|
|
983
|
-
try {
|
|
984
|
-
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
985
|
-
if (!existingSession) {
|
|
986
|
-
res.status(404).json({ error: "会话不存在。" });
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
const claudeSessionId = existingSession.claudeSessionId;
|
|
990
|
-
if (!claudeSessionId) {
|
|
991
|
-
res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const command = existingSession.command.trim();
|
|
995
|
-
if (!/^claude\b/.test(command)) {
|
|
996
|
-
res.status(400).json({ error: "只有 Claude 命令支持恢复功能。" });
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
const newMode = body.mode
|
|
1000
|
-
? normalizeMode(body.mode, config.defaultMode)
|
|
1001
|
-
: normalizeMode(existingSession.mode, config.defaultMode);
|
|
1002
|
-
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
1003
|
-
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: sessionId });
|
|
1004
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
1005
|
-
res.status(201).json({ resumedFromSessionId: sessionId, ...newSnapshot });
|
|
1006
|
-
}
|
|
1007
|
-
catch (error) {
|
|
1008
|
-
res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
|
|
1012
|
-
const claudeSessionId = String(req.params.claudeSessionId || "").trim();
|
|
1013
|
-
const body = req.body;
|
|
1014
|
-
try {
|
|
1015
|
-
if (!claudeSessionId) {
|
|
1016
|
-
res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
|
|
1020
|
-
if (existingSession) {
|
|
1021
|
-
const command = existingSession.command.trim();
|
|
1022
|
-
if (!/^claude\b/.test(command)) {
|
|
1023
|
-
res.status(400).json({ error: "只有 Claude 命令支持按 Claude Session ID 恢复。" });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
if (!existingSession.cwd || !processes.hasClaudeSessionFile(existingSession.cwd, claudeSessionId)) {
|
|
1027
|
-
res.status(400).json({ error: "对应的 Claude 历史会话文件不存在,无法恢复。" });
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const newMode = body.mode
|
|
1031
|
-
? normalizeMode(body.mode, config.defaultMode)
|
|
1032
|
-
: normalizeMode(existingSession.mode, config.defaultMode);
|
|
1033
|
-
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
1034
|
-
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: existingSession.id });
|
|
1035
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
1036
|
-
res.status(201).json({ resumedFromSessionId: existingSession.id, resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
1037
|
-
}
|
|
1038
|
-
else {
|
|
1039
|
-
// No existing wand session — resume directly with cwd from request body
|
|
1040
|
-
const cwd = body.cwd?.trim();
|
|
1041
|
-
if (!cwd) {
|
|
1042
|
-
res.status(400).json({ error: "未找到对应的会话记录,请提供工作目录 (cwd)。" });
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
const newMode = normalizeMode(body.mode, config.defaultMode);
|
|
1046
|
-
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
1047
|
-
const newSnapshot = processes.start(resumeCommand, cwd, newMode);
|
|
1048
|
-
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
catch (error) {
|
|
1052
|
-
res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
|
|
1053
|
-
}
|
|
1054
|
-
});
|
|
1055
|
-
app.post("/api/sessions/:id/input", (req, res) => {
|
|
1056
|
-
const body = req.body;
|
|
1057
|
-
const sessionId = req.params.id;
|
|
1058
|
-
const input = body.input ?? "";
|
|
1059
|
-
const view = body.view;
|
|
1060
|
-
const shortcutKey = body.shortcutKey;
|
|
1061
|
-
console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
|
|
1062
|
-
try {
|
|
1063
|
-
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
1064
|
-
console.error("[wand] Input request succeeded", { sessionId, status: snapshot.status, inputLength: input.length, view: view ?? "chat" });
|
|
1065
|
-
res.json(snapshot);
|
|
1066
|
-
}
|
|
1067
|
-
catch (error) {
|
|
1068
|
-
const response = getInputErrorResponse(error, sessionId);
|
|
1069
|
-
console.error("[wand] Input request failed", {
|
|
1070
|
-
sessionId, inputLength: input.length, view: view ?? "chat",
|
|
1071
|
-
responseStatus: response.statusCode, responsePayload: response.payload,
|
|
1072
|
-
error: getInputDebugMeta(error),
|
|
1073
|
-
});
|
|
1074
|
-
res.status(response.statusCode).json(response.payload);
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1077
|
-
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
1078
|
-
const body = req.body;
|
|
1079
|
-
try {
|
|
1080
|
-
const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
|
|
1081
|
-
res.json(snapshot);
|
|
1082
|
-
}
|
|
1083
|
-
catch (error) {
|
|
1084
|
-
res.status(400).json({ error: getErrorMessage(error, "无法调整终端大小。") });
|
|
1085
|
-
}
|
|
1086
|
-
});
|
|
1087
|
-
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
1088
|
-
try {
|
|
1089
|
-
res.json(processes.approvePermission(req.params.id));
|
|
1090
|
-
}
|
|
1091
|
-
catch (error) {
|
|
1092
|
-
res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
1096
|
-
try {
|
|
1097
|
-
res.json(processes.denyPermission(req.params.id));
|
|
1098
|
-
}
|
|
1099
|
-
catch (error) {
|
|
1100
|
-
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
1101
|
-
}
|
|
1102
|
-
});
|
|
1103
|
-
app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
|
|
1104
|
-
try {
|
|
1105
|
-
const { requestId } = req.params;
|
|
1106
|
-
const body = req.body;
|
|
1107
|
-
res.json(processes.resolveEscalation(req.params.id, requestId, body.resolution));
|
|
1108
|
-
}
|
|
1109
|
-
catch (error) {
|
|
1110
|
-
res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
1114
|
-
try {
|
|
1115
|
-
res.json(processes.stop(req.params.id));
|
|
1116
|
-
}
|
|
1117
|
-
catch (error) {
|
|
1118
|
-
res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
app.delete("/api/sessions/:id", (req, res) => {
|
|
1122
|
-
try {
|
|
1123
|
-
processes.delete(req.params.id);
|
|
1124
|
-
res.json({ ok: true });
|
|
1125
|
-
}
|
|
1126
|
-
catch (error) {
|
|
1127
|
-
res.status(400).json({ error: getErrorMessage(error, "无法删除会话。") });
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
812
|
// ── WebSocket broadcast layer ──
|
|
1131
813
|
const server = useHttps
|
|
1132
814
|
? (() => {
|
|
@@ -1163,9 +845,16 @@ export async function startServer(config, configPath) {
|
|
|
1163
845
|
// Start configured background sessions after the server is already reachable.
|
|
1164
846
|
processes.runStartupCommands();
|
|
1165
847
|
// Background update check on startup
|
|
1166
|
-
checkNpmLatestVersion().then((
|
|
1167
|
-
|
|
1168
|
-
|
|
848
|
+
checkNpmLatestVersion().then((info) => {
|
|
849
|
+
cachedUpdateInfo = info;
|
|
850
|
+
if (info.updateAvailable) {
|
|
851
|
+
process.stdout.write(`[wand] 发现新版本 ${info.latest}(当前 ${info.current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
|
|
852
|
+
// Broadcast update notification to all connected WS clients
|
|
853
|
+
wsManager.emitEvent({
|
|
854
|
+
type: "notification",
|
|
855
|
+
sessionId: "__system__",
|
|
856
|
+
data: { kind: "update", current: info.current, latest: info.latest },
|
|
857
|
+
});
|
|
1169
858
|
}
|
|
1170
859
|
}).catch(() => { });
|
|
1171
860
|
}
|