@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.
@@ -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 extractSemverFromName(fileName.replace(/\.apk$/i, ""));
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 extractSemverFromName(fileName.replace(/\.dmg$/i, ""));
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);
@@ -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 which = spawnSync("which", ["wand"], { encoding: "utf8" });
444
- if (which.status === 0 && which.stdout)
445
- return which.stdout.trim();
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
- /** 简易语义化版本比较;返回正数 = a > b。 */
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
+ }