@co0ontty/wand 1.72.0 → 1.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,17 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
2
3
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
+ import { buildChildEnv } from "./env-utils.js";
3
5
  import { runGit as runGitBase, runGitRaw as runGitRawBase, getGitErrorMessage } from "./git-utils.js";
6
+ import { applyThinkingEffortToPrompt, thinkingEffortToCodexReasoningEffort } from "./structured-session-manager.js";
4
7
  const GIT_TIMEOUT_MS = 1500;
5
8
  const GIT_PUSH_TIMEOUT_MS = 30_000;
6
9
  const MAX_FILE_ENTRIES = 200;
7
10
  // AI 生成 message/tag 的超时。SDK 链路 = spawn claude + API 调用(带自动重试),
8
11
  // 30s 在 API 抖动时不够用,放宽到 60s。
9
12
  const CLAUDE_MESSAGE_TIMEOUT_MS = 60_000;
13
+ const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
14
+ const QUICK_COMMIT_CLI_TIMEOUT_MS = 120_000;
10
15
  const MAX_DIFF_FOR_AI = 100_000;
11
16
  const GIT_MAX_BUFFER = 16 * 1024 * 1024;
12
17
  function runGit(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
@@ -257,9 +262,9 @@ export class QuickCommitError extends Error {
257
262
  }
258
263
  }
259
264
  // ── AI commit message generation ──
260
- async function callClaudeText(prompt, cwd, language) {
265
+ async function callClaudeText(prompt, cwd, language, model) {
261
266
  try {
262
- return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS, language });
267
+ return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS, language, model: model ?? undefined });
263
268
  }
264
269
  catch (error) {
265
270
  if (error instanceof ClaudeRunError) {
@@ -275,6 +280,106 @@ async function callClaudeText(prompt, cwd, language) {
275
280
  throw error;
276
281
  }
277
282
  }
283
+ function normalizeProvider(provider) {
284
+ return provider === "codex" ? "codex" : "claude";
285
+ }
286
+ function stripFences(raw) {
287
+ return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
288
+ }
289
+ function normalizeAiText(raw) {
290
+ return stripFences(raw).replace(/^["'`]+|["'`]+$/g, "").trim();
291
+ }
292
+ function extractCodexText(stdout) {
293
+ let lastAgentText = "";
294
+ for (const line of stdout.split(/\r?\n/)) {
295
+ const trimmed = line.trim();
296
+ if (!trimmed.startsWith("{"))
297
+ continue;
298
+ try {
299
+ const parsed = JSON.parse(trimmed);
300
+ if (parsed.type === "item.completed" && parsed.item?.type === "agent_message" && typeof parsed.item.text === "string") {
301
+ lastAgentText = parsed.item.text;
302
+ }
303
+ }
304
+ catch {
305
+ // ignore non-JSON diagnostics mixed into stdout
306
+ }
307
+ }
308
+ if (lastAgentText)
309
+ return lastAgentText.trim();
310
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
311
+ const noise = /^(OpenAI Codex|[-]+$|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|codex$|tokens used$|[0-9,]+$)/i;
312
+ for (let i = lines.length - 1; i >= 0; i--) {
313
+ if (!noise.test(lines[i]))
314
+ return lines[i];
315
+ }
316
+ return "";
317
+ }
318
+ function runCliText(command, args, prompt, opts) {
319
+ return new Promise((resolve, reject) => {
320
+ const child = spawn(command, args, {
321
+ cwd: opts.cwd,
322
+ env: buildChildEnv(opts.inheritEnv !== false),
323
+ stdio: ["pipe", "pipe", "pipe"],
324
+ });
325
+ let stdout = "";
326
+ let stderr = "";
327
+ let settled = false;
328
+ const timeout = setTimeout(() => {
329
+ settled = true;
330
+ child.kill("SIGTERM");
331
+ reject(new QuickCommitError(`${command} 调用超时。`, "CLAUDE_TIMEOUT"));
332
+ }, opts.timeoutMs);
333
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
334
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
335
+ child.on("error", (error) => {
336
+ if (settled)
337
+ return;
338
+ settled = true;
339
+ clearTimeout(timeout);
340
+ const code = error.code === "ENOENT" ? "CLAUDE_CLI_MISSING" : "CLAUDE_CLI_FAILED";
341
+ reject(new QuickCommitError(error.code === "ENOENT" ? `未找到 ${command} CLI。` : `${command} CLI 失败:${error.message}`, code));
342
+ });
343
+ child.on("close", (code) => {
344
+ if (settled)
345
+ return;
346
+ settled = true;
347
+ clearTimeout(timeout);
348
+ if (code === 0) {
349
+ resolve(stdout);
350
+ return;
351
+ }
352
+ reject(new QuickCommitError(`${command} CLI 失败:${(stderr || stdout).trim() || `exit ${code}`}`, "CLAUDE_CLI_FAILED"));
353
+ });
354
+ child.stdin?.end(prompt);
355
+ });
356
+ }
357
+ async function callCodexText(prompt, cwd, opts) {
358
+ const args = ["exec", "--json", "--color", "never", "--skip-git-repo-check", "--sandbox", "read-only"];
359
+ const model = opts.model?.trim();
360
+ if (model && model !== "default")
361
+ args.push("--model", model);
362
+ const reasoningEffort = thinkingEffortToCodexReasoningEffort(opts.thinkingEffort ?? "off");
363
+ if (reasoningEffort)
364
+ args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
365
+ args.push("-");
366
+ const stdout = await runCliText("codex", args, prompt, {
367
+ cwd,
368
+ timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
369
+ inheritEnv: opts.inheritEnv,
370
+ });
371
+ const text = extractCodexText(stdout);
372
+ if (!text) {
373
+ throw new QuickCommitError("Codex 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
374
+ }
375
+ return text;
376
+ }
377
+ async function callAiText(prompt, cwd, language, opts) {
378
+ if (normalizeProvider(opts.provider) === "codex") {
379
+ return callCodexText(prompt, cwd, opts);
380
+ }
381
+ return callClaudeText(prompt, cwd, language, opts.model);
382
+ }
278
383
  function collectStagedDiff(cwd) {
279
384
  let diff;
280
385
  try {
@@ -296,14 +401,14 @@ function collectStagedDiff(cwd) {
296
401
  }
297
402
  return diff;
298
403
  }
299
- async function generateCommitMessage(cwd, language) {
404
+ async function generateCommitMessage(cwd, language, ai = {}) {
300
405
  const diff = collectStagedDiff(cwd);
301
406
  const lang = language.trim() || "中文";
302
407
  const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
303
- const raw = await callClaudeText(prompt, cwd, language);
304
- const message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
408
+ const raw = await callAiText(prompt, cwd, language, ai);
409
+ const message = normalizeAiText(raw);
305
410
  if (!message) {
306
- throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
411
+ throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
307
412
  }
308
413
  return message;
309
414
  }
@@ -334,7 +439,7 @@ function sanitizeSuggestedTag(value) {
334
439
  return undefined;
335
440
  return cleaned;
336
441
  }
337
- async function generateCommitMessageWithTag(cwd, language) {
442
+ async function generateCommitMessageWithTag(cwd, language, ai = {}) {
338
443
  const diff = collectStagedDiff(cwd);
339
444
  let latestTag;
340
445
  try {
@@ -356,25 +461,25 @@ async function generateCommitMessageWithTag(cwd, language) {
356
461
 
357
462
  git diff:
358
463
  ${diff}`;
359
- const raw = await callClaudeText(prompt, cwd, language);
464
+ const raw = await callAiText(prompt, cwd, language, ai);
360
465
  const parsed = tryParseJson(raw);
361
466
  let message;
362
467
  let suggestedTag;
363
468
  if (parsed && typeof parsed.message === "string") {
364
- message = parsed.message.replace(/^["'`]+|["'`]+$/g, "").trim();
469
+ message = normalizeAiText(parsed.message);
365
470
  suggestedTag = sanitizeSuggestedTag(parsed.tag);
366
471
  }
367
472
  else {
368
473
  // Fallback: treat whole output as message, no tag suggestion
369
- message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
474
+ message = normalizeAiText(raw);
370
475
  suggestedTag = undefined;
371
476
  }
372
477
  if (!message) {
373
- throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
478
+ throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
374
479
  }
375
480
  return { message, suggestedTag };
376
481
  }
377
- export async function generateCommitMessageOnly(cwd, language) {
482
+ export async function generateCommitMessageOnly(cwd, language, ai = {}) {
378
483
  if (!cwd || !existsSync(cwd)) {
379
484
  throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
380
485
  }
@@ -384,13 +489,13 @@ export async function generateCommitMessageOnly(cwd, language) {
384
489
  catch {
385
490
  // best-effort staging so the diff is complete
386
491
  }
387
- return generateCommitMessageWithTag(cwd, language);
492
+ return generateCommitMessageWithTag(cwd, language, ai);
388
493
  }
389
494
  /**
390
495
  * Ask Claude for a single tag string. Called from `runQuickCommit` after the commit has
391
496
  * already landed, so we look at `git show HEAD` and use `HEAD~1` for the previous tag.
392
497
  */
393
- async function generateTagAfterCommit(cwd, language, commitMessage) {
498
+ async function generateTagAfterCommit(cwd, language, commitMessage, ai = {}) {
394
499
  let diff;
395
500
  try {
396
501
  diff = runGit(["show", "HEAD", "--no-color", "--submodule=log"], cwd, 5000);
@@ -430,7 +535,7 @@ commit message:${commitMessage}
430
535
 
431
536
  git diff:
432
537
  ${diff}`;
433
- const raw = await callClaudeText(prompt, cwd, language);
538
+ const raw = await callAiText(prompt, cwd, language, ai);
434
539
  const parsed = tryParseJson(raw);
435
540
  let suggested;
436
541
  if (parsed && typeof parsed.tag === "string") {
@@ -766,8 +871,145 @@ function collectSubmodulesForPush(cwd) {
766
871
  }
767
872
  return { base, infos };
768
873
  }
874
+ function buildFallbackPrompt(opts, priorError) {
875
+ const lang = opts.language.trim() || "中文";
876
+ const messageLine = opts.autoMessage === false
877
+ ? `- 使用这个 commit message:${(opts.customMessage || "").trim()}`
878
+ : `- 先根据当前 staged/unstaged diff 生成一条简洁的 ${lang} commit message(祈使句,不超过 50 字)`;
879
+ const tagLine = opts.tag?.trim()
880
+ ? `- 提交后创建 tag:${opts.tag.trim()}`
881
+ : opts.autoTag
882
+ ? "- 提交后根据改动幅度创建下一个语义化版本 tag"
883
+ : "- 不创建 tag";
884
+ const pushLine = opts.push ? "- 提交完成后推送当前分支;如果创建了 tag,也推送该 tag" : "- 不执行 push";
885
+ const submoduleLine = opts.submodule
886
+ ? "- 如果 submodule 内部也有改动,先在对应 submodule 内 add/commit,再提交父仓库里的 submodule 指针"
887
+ : "- 不进入 submodule 内部提交,只提交父仓库自身已纳入的改动";
888
+ return [
889
+ "你正在作为 Wand 的快捷提交兜底执行器运行。前置的内置快捷提交流程失败了,现在请直接用 CLI 工具完成同一件事。",
890
+ "",
891
+ "约束:",
892
+ "- 只允许执行与 git 快捷提交直接相关的命令,例如 git status、git diff、git add、git commit、git tag、git push、git submodule status。",
893
+ "- 不要修改源代码内容,不要运行测试,不要安装依赖,不要重构文件。",
894
+ "- 如果没有可提交改动,明确说明并停止,不要创建空 commit。",
895
+ "- commit message 和自然语言输出使用 " + lang + "。",
896
+ "",
897
+ "任务:",
898
+ "- 执行 git add -A 纳入当前改动。",
899
+ messageLine,
900
+ tagLine,
901
+ pushLine,
902
+ submoduleLine,
903
+ "",
904
+ `内置流程失败原因:${priorError}`,
905
+ "",
906
+ "完成后只输出一行 JSON:{\"ok\":true,\"message\":\"...\",\"tag\":\"...\"}。失败时输出一行 JSON:{\"ok\":false,\"error\":\"...\"}。",
907
+ ].join("\n");
908
+ }
909
+ function getHead(cwd) {
910
+ try {
911
+ return runGit(["rev-parse", "HEAD"], cwd);
912
+ }
913
+ catch {
914
+ return null;
915
+ }
916
+ }
917
+ function getHeadSummary(cwd) {
918
+ try {
919
+ const raw = runGit(["log", "-1", "--pretty=format:%h%x09%s"], cwd);
920
+ const parts = raw.split("\t");
921
+ return { hash: parts[0] ?? "", message: parts.slice(1).join("\t") };
922
+ }
923
+ catch {
924
+ return { hash: "", message: "" };
925
+ }
926
+ }
927
+ function getLatestTagAtHead(cwd) {
928
+ try {
929
+ return runGit(["describe", "--tags", "--exact-match", "HEAD"], cwd) || undefined;
930
+ }
931
+ catch {
932
+ return undefined;
933
+ }
934
+ }
935
+ async function runQuickCommitFallbackCli(opts, priorError) {
936
+ assertGitWorkTree(opts.cwd);
937
+ const beforeHead = getHead(opts.cwd);
938
+ const prompt = buildFallbackPrompt(opts, priorError);
939
+ const provider = normalizeProvider(opts.provider);
940
+ if (provider === "codex") {
941
+ const args = ["exec", "--json", "--color", "never", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
942
+ const model = opts.model?.trim();
943
+ if (model && model !== "default")
944
+ args.push("--model", model);
945
+ const reasoningEffort = thinkingEffortToCodexReasoningEffort(opts.thinkingEffort ?? "off");
946
+ if (reasoningEffort)
947
+ args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
948
+ args.push("-");
949
+ await runCliText("codex", args, prompt, {
950
+ cwd: opts.cwd,
951
+ timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
952
+ inheritEnv: opts.inheritEnv,
953
+ });
954
+ }
955
+ else {
956
+ const args = ["-p", "--verbose", "--output-format", "stream-json"];
957
+ const model = opts.model?.trim();
958
+ if (model && model !== "default")
959
+ args.push("--model", model);
960
+ args.push("--permission-mode", "bypassPermissions");
961
+ const effectivePrompt = applyThinkingEffortToPrompt(prompt, opts.thinkingEffort ?? "off");
962
+ await runCliText("claude", args, effectivePrompt, {
963
+ cwd: opts.cwd,
964
+ timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
965
+ inheritEnv: opts.inheritEnv,
966
+ });
967
+ }
968
+ const afterHead = getHead(opts.cwd);
969
+ if (!afterHead || afterHead === beforeHead) {
970
+ throw new QuickCommitError("CLI 兜底没有创建新的 commit。", "GIT_COMMIT_FAILED");
971
+ }
972
+ const commit = getHeadSummary(opts.cwd);
973
+ const tag = getLatestTagAtHead(opts.cwd);
974
+ return {
975
+ ok: true,
976
+ commit,
977
+ tag: tag ? { name: tag } : undefined,
978
+ pushed: false,
979
+ };
980
+ }
981
+ function shouldFallbackToCli(error) {
982
+ return ![
983
+ "CWD_MISSING",
984
+ "NO_CWD",
985
+ "NOT_A_GIT_REPO",
986
+ "NO_COMMIT",
987
+ "NOTHING_TO_COMMIT",
988
+ "NOTHING_TO_PUSH",
989
+ "EMPTY_MESSAGE",
990
+ "EMPTY_TAG",
991
+ "TAG_EXISTS",
992
+ ].includes(error.code);
993
+ }
994
+ export async function runQuickCommitWithFallback(opts) {
995
+ try {
996
+ return await runQuickCommit(opts);
997
+ }
998
+ catch (error) {
999
+ if (error instanceof QuickCommitError && shouldFallbackToCli(error)) {
1000
+ return runQuickCommitFallbackCli(opts, error.message);
1001
+ }
1002
+ throw error;
1003
+ }
1004
+ }
769
1005
  export async function runQuickCommit(opts) {
770
1006
  const { cwd, language, autoMessage, customMessage, tag, autoTag, push, submodule } = opts;
1007
+ const ai = {
1008
+ provider: opts.provider,
1009
+ model: opts.model,
1010
+ thinkingEffort: opts.thinkingEffort,
1011
+ inheritEnv: opts.inheritEnv,
1012
+ };
771
1013
  assertGitWorkTree(cwd);
772
1014
  // 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
773
1015
  // AI 生成 message 时也基于这个 staged diff。
@@ -802,7 +1044,7 @@ export async function runQuickCommit(opts) {
802
1044
  }
803
1045
  let message;
804
1046
  if (autoMessage) {
805
- message = await generateCommitMessage(cwd, language);
1047
+ message = await generateCommitMessage(cwd, language, ai);
806
1048
  }
807
1049
  else {
808
1050
  message = (customMessage || "").trim();
@@ -855,7 +1097,7 @@ export async function runQuickCommit(opts) {
855
1097
  // Tag: explicit `tag` wins; if empty + autoTag, ask Claude; otherwise skip.
856
1098
  let tagName = (tag || "").trim();
857
1099
  if (!tagName && autoTag) {
858
- tagName = await generateTagAfterCommit(cwd, language, message);
1100
+ tagName = await generateTagAfterCommit(cwd, language, message, ai);
859
1101
  }
860
1102
  if (tagName) {
861
1103
  try {
package/dist/models.js CHANGED
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  import { extractSemver } from "./version-utils.js";
4
4
  const execAsync = promisify(exec);
5
5
  const CLAUDE_MODELS = [
6
- { id: "default", label: "default(跟随 Claude Code 默认)", alias: true },
6
+ { id: "default", label: "Sonnet 4.6 · claude-sonnet-4-6(Claude Code 默认)", alias: true },
7
7
  { id: "opus", label: "opus(最新 Opus)", alias: true },
8
8
  { id: "sonnet", label: "sonnet(最新 Sonnet)", alias: true },
9
9
  { id: "haiku", label: "haiku(最新 Haiku)", alias: true },
@@ -13,7 +13,7 @@ const CLAUDE_MODELS = [
13
13
  { id: "claude-haiku-4-5-20251001", label: "Haiku 4.5 · claude-haiku-4-5-20251001" },
14
14
  ];
15
15
  const CODEX_FALLBACK_MODELS = [
16
- { id: "default", label: "default(跟随 Codex 默认)", alias: true },
16
+ { id: "default", label: "GPT-5.5 · gpt-5.5(Codex 默认)", alias: true },
17
17
  ];
18
18
  let cache = null;
19
19
  function cloneClaudeModels() {
@@ -37,15 +37,15 @@ async function probeCodexModels() {
37
37
  .sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99));
38
38
  if (!visible.length)
39
39
  return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
40
+ const defaultModel = visible[0];
41
+ const defaultLabel = formatCodexModelLabel(defaultModel);
40
42
  const result = [
41
- { id: "default", label: "default(跟随 Codex 默认)", alias: true },
43
+ { id: "default", label: `${defaultLabel}(Codex 默认)`, alias: true },
42
44
  ];
43
45
  for (const m of visible) {
44
46
  result.push({
45
47
  id: m.slug,
46
- label: m.display_name && m.display_name !== m.slug
47
- ? `${m.display_name} · ${m.slug}`
48
- : m.slug,
48
+ label: formatCodexModelLabel(m),
49
49
  });
50
50
  }
51
51
  return result;
@@ -54,6 +54,11 @@ async function probeCodexModels() {
54
54
  return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
55
55
  }
56
56
  }
57
+ function formatCodexModelLabel(model) {
58
+ return model.display_name && model.display_name !== model.slug
59
+ ? `${model.display_name} · ${model.slug}`
60
+ : model.slug;
61
+ }
57
62
  export function getCachedModels() {
58
63
  if (!cache) {
59
64
  cache = {
package/dist/pidfile.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * - Linux / macOS 使用 Unix domain socket。
12
12
  * - Windows 不支持,attach 模式直接跳过;新启的 `wand web` 仍会按老逻辑启动(端口冲突时报错)。
13
13
  */
14
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { chmodSync, chownSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import path from "node:path";
16
16
  import process from "node:process";
17
17
  export function pidfilePath(configPath) {
@@ -28,6 +28,7 @@ export function writePidfile(configPath, info) {
28
28
  const tmp = `${file}.tmp`;
29
29
  writeFileSync(tmp, JSON.stringify(info, null, 2) + "\n", { mode: 0o600 });
30
30
  renameSync(tmp, file);
31
+ applyConfigDirOwnership(file, configPath, 0o600);
31
32
  }
32
33
  /** 读取并校验。文件不存在 / 损坏 / 进程不在 → 返回 null。 */
33
34
  export function readPidfile(configPath) {
@@ -115,3 +116,18 @@ export function readLiveInstance(configPath) {
115
116
  return null;
116
117
  return info;
117
118
  }
119
+ function applyConfigDirOwnership(filePath, configPath, mode) {
120
+ try {
121
+ const owner = statSync(path.dirname(configPath));
122
+ chownSync(filePath, owner.uid, owner.gid);
123
+ }
124
+ catch {
125
+ /* best effort: non-root processes cannot chown, and usually already own the file */
126
+ }
127
+ try {
128
+ chmodSync(filePath, mode);
129
+ }
130
+ catch {
131
+ /* noop */
132
+ }
133
+ }
@@ -4,7 +4,7 @@ import { normalizeMode } from "./config.js";
4
4
  import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
5
5
  import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
6
6
  import { resolveSessionCwd } from "./session-cwd.js";
7
- import { getGitStatus, QuickCommitError, runQuickCommit, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
7
+ import { getGitStatus, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
8
8
  import { getErrorMessage } from "./error-utils.js";
9
9
  export { getErrorMessage };
10
10
  function getInputErrorResponse(error, sessionId) {
@@ -470,9 +470,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
470
470
  }
471
471
  const body = (req.body ?? {});
472
472
  try {
473
- const result = await runQuickCommit({
473
+ const result = await runQuickCommitWithFallback({
474
474
  cwd: snapshot.cwd,
475
475
  language: config.language ?? "",
476
+ provider: snapshot.provider,
477
+ model: snapshot.selectedModel ?? snapshot.structuredState?.model ?? config.defaultModel,
478
+ thinkingEffort: snapshot.thinkingEffort ?? config.defaultThinkingEffort,
479
+ inheritEnv: config.inheritEnv,
476
480
  autoMessage: body.autoMessage !== false,
477
481
  customMessage: typeof body.customMessage === "string" ? body.customMessage : undefined,
478
482
  tag: typeof body.tag === "string" ? body.tag : undefined,
@@ -502,7 +506,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
502
506
  return;
503
507
  }
504
508
  try {
505
- const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
509
+ const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
510
+ provider: snapshot.provider,
511
+ model: snapshot.selectedModel ?? snapshot.structuredState?.model ?? config.defaultModel,
512
+ thinkingEffort: snapshot.thinkingEffort ?? config.defaultThinkingEffort,
513
+ inheritEnv: config.inheritEnv,
514
+ });
506
515
  res.json(result);
507
516
  }
508
517
  catch (error) {
package/dist/server.d.ts CHANGED
@@ -22,4 +22,11 @@ export interface ServerHandle {
22
22
  pathRepair: PathRepairResult;
23
23
  close(): Promise<void>;
24
24
  }
25
+ export declare class PortInUseError extends Error {
26
+ readonly port: number;
27
+ readonly host: string;
28
+ readonly code = "EADDRINUSE";
29
+ constructor(port: number, host: string);
30
+ }
31
+ export declare function isPortInUseError(error: unknown): error is PortInUseError;
25
32
  export declare function startServer(config: WandConfig, configPath: string): Promise<ServerHandle>;
package/dist/server.js CHANGED
@@ -978,6 +978,23 @@ function streamFileWithRange(req, res, options) {
978
978
  });
979
979
  stream.pipe(res);
980
980
  }
981
+ export class PortInUseError extends Error {
982
+ port;
983
+ host;
984
+ code = "EADDRINUSE";
985
+ constructor(port, host) {
986
+ super(`Port ${port} is already in use`);
987
+ this.port = port;
988
+ this.host = host;
989
+ this.name = "PortInUseError";
990
+ }
991
+ }
992
+ export function isPortInUseError(error) {
993
+ return error instanceof PortInUseError
994
+ || (!!error
995
+ && typeof error === "object"
996
+ && error.code === "EADDRINUSE");
997
+ }
981
998
  export async function startServer(config, configPath) {
982
999
  // 关键:在创建 ProcessManager / 任何 spawn 之前先修 PATH。
983
1000
  // 服务被注册为 systemd / launchd 时,unit 文件里的 PATH 是安装那一刻烧死的,
@@ -2146,6 +2163,11 @@ export async function startServer(config, configPath) {
2146
2163
  });
2147
2164
  const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {}, useHttps);
2148
2165
  wsManager.setup((id) => structuredSessions.get(id) ?? processes.get(id));
2166
+ wss.on("error", (err) => {
2167
+ if (err.code === "EADDRINUSE")
2168
+ return;
2169
+ wandError("WebSocket 异常", err.message);
2170
+ });
2149
2171
  // Wire process events to WebSocket broadcast
2150
2172
  processes.on("process", (event) => {
2151
2173
  wsManager.emitEvent(event);
@@ -2219,7 +2241,32 @@ export async function startServer(config, configPath) {
2219
2241
  let bindAddr = config.host === "0.0.0.0" ? "0.0.0.0" : config.host;
2220
2242
  const collectedUrls = [];
2221
2243
  await new Promise((resolve, reject) => {
2244
+ const cleanupFailedListen = () => {
2245
+ try {
2246
+ wss.close();
2247
+ }
2248
+ catch { /* noop */ }
2249
+ try {
2250
+ server.close();
2251
+ }
2252
+ catch { /* noop */ }
2253
+ try {
2254
+ storage.close();
2255
+ }
2256
+ catch { /* noop */ }
2257
+ };
2258
+ const onListenError = (err) => {
2259
+ server.off("error", onListenError);
2260
+ cleanupFailedListen();
2261
+ if (err.code === "EADDRINUSE") {
2262
+ reject(new PortInUseError(config.port, config.host));
2263
+ return;
2264
+ }
2265
+ reject(err);
2266
+ };
2267
+ server.once("error", onListenError);
2222
2268
  server.listen(config.port, config.host, () => {
2269
+ server.off("error", onListenError);
2223
2270
  bindAddr = `${config.host}:${config.port}`;
2224
2271
  const scheme = useHttps ? "HTTPS" : "HTTP";
2225
2272
  // 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
@@ -2232,13 +2279,6 @@ export async function startServer(config, configPath) {
2232
2279
  }
2233
2280
  resolve();
2234
2281
  });
2235
- server.on("error", (err) => {
2236
- if (err.code === "EADDRINUSE") {
2237
- wandError(`端口 ${config.port} 已被占用`, `可能有另一个 Wand 进程正在运行。`, `解决方法(二选一):\n1. 在浏览器中访问当前运行的 Wand\n2. 或者终止占用端口的进程:\n kill $(lsof -ti :${config.port})\n\n如果你确定没有其他实例在运行,可能是有程序意外占用了端口。`);
2238
- process.exit(1);
2239
- }
2240
- reject(err);
2241
- });
2242
2282
  });
2243
2283
  if (!storage.hasCustomPassword() && config.password === "change-me") {
2244
2284
  wandWarn("正在使用默认密码(change-me),任何能访问本机的人都可以登录。", "修改方法:在界面右上角「设置」中修改密码,或运行:node dist/cli.js config:set password <你的新密码>");