@co0ontty/wand 1.43.7 → 1.44.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/env-utils.d.ts +2 -0
- package/dist/env-utils.js +4 -0
- package/dist/git-quick-commit.d.ts +9 -0
- package/dist/git-quick-commit.js +191 -26
- package/dist/language-prompt.d.ts +5 -0
- package/dist/language-prompt.js +9 -0
- package/dist/models.d.ts +1 -6
- package/dist/models.js +2 -14
- package/dist/npm-update-utils.d.ts +0 -1
- package/dist/npm-update-utils.js +4 -13
- package/dist/path-repair.d.ts +4 -0
- package/dist/path-repair.js +6 -16
- package/dist/process-manager.d.ts +0 -2
- package/dist/process-manager.js +3 -13
- package/dist/pty-text-utils.d.ts +0 -2
- package/dist/pty-text-utils.js +2 -2
- package/dist/resume-policy.d.ts +0 -2
- package/dist/resume-policy.js +0 -11
- package/dist/server-session-routes.js +3 -8
- package/dist/server.js +3 -65
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +0 -3
- package/dist/structured-session-manager.d.ts +0 -5
- package/dist/structured-session-manager.js +6 -46
- package/dist/tui/commands.js +6 -16
- package/dist/types.d.ts +2 -0
- package/dist/version-utils.d.ts +16 -0
- package/dist/version-utils.js +66 -0
- package/dist/web-ui/content/scripts.js +54 -21849
- package/dist/web-ui/content/styles.css +1 -16358
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +5 -6
|
@@ -146,12 +146,10 @@ function isMergeActionAllowed(snapshot) {
|
|
|
146
146
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
|
|
147
147
|
app.get("/api/sessions", (_req, res) => {
|
|
148
148
|
const all = listAllSessionsSlim(processes, structured);
|
|
149
|
-
console.log("[WAND] GET /api/sessions count:", all.length, "sessions:", all.map(s => ({ id: s.id.substring(0, 8), kind: s.sessionKind, runner: s.runner, status: s.status })));
|
|
150
149
|
res.json(all);
|
|
151
150
|
});
|
|
152
151
|
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
153
152
|
const body = req.body;
|
|
154
|
-
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model, thinkingEffort: body.thinkingEffort }));
|
|
155
153
|
try {
|
|
156
154
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
|
|
157
155
|
res.status(400).json({ error: "结构化会话当前仅支持 Claude 或 Codex provider。" });
|
|
@@ -169,7 +167,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
169
167
|
? body.thinkingEffort
|
|
170
168
|
: undefined,
|
|
171
169
|
});
|
|
172
|
-
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
173
170
|
onSessionCreated?.(body.cwd ?? snapshot.cwd);
|
|
174
171
|
const prompt = body.prompt?.trim();
|
|
175
172
|
if (prompt) {
|
|
@@ -244,7 +241,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
244
241
|
// 让退出 handler 不要把剩余 queuedMessages 清空(默认行为是清空)。
|
|
245
242
|
const preserveQueue = !!req.body?.preserveQueue;
|
|
246
243
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
247
|
-
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "preserveQueue:", preserveQueue, "idempotencyKey:", idempotencyKey);
|
|
248
244
|
try {
|
|
249
245
|
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
|
|
250
246
|
res.json(snapshot);
|
|
@@ -430,6 +426,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
430
426
|
tag: typeof body.tag === "string" ? body.tag : undefined,
|
|
431
427
|
autoTag: !!body.autoTag,
|
|
432
428
|
push: !!body.push,
|
|
429
|
+
submodule: !!body.submodule,
|
|
433
430
|
});
|
|
434
431
|
res.json(result);
|
|
435
432
|
}
|
|
@@ -510,6 +507,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
510
507
|
cwd: snapshot.cwd,
|
|
511
508
|
pushCommits: body.pushCommits !== false,
|
|
512
509
|
pushTags: !!body.pushTags,
|
|
510
|
+
submodule: !!body.submodule,
|
|
511
|
+
tagName: typeof body.tag === "string" ? body.tag : undefined,
|
|
513
512
|
});
|
|
514
513
|
res.json(result);
|
|
515
514
|
}
|
|
@@ -591,10 +590,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
591
590
|
app.post("/api/sessions/:id/resume", (req, res) => {
|
|
592
591
|
const sessionId = req.params.id;
|
|
593
592
|
const body = req.body;
|
|
594
|
-
console.log("[WAND] POST /api/sessions/:id/resume sessionId:", sessionId);
|
|
595
593
|
try {
|
|
596
594
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
597
|
-
console.log("[WAND] resume lookup: found:", !!existingSession, "sessionKind:", existingSession?.sessionKind, "claudeSessionId:", existingSession?.claudeSessionId);
|
|
598
595
|
if (!existingSession) {
|
|
599
596
|
res.status(404).json({ error: "会话不存在。" });
|
|
600
597
|
return;
|
|
@@ -644,7 +641,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
644
641
|
app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
|
|
645
642
|
const claudeSessionId = String(req.params.claudeSessionId || "").trim();
|
|
646
643
|
const body = req.body;
|
|
647
|
-
console.log("[WAND] POST /api/claude-sessions/:claudeSessionId/resume claudeSessionId:", claudeSessionId, "cwd:", body.cwd);
|
|
648
644
|
try {
|
|
649
645
|
if (!claudeSessionId) {
|
|
650
646
|
res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
|
|
@@ -722,7 +718,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
722
718
|
app.post("/api/codex-sessions/:threadId/resume", express.json(), async (req, res) => {
|
|
723
719
|
const threadId = String(req.params.threadId || "").trim();
|
|
724
720
|
const body = req.body;
|
|
725
|
-
console.log("[WAND] POST /api/codex-sessions/:threadId/resume threadId:", threadId, "cwd:", body.cwd);
|
|
726
721
|
try {
|
|
727
722
|
if (!threadId) {
|
|
728
723
|
res.status(400).json({ error: "Codex 会话 ID 不能为空。" });
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { compareSemver, extractSemver } from "./version-utils.js";
|
|
2
3
|
import compression from "compression";
|
|
3
4
|
import express from "express";
|
|
4
5
|
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -70,56 +71,6 @@ async function checkNpmLatestVersion(forceRefresh = false) {
|
|
|
70
71
|
updateAvailable: latest !== PKG_VERSION && compareSemver(latest, PKG_VERSION) > 0,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
function compareSemver(a, b) {
|
|
74
|
-
const parse = (v) => {
|
|
75
|
-
const [main, ...rest] = v.split("-");
|
|
76
|
-
const pre = rest.join("-");
|
|
77
|
-
const mainParts = main.split(".").map((n) => Number(n) || 0);
|
|
78
|
-
return { mainParts, pre };
|
|
79
|
-
};
|
|
80
|
-
const pa = parse(a);
|
|
81
|
-
const pb = parse(b);
|
|
82
|
-
for (let i = 0; i < 3; i++) {
|
|
83
|
-
const diff = (pa.mainParts[i] || 0) - (pb.mainParts[i] || 0);
|
|
84
|
-
if (diff !== 0)
|
|
85
|
-
return diff;
|
|
86
|
-
}
|
|
87
|
-
// Main version equal — apply semver prerelease rule: no prerelease > with prerelease.
|
|
88
|
-
if (!pa.pre && pb.pre)
|
|
89
|
-
return 1;
|
|
90
|
-
if (pa.pre && !pb.pre)
|
|
91
|
-
return -1;
|
|
92
|
-
if (!pa.pre && !pb.pre)
|
|
93
|
-
return 0;
|
|
94
|
-
// Both have prerelease: 按 . 分段比较 (数字段数值比, 非数字段字典序), 贴近标准 semver,
|
|
95
|
-
// 避免跨月/跨年的 debug.MMDDHHMM 后缀因纯字典序而排反。
|
|
96
|
-
const segA = pa.pre.split(".");
|
|
97
|
-
const segB = pb.pre.split(".");
|
|
98
|
-
const segLen = Math.max(segA.length, segB.length);
|
|
99
|
-
for (let i = 0; i < segLen; i++) {
|
|
100
|
-
const sa = segA[i];
|
|
101
|
-
const sb = segB[i];
|
|
102
|
-
if (sa === undefined)
|
|
103
|
-
return -1; // 段少者更小
|
|
104
|
-
if (sb === undefined)
|
|
105
|
-
return 1;
|
|
106
|
-
const na = Number(sa);
|
|
107
|
-
const nb = Number(sb);
|
|
108
|
-
const aIsNum = sa !== "" && !Number.isNaN(na);
|
|
109
|
-
const bIsNum = sb !== "" && !Number.isNaN(nb);
|
|
110
|
-
if (aIsNum && bIsNum) {
|
|
111
|
-
if (na !== nb)
|
|
112
|
-
return na < nb ? -1 : 1;
|
|
113
|
-
}
|
|
114
|
-
else if (aIsNum !== bIsNum) {
|
|
115
|
-
return aIsNum ? -1 : 1; // 数字段 < 非数字段
|
|
116
|
-
}
|
|
117
|
-
else if (sa !== sb) {
|
|
118
|
-
return sa < sb ? -1 : 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
|
123
74
|
/** 读取 dist/build-info.json(由 scripts/stamp-build-info.js 在 build 时生成)。 */
|
|
124
75
|
function readBuildInfo() {
|
|
125
76
|
try {
|
|
@@ -382,15 +333,6 @@ async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
|
382
333
|
return { user, assistant };
|
|
383
334
|
}
|
|
384
335
|
// ── Git helpers ──
|
|
385
|
-
async function isGitRepo(dirPath) {
|
|
386
|
-
try {
|
|
387
|
-
await execAsync("git rev-parse --is-inside-work-tree", { cwd: dirPath });
|
|
388
|
-
return true;
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
336
|
async function getGitRepoRoot(dirPath) {
|
|
395
337
|
try {
|
|
396
338
|
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd: dirPath });
|
|
@@ -576,10 +518,6 @@ function normalizeMode(input, fallback) {
|
|
|
576
518
|
return isExecutionMode(input) ? input : fallback;
|
|
577
519
|
}
|
|
578
520
|
/** Match a semver-looking token in a file name (with optional pre-release / build metadata). */
|
|
579
|
-
function extractSemverFromName(name) {
|
|
580
|
-
const match = name.match(/(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)/);
|
|
581
|
-
return match ? match[1] : null;
|
|
582
|
-
}
|
|
583
521
|
function resolveAndroidApkDir(configDir, config) {
|
|
584
522
|
const configuredDir = config.android?.apkDir?.trim();
|
|
585
523
|
if (!configuredDir) {
|
|
@@ -588,7 +526,7 @@ function resolveAndroidApkDir(configDir, config) {
|
|
|
588
526
|
return path.isAbsolute(configuredDir) ? configuredDir : path.resolve(configDir, configuredDir);
|
|
589
527
|
}
|
|
590
528
|
function extractAndroidApkVersion(fileName) {
|
|
591
|
-
return
|
|
529
|
+
return extractSemver(fileName.replace(/\.apk$/i, ""));
|
|
592
530
|
}
|
|
593
531
|
async function resolveAndroidApkAsset(configDir, config) {
|
|
594
532
|
if (config.android?.enabled !== true)
|
|
@@ -666,7 +604,7 @@ function resolveMacosDmgDir(configDir, config) {
|
|
|
666
604
|
return path.isAbsolute(configuredDir) ? configuredDir : path.resolve(configDir, configuredDir);
|
|
667
605
|
}
|
|
668
606
|
function extractMacosDmgVersion(fileName) {
|
|
669
|
-
return
|
|
607
|
+
return extractSemver(fileName.replace(/\.dmg$/i, ""));
|
|
670
608
|
}
|
|
671
609
|
async function resolveMacosDmgAsset(configDir, config) {
|
|
672
610
|
if (config.macos?.enabled !== true)
|
package/dist/storage.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ export declare class WandStorage {
|
|
|
32
32
|
getAppSecret(): string | null;
|
|
33
33
|
/** Persist appSecret in database (DB is the authoritative source after first migration) */
|
|
34
34
|
setAppSecret(value: string): void;
|
|
35
|
-
hasAppSecret(): boolean;
|
|
36
35
|
saveAuthSession(token: string, expiresAt: number): void;
|
|
37
36
|
getAuthSession(token: string): PersistedAuthSession | null;
|
|
38
37
|
deleteAuthSession(token: string): void;
|
package/dist/storage.js
CHANGED
|
@@ -310,9 +310,6 @@ export class WandStorage {
|
|
|
310
310
|
setAppSecret(value) {
|
|
311
311
|
this.setConfigValue("appSecret", value);
|
|
312
312
|
}
|
|
313
|
-
hasAppSecret() {
|
|
314
|
-
return this.getAppSecret() !== null;
|
|
315
|
-
}
|
|
316
313
|
// ============ Auth Session Methods ============
|
|
317
314
|
saveAuthSession(token, expiresAt) {
|
|
318
315
|
this.db
|
|
@@ -88,10 +88,6 @@ export declare class StructuredSessionManager {
|
|
|
88
88
|
idempotencyKey?: string;
|
|
89
89
|
preserveQueue?: boolean;
|
|
90
90
|
}): Promise<SessionSnapshot>;
|
|
91
|
-
/** Approve a pending permission request. */
|
|
92
|
-
approvePermission(sessionId: string): SessionSnapshot;
|
|
93
|
-
/** Deny a pending permission request. */
|
|
94
|
-
denyPermission(sessionId: string): SessionSnapshot;
|
|
95
91
|
/**
|
|
96
92
|
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
97
93
|
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
@@ -125,7 +121,6 @@ export declare class StructuredSessionManager {
|
|
|
125
121
|
private emitStructuredSnapshot;
|
|
126
122
|
private flushNextQueuedMessage;
|
|
127
123
|
private emit;
|
|
128
|
-
private resolvePermission;
|
|
129
124
|
private incrementApprovalStats;
|
|
130
125
|
private buildCodexArgs;
|
|
131
126
|
private runCodexStreaming;
|
|
@@ -7,8 +7,8 @@ import path from "node:path";
|
|
|
7
7
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
9
9
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
10
|
-
import { buildChildEnv } from "./env-utils.js";
|
|
11
|
-
import { buildLanguageDirective } from "./language-prompt.js";
|
|
10
|
+
import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
11
|
+
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
12
12
|
function defaultStructuredRunner(provider) {
|
|
13
13
|
return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
|
|
14
14
|
}
|
|
@@ -169,9 +169,6 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
|
169
169
|
* authoritative final snapshot. */
|
|
170
170
|
const STREAM_SAVE_THROTTLE_MS = 200;
|
|
171
171
|
const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
|
|
172
|
-
function isRunningAsRoot() {
|
|
173
|
-
return process.getuid?.() === 0 || process.geteuid?.() === 0;
|
|
174
|
-
}
|
|
175
172
|
/**
|
|
176
173
|
* 检测当前系统是否使用 musl libc(Alpine Linux 等)。
|
|
177
174
|
* Node.js 进程报告中 glibcVersionRuntime 仅在 glibc 系统存在;musl 系统为 undefined。
|
|
@@ -385,9 +382,7 @@ function buildAppendSystemPromptParts(language, mode) {
|
|
|
385
382
|
const isChinese = trimmedLanguage === "中文";
|
|
386
383
|
const parts = [];
|
|
387
384
|
if (mode === "managed") {
|
|
388
|
-
parts.push(isChinese
|
|
389
|
-
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
390
|
-
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
385
|
+
parts.push(buildManagedAutonomyDirective(isChinese));
|
|
391
386
|
}
|
|
392
387
|
if (trimmedLanguage) {
|
|
393
388
|
const directive = buildLanguageDirective(trimmedLanguage);
|
|
@@ -610,7 +605,6 @@ export class StructuredSessionManager {
|
|
|
610
605
|
if (opts?.idempotencyKey) {
|
|
611
606
|
const mapKey = `${id}:${opts.idempotencyKey}`;
|
|
612
607
|
if (this.seenIdempotencyKeys.has(mapKey)) {
|
|
613
|
-
console.log("[WAND] sendMessage: duplicate idempotency key rejected", { id, key: opts.idempotencyKey });
|
|
614
608
|
const err = new Error("检测到重复发送,已拦截。");
|
|
615
609
|
err.code = "duplicate_idempotency_key";
|
|
616
610
|
throw err;
|
|
@@ -773,17 +767,6 @@ export class StructuredSessionManager {
|
|
|
773
767
|
throw error;
|
|
774
768
|
}
|
|
775
769
|
}
|
|
776
|
-
// ---------------------------------------------------------------------------
|
|
777
|
-
// Permission resolution (called from server routes)
|
|
778
|
-
// ---------------------------------------------------------------------------
|
|
779
|
-
/** Approve a pending permission request. */
|
|
780
|
-
approvePermission(sessionId) {
|
|
781
|
-
return this.resolvePermission(sessionId, true);
|
|
782
|
-
}
|
|
783
|
-
/** Deny a pending permission request. */
|
|
784
|
-
denyPermission(sessionId) {
|
|
785
|
-
return this.resolvePermission(sessionId, false);
|
|
786
|
-
}
|
|
787
770
|
/**
|
|
788
771
|
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
789
772
|
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
@@ -1055,31 +1038,6 @@ export class StructuredSessionManager {
|
|
|
1055
1038
|
this.emitEvent(event);
|
|
1056
1039
|
}
|
|
1057
1040
|
}
|
|
1058
|
-
resolvePermission(sessionId, approved) {
|
|
1059
|
-
const session = this.requireSession(sessionId);
|
|
1060
|
-
const scope = session.pendingEscalation?.scope;
|
|
1061
|
-
if (approved && scope) {
|
|
1062
|
-
this.incrementApprovalStats(session, scope);
|
|
1063
|
-
}
|
|
1064
|
-
const updated = {
|
|
1065
|
-
...session,
|
|
1066
|
-
pendingEscalation: null,
|
|
1067
|
-
permissionBlocked: false,
|
|
1068
|
-
lastEscalationResult: session.pendingEscalation ? {
|
|
1069
|
-
requestId: session.pendingEscalation.requestId,
|
|
1070
|
-
resolution: approved ? "approve_once" : "deny",
|
|
1071
|
-
reason: approved ? "user_approved" : "user_denied",
|
|
1072
|
-
} : session.lastEscalationResult ?? null,
|
|
1073
|
-
};
|
|
1074
|
-
this.sessions.set(sessionId, updated);
|
|
1075
|
-
this.storage.saveSession(updated);
|
|
1076
|
-
this.emit({
|
|
1077
|
-
type: "status",
|
|
1078
|
-
sessionId,
|
|
1079
|
-
data: { permissionBlocked: false, approvalStats: updated.approvalStats, sessionKind: "structured" },
|
|
1080
|
-
});
|
|
1081
|
-
return updated;
|
|
1082
|
-
}
|
|
1083
1041
|
incrementApprovalStats(session, scope) {
|
|
1084
1042
|
const prev = session.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 };
|
|
1085
1043
|
const stats = { ...prev };
|
|
@@ -2417,7 +2375,7 @@ export class StructuredSessionManager {
|
|
|
2417
2375
|
output: turnState.result,
|
|
2418
2376
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2419
2377
|
messages: msgs,
|
|
2420
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
2378
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
2421
2379
|
pendingEscalation: null,
|
|
2422
2380
|
permissionBlocked: false,
|
|
2423
2381
|
structuredState: {
|
|
@@ -2437,6 +2395,8 @@ export class StructuredSessionManager {
|
|
|
2437
2395
|
return;
|
|
2438
2396
|
if (interruptPrompt) {
|
|
2439
2397
|
this.interruptedWith.delete(sessionId);
|
|
2398
|
+
// 与 codex/cli runner 对齐:清掉"保留队列"标记,避免 stale flag 影响下一次普通 interrupt。
|
|
2399
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2440
2400
|
setImmediate(() => {
|
|
2441
2401
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|
|
2442
2402
|
console.error("[WAND] sdk interrupt-and-send failed:", err);
|
package/dist/tui/commands.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
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";
|
|
8
9
|
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
9
10
|
import os from "node:os";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import process from "node:process";
|
|
12
13
|
import { installPackageGloballySync, resolveGlobalWandCli } from "../npm-update-utils.js";
|
|
14
|
+
import { whichSync } from "../path-repair.js";
|
|
13
15
|
import { computeRelaunch } from "../relaunch.js";
|
|
14
16
|
const PACKAGE_NAME = "@co0ontty/wand";
|
|
15
17
|
// ─── 重启 ────────────────────────────────────────────────────────────────
|
|
@@ -440,9 +442,9 @@ function resolveWandBin(ctx) {
|
|
|
440
442
|
const argv1 = process.argv[1];
|
|
441
443
|
if (argv1 && existsSync(argv1))
|
|
442
444
|
return argv1;
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
445
|
-
return
|
|
445
|
+
const found = whichSync("wand");
|
|
446
|
+
if (found)
|
|
447
|
+
return found;
|
|
446
448
|
return "wand";
|
|
447
449
|
}
|
|
448
450
|
/**
|
|
@@ -676,16 +678,4 @@ function uninstallLaunchdService(scope) {
|
|
|
676
678
|
function errMsg(err) {
|
|
677
679
|
return err instanceof Error ? err.message : String(err);
|
|
678
680
|
}
|
|
679
|
-
|
|
680
|
-
function compareSemver(a, b) {
|
|
681
|
-
const pa = a.replace(/^v/, "").split(/[.\-+]/).map((s) => Number.parseInt(s, 10));
|
|
682
|
-
const pb = b.replace(/^v/, "").split(/[.\-+]/).map((s) => Number.parseInt(s, 10));
|
|
683
|
-
const len = Math.max(pa.length, pb.length);
|
|
684
|
-
for (let i = 0; i < len; i++) {
|
|
685
|
-
const x = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
686
|
-
const y = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
687
|
-
if (x !== y)
|
|
688
|
-
return x - y;
|
|
689
|
-
}
|
|
690
|
-
return 0;
|
|
691
|
-
}
|
|
681
|
+
// compareSemver 已统一到 ../version-utils.ts
|
package/dist/types.d.ts
CHANGED
|
@@ -198,6 +198,8 @@ export interface GitStatusResult {
|
|
|
198
198
|
};
|
|
199
199
|
/** Most recent tag reachable from HEAD (`git describe --tags --abbrev=0`), if any. */
|
|
200
200
|
latestTag?: string;
|
|
201
|
+
/** True 当仓库声明了 submodule(任一改动条目为 submodule)。前端据此决定是否渲染 Submodule 球。 */
|
|
202
|
+
hasSubmodule?: boolean;
|
|
201
203
|
error?: string;
|
|
202
204
|
}
|
|
203
205
|
export interface QuickCommitResult {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语义化版本工具:单一真源,供 server / tui / path-repair / models 共用,
|
|
3
|
+
* 避免各处各写一份比较/提取逻辑导致 debug.MMDDHHMM 后缀排序不一致。
|
|
4
|
+
*/
|
|
5
|
+
/** 从任意文本中提取 X.Y.Z[-/+后缀] 形式的版本号(带捕获组)。 */
|
|
6
|
+
export declare const SEMVER_PATTERN: RegExp;
|
|
7
|
+
/** 提取文本中的第一个语义化版本号,没有则返回 null。 */
|
|
8
|
+
export declare function extractSemver(text: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* 比较两个语义化版本号,返回正数 = a > b,负数 = a < b,0 = 相等。
|
|
11
|
+
* - 容忍前导 `v`(如 nvm/fnm 的 v18.0.0 目录名)。
|
|
12
|
+
* - 主版本逐段数值比较;相等时按 semver 规则:无 prerelease > 有 prerelease。
|
|
13
|
+
* - 两者都有 prerelease 时按 `.` 分段比较(数字段数值比、非数字段字典序),
|
|
14
|
+
* 贴近标准 semver,避免 debug.MMDDHHMM 后缀因纯字典序而跨月/跨年排反。
|
|
15
|
+
*/
|
|
16
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语义化版本工具:单一真源,供 server / tui / path-repair / models 共用,
|
|
3
|
+
* 避免各处各写一份比较/提取逻辑导致 debug.MMDDHHMM 后缀排序不一致。
|
|
4
|
+
*/
|
|
5
|
+
/** 从任意文本中提取 X.Y.Z[-/+后缀] 形式的版本号(带捕获组)。 */
|
|
6
|
+
export const SEMVER_PATTERN = /(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)/;
|
|
7
|
+
/** 提取文本中的第一个语义化版本号,没有则返回 null。 */
|
|
8
|
+
export function extractSemver(text) {
|
|
9
|
+
const match = text.match(SEMVER_PATTERN);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 比较两个语义化版本号,返回正数 = a > b,负数 = a < b,0 = 相等。
|
|
14
|
+
* - 容忍前导 `v`(如 nvm/fnm 的 v18.0.0 目录名)。
|
|
15
|
+
* - 主版本逐段数值比较;相等时按 semver 规则:无 prerelease > 有 prerelease。
|
|
16
|
+
* - 两者都有 prerelease 时按 `.` 分段比较(数字段数值比、非数字段字典序),
|
|
17
|
+
* 贴近标准 semver,避免 debug.MMDDHHMM 后缀因纯字典序而跨月/跨年排反。
|
|
18
|
+
*/
|
|
19
|
+
export function compareSemver(a, b) {
|
|
20
|
+
const parse = (v) => {
|
|
21
|
+
const [main, ...rest] = v.replace(/^v/, "").split("-");
|
|
22
|
+
const pre = rest.join("-");
|
|
23
|
+
const mainParts = main.split(".").map((n) => Number(n) || 0);
|
|
24
|
+
return { mainParts, pre };
|
|
25
|
+
};
|
|
26
|
+
const pa = parse(a);
|
|
27
|
+
const pb = parse(b);
|
|
28
|
+
for (let i = 0; i < 3; i++) {
|
|
29
|
+
const diff = (pa.mainParts[i] || 0) - (pb.mainParts[i] || 0);
|
|
30
|
+
if (diff !== 0)
|
|
31
|
+
return diff;
|
|
32
|
+
}
|
|
33
|
+
// Main version equal — apply semver prerelease rule: no prerelease > with prerelease.
|
|
34
|
+
if (!pa.pre && pb.pre)
|
|
35
|
+
return 1;
|
|
36
|
+
if (pa.pre && !pb.pre)
|
|
37
|
+
return -1;
|
|
38
|
+
if (!pa.pre && !pb.pre)
|
|
39
|
+
return 0;
|
|
40
|
+
const segA = pa.pre.split(".");
|
|
41
|
+
const segB = pb.pre.split(".");
|
|
42
|
+
const segLen = Math.max(segA.length, segB.length);
|
|
43
|
+
for (let i = 0; i < segLen; i++) {
|
|
44
|
+
const sa = segA[i];
|
|
45
|
+
const sb = segB[i];
|
|
46
|
+
if (sa === undefined)
|
|
47
|
+
return -1; // 段少者更小
|
|
48
|
+
if (sb === undefined)
|
|
49
|
+
return 1;
|
|
50
|
+
const na = Number(sa);
|
|
51
|
+
const nb = Number(sb);
|
|
52
|
+
const aIsNum = sa !== "" && !Number.isNaN(na);
|
|
53
|
+
const bIsNum = sb !== "" && !Number.isNaN(nb);
|
|
54
|
+
if (aIsNum && bIsNum) {
|
|
55
|
+
if (na !== nb)
|
|
56
|
+
return na < nb ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
else if (aIsNum !== bIsNum) {
|
|
59
|
+
return aIsNum ? -1 : 1; // 数字段 < 非数字段
|
|
60
|
+
}
|
|
61
|
+
else if (sa !== sb) {
|
|
62
|
+
return sa < sb ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|