@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.
- package/dist/build-info.json +3 -3
- package/dist/claude-sdk-runner.d.ts +2 -0
- package/dist/claude-sdk-runner.js +2 -0
- package/dist/cli.js +231 -4
- package/dist/git-quick-commit.d.ts +13 -2
- package/dist/git-quick-commit.js +259 -17
- package/dist/models.js +11 -6
- package/dist/pidfile.js +17 -1
- package/dist/server-session-routes.js +12 -3
- package/dist/server.d.ts +7 -0
- package/dist/server.js +47 -7
- package/dist/tui/commands.js +56 -6
- package/dist/tui/index.js +2 -2
- package/dist/tui/ipc-server.js +20 -2
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/git-quick-commit.js
CHANGED
|
@@ -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
|
|
304
|
-
const message = raw
|
|
408
|
+
const raw = await callAiText(prompt, cwd, language, ai);
|
|
409
|
+
const message = normalizeAiText(raw);
|
|
305
410
|
if (!message) {
|
|
306
|
-
throw new QuickCommitError("
|
|
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
|
|
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
|
|
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
|
|
474
|
+
message = normalizeAiText(raw);
|
|
370
475
|
suggestedTag = undefined;
|
|
371
476
|
}
|
|
372
477
|
if (!message) {
|
|
373
|
-
throw new QuickCommitError("
|
|
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
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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 <你的新密码>");
|