@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/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-06-
|
|
4
|
-
"version": "1.
|
|
2
|
+
"commit": "505999eb2201a378806a30cbcd813c16cce4cb4b",
|
|
3
|
+
"builtAt": "2026-06-19T09:43:51.607Z",
|
|
4
|
+
"version": "1.74.1",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -6,6 +6,8 @@ export declare class ClaudeRunError extends Error {
|
|
|
6
6
|
export interface RunClaudePrintOptions {
|
|
7
7
|
cwd?: string;
|
|
8
8
|
timeoutMs: number;
|
|
9
|
+
/** Optional Claude model id / alias. Empty or "default" keeps Claude Code's own default. */
|
|
10
|
+
model?: string;
|
|
9
11
|
/**
|
|
10
12
|
* 用户偏好的回复语言(取自 config.language)。传进来时会以
|
|
11
13
|
* `appendSystemPrompt` 形式灌给 Claude,保证 quick-commit / prompt-optimizer
|
|
@@ -118,6 +118,7 @@ export async function runClaudePrint(prompt, options) {
|
|
|
118
118
|
const timeoutHandle = setTimeout(() => abortController.abort(), options.timeoutMs);
|
|
119
119
|
const sdkClaudeBinary = resolveSdkClaudeBinary();
|
|
120
120
|
const languageDirective = options.language ? buildLanguageDirective(options.language) : "";
|
|
121
|
+
const model = options.model?.trim();
|
|
121
122
|
const sdkOptions = {
|
|
122
123
|
abortController,
|
|
123
124
|
tools: [],
|
|
@@ -127,6 +128,7 @@ export async function runClaudePrint(prompt, options) {
|
|
|
127
128
|
...(cwd ? { cwd } : {}),
|
|
128
129
|
...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
|
|
129
130
|
...(languageDirective ? { appendSystemPrompt: languageDirective } : {}),
|
|
131
|
+
...(model && model !== "default" ? { model } : {}),
|
|
130
132
|
};
|
|
131
133
|
// 单条 user message → AsyncGenerator,SDK 的 streaming input 协议要求。
|
|
132
134
|
async function* singleShot() {
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { request as httpRequest } from "node:http";
|
|
5
|
+
import { request as httpsRequest } from "node:https";
|
|
6
|
+
import net from "node:net";
|
|
2
7
|
import process from "node:process";
|
|
3
8
|
import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
4
|
-
import { readLiveInstance, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
|
|
9
|
+
import { isPidAlive, readLiveInstance, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
|
|
5
10
|
import { getErrorMessage } from "./error-utils.js";
|
|
6
11
|
async function main() {
|
|
7
12
|
const args = process.argv.slice(2);
|
|
@@ -17,15 +22,26 @@ async function main() {
|
|
|
17
22
|
// web 命令下"已存在"的 ready 信息由统一的启动 banner / TUI 展示,避免重复。
|
|
18
23
|
const config = await ensureRequiredFiles(configPath, { silentReady: true });
|
|
19
24
|
// —— 单实例检测:如果已有 wand 主进程在跑,直接 attach 而不重启服务 ——
|
|
20
|
-
const live =
|
|
25
|
+
const live = await discoverAttachableInstance(configPath);
|
|
21
26
|
if (live) {
|
|
22
27
|
await runAttach(live, configPath, useTui);
|
|
23
28
|
break;
|
|
24
29
|
}
|
|
25
30
|
const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
|
|
26
31
|
ensureNodePtyHelperExecutable();
|
|
27
|
-
const { startServer } = await import("./server.js");
|
|
28
|
-
|
|
32
|
+
const { isPortInUseError, startServer } = await import("./server.js");
|
|
33
|
+
let handle;
|
|
34
|
+
try {
|
|
35
|
+
handle = await startServer(config, configPath);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (isPortInUseError(err)) {
|
|
39
|
+
if (await handlePortInUse(config, configPath, useTui)) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
29
45
|
// —— 注册实例:写 pidfile + 启动 IPC 服务端(TUI / banner 模式都需要) ——
|
|
30
46
|
const startedAtMs = Date.now();
|
|
31
47
|
const ipcCtx = await registerInstance(handle, configPath, startedAtMs);
|
|
@@ -336,6 +352,217 @@ async function runAttach(live, configPath, useTui) {
|
|
|
336
352
|
process.on("SIGINT", onSignal);
|
|
337
353
|
process.on("SIGTERM", onSignal);
|
|
338
354
|
}
|
|
355
|
+
async function handlePortInUse(config, configPath, useTui) {
|
|
356
|
+
const live = await discoverAttachableInstance(configPath);
|
|
357
|
+
if (live) {
|
|
358
|
+
await runAttach(live, configPath, useTui);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
const probe = await probeWandHttp(config);
|
|
362
|
+
const pid = findListeningPid(config.port);
|
|
363
|
+
if (probe?.isWand) {
|
|
364
|
+
printDetectedWandWithoutAttach(config, configPath, probe.url, pid);
|
|
365
|
+
process.exitCode = 0;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
printPortInUse(config, configPath, pid);
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
async function discoverAttachableInstance(configPath) {
|
|
373
|
+
const live = readLiveInstance(configPath);
|
|
374
|
+
if (live)
|
|
375
|
+
return live;
|
|
376
|
+
const sockPath = socketPath(configPath);
|
|
377
|
+
if (!sockPath || !existsSync(sockPath))
|
|
378
|
+
return null;
|
|
379
|
+
const snapshot = await readIpcSnapshot(sockPath);
|
|
380
|
+
if (!snapshot)
|
|
381
|
+
return null;
|
|
382
|
+
const pid = snapshot.header.pid;
|
|
383
|
+
if (!isPidAlive(pid))
|
|
384
|
+
return null;
|
|
385
|
+
return {
|
|
386
|
+
pid,
|
|
387
|
+
version: snapshot.header.version,
|
|
388
|
+
startedAt: snapshot.header.startedAtMs,
|
|
389
|
+
url: snapshot.header.url,
|
|
390
|
+
scheme: snapshot.header.scheme,
|
|
391
|
+
bindAddr: snapshot.header.bindAddr,
|
|
392
|
+
configPath: snapshot.header.configPath,
|
|
393
|
+
dbPath: snapshot.header.dbPath,
|
|
394
|
+
socket: sockPath,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function readIpcSnapshot(sockPath) {
|
|
398
|
+
return new Promise((resolve) => {
|
|
399
|
+
const sock = net.createConnection({ path: sockPath });
|
|
400
|
+
let buf = "";
|
|
401
|
+
let finished = false;
|
|
402
|
+
const finish = (value) => {
|
|
403
|
+
if (finished)
|
|
404
|
+
return;
|
|
405
|
+
finished = true;
|
|
406
|
+
clearTimeout(timer);
|
|
407
|
+
try {
|
|
408
|
+
sock.destroy();
|
|
409
|
+
}
|
|
410
|
+
catch { /* noop */ }
|
|
411
|
+
resolve(value);
|
|
412
|
+
};
|
|
413
|
+
const timer = setTimeout(() => finish(null), 800);
|
|
414
|
+
timer.unref?.();
|
|
415
|
+
sock.setEncoding("utf8");
|
|
416
|
+
sock.on("connect", () => {
|
|
417
|
+
sock.write(JSON.stringify({ id: "probe", cmd: "snapshot" }) + "\n");
|
|
418
|
+
});
|
|
419
|
+
sock.on("data", (chunk) => {
|
|
420
|
+
buf += chunk;
|
|
421
|
+
let idx;
|
|
422
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
423
|
+
const line = buf.slice(0, idx).trim();
|
|
424
|
+
buf = buf.slice(idx + 1);
|
|
425
|
+
if (!line)
|
|
426
|
+
continue;
|
|
427
|
+
let msg = null;
|
|
428
|
+
try {
|
|
429
|
+
msg = JSON.parse(line);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (msg?.id !== "probe" || !msg.ok)
|
|
435
|
+
continue;
|
|
436
|
+
const data = msg.data;
|
|
437
|
+
if (isIpcSnapshotData(data)) {
|
|
438
|
+
finish(data);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
sock.on("error", () => finish(null));
|
|
444
|
+
sock.on("close", () => finish(null));
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function isIpcSnapshotData(value) {
|
|
448
|
+
if (!value || typeof value !== "object")
|
|
449
|
+
return false;
|
|
450
|
+
const snap = value;
|
|
451
|
+
const header = snap.header;
|
|
452
|
+
return !!header
|
|
453
|
+
&& typeof header.pid === "number"
|
|
454
|
+
&& typeof header.version === "string"
|
|
455
|
+
&& typeof header.startedAtMs === "number"
|
|
456
|
+
&& typeof header.url === "string"
|
|
457
|
+
&& (header.scheme === "HTTP" || header.scheme === "HTTPS")
|
|
458
|
+
&& typeof header.bindAddr === "string"
|
|
459
|
+
&& typeof header.configPath === "string"
|
|
460
|
+
&& typeof header.dbPath === "string"
|
|
461
|
+
&& Array.isArray(snap.sessions);
|
|
462
|
+
}
|
|
463
|
+
async function probeWandHttp(config) {
|
|
464
|
+
const schemes = config.https ? ["https", "http"] : ["http", "https"];
|
|
465
|
+
for (const scheme of schemes) {
|
|
466
|
+
for (const host of probeHosts(config.host)) {
|
|
467
|
+
const baseUrl = `${scheme}://${host}:${config.port}`;
|
|
468
|
+
const sessionCheck = await requestText(`${baseUrl}/api/session-check`);
|
|
469
|
+
if (sessionCheck && looksLikeWandSessionCheck(sessionCheck.body)) {
|
|
470
|
+
return { isWand: true, url: baseUrl };
|
|
471
|
+
}
|
|
472
|
+
const home = await requestText(baseUrl);
|
|
473
|
+
if (home && home.body.includes("<title>Wand Console</title>")) {
|
|
474
|
+
return { isWand: true, url: baseUrl };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function probeHosts(configHost) {
|
|
481
|
+
const hosts = ["127.0.0.1", "localhost"];
|
|
482
|
+
if (configHost && configHost !== "0.0.0.0" && configHost !== "::" && !hosts.includes(configHost)) {
|
|
483
|
+
hosts.push(configHost);
|
|
484
|
+
}
|
|
485
|
+
return hosts;
|
|
486
|
+
}
|
|
487
|
+
function looksLikeWandSessionCheck(body) {
|
|
488
|
+
try {
|
|
489
|
+
const parsed = JSON.parse(body);
|
|
490
|
+
return typeof parsed.authed === "boolean";
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function requestText(urlText) {
|
|
497
|
+
return new Promise((resolve) => {
|
|
498
|
+
let url;
|
|
499
|
+
try {
|
|
500
|
+
url = new URL(urlText);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
resolve(null);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const request = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
507
|
+
const req = request(url, {
|
|
508
|
+
method: "GET",
|
|
509
|
+
timeout: 800,
|
|
510
|
+
rejectUnauthorized: false,
|
|
511
|
+
}, (res) => {
|
|
512
|
+
let body = "";
|
|
513
|
+
res.setEncoding("utf8");
|
|
514
|
+
res.on("data", (chunk) => {
|
|
515
|
+
if (body.length < 64 * 1024)
|
|
516
|
+
body += chunk;
|
|
517
|
+
});
|
|
518
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
519
|
+
});
|
|
520
|
+
req.on("timeout", () => {
|
|
521
|
+
req.destroy();
|
|
522
|
+
resolve(null);
|
|
523
|
+
});
|
|
524
|
+
req.on("error", () => resolve(null));
|
|
525
|
+
req.end();
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function findListeningPid(port) {
|
|
529
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
530
|
+
return null;
|
|
531
|
+
try {
|
|
532
|
+
const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8", timeout: 1000 });
|
|
533
|
+
if (result.status !== 0 && !result.stdout)
|
|
534
|
+
return null;
|
|
535
|
+
const pid = (result.stdout || "")
|
|
536
|
+
.split(/\s+/)
|
|
537
|
+
.map((part) => Number(part.trim()))
|
|
538
|
+
.find((value) => Number.isInteger(value) && value > 0 && value !== process.pid);
|
|
539
|
+
return pid ?? null;
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function printDetectedWandWithoutAttach(config, configPath, url, pid) {
|
|
546
|
+
const pidLine = pid ? ` PID ${pid}\n` : "";
|
|
547
|
+
process.stdout.write(`[wand] 检测到已有 Wand 服务正在运行:${url}\n` +
|
|
548
|
+
pidLine +
|
|
549
|
+
` Config ${configPath}\n` +
|
|
550
|
+
` Port ${config.port}\n` +
|
|
551
|
+
` 当前无法 attach:${socketPath(configPath) || "IPC socket"} 不可达。\n` +
|
|
552
|
+
` 请重启一次正在运行的服务;之后再执行 wand web 会恢复正常 attach。\n`);
|
|
553
|
+
}
|
|
554
|
+
function printPortInUse(config, configPath, pid) {
|
|
555
|
+
const pidHint = pid ? `\n 占用进程 PID: ${pid}` : "";
|
|
556
|
+
const killHint = pid
|
|
557
|
+
? `kill ${pid}`
|
|
558
|
+
: `kill $(lsof -ti :${config.port})`;
|
|
559
|
+
process.stderr.write(`\n✗ [wand] 端口 ${config.port} 已被占用,但没有发现可 attach 的 Wand 实例。${pidHint}\n` +
|
|
560
|
+
` Config: ${configPath}\n` +
|
|
561
|
+
` Socket: ${socketPath(configPath) || "当前平台不支持 Unix socket"}\n\n` +
|
|
562
|
+
`解决方法(二选一):\n` +
|
|
563
|
+
`1. 如果这是旧 Wand 服务,重启它后再运行 wand web\n` +
|
|
564
|
+
`2. 如果不是 Wand,占用进程可用以下命令终止:\n ${killHint}\n\n`);
|
|
565
|
+
}
|
|
339
566
|
function setConfigValue(config, key, value) {
|
|
340
567
|
// 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,password 也走 DB
|
|
341
568
|
// (由 case "config:set" 直接处理),这里只剩纯 JSON 字段。
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { GitStatusResult, PushResult, QuickCommitResult, TagHeadResult } from "./types.js";
|
|
1
|
+
import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
|
|
2
2
|
export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
|
|
3
3
|
export declare function getGitStatus(cwd: string): GitStatusResult;
|
|
4
4
|
interface QuickCommitOptions {
|
|
5
5
|
cwd: string;
|
|
6
6
|
language: string;
|
|
7
|
+
provider?: SessionProvider;
|
|
8
|
+
model?: string | null;
|
|
9
|
+
thinkingEffort?: SessionSnapshot["thinkingEffort"];
|
|
10
|
+
inheritEnv?: boolean;
|
|
7
11
|
autoMessage: boolean;
|
|
8
12
|
customMessage?: string;
|
|
9
13
|
tag?: string;
|
|
@@ -16,6 +20,12 @@ interface QuickCommitOptions {
|
|
|
16
20
|
*/
|
|
17
21
|
submodule?: boolean;
|
|
18
22
|
}
|
|
23
|
+
interface QuickCommitAiOptions {
|
|
24
|
+
provider?: SessionProvider;
|
|
25
|
+
model?: string | null;
|
|
26
|
+
thinkingEffort?: SessionSnapshot["thinkingEffort"];
|
|
27
|
+
inheritEnv?: boolean;
|
|
28
|
+
}
|
|
19
29
|
export declare class QuickCommitError extends Error {
|
|
20
30
|
readonly code: QuickCommitErrorCode;
|
|
21
31
|
constructor(message: string, code: QuickCommitErrorCode);
|
|
@@ -25,7 +35,7 @@ export interface GenerateCommitMessageResult {
|
|
|
25
35
|
/** AI-suggested next tag derived from the staged diff and the latest existing tag. */
|
|
26
36
|
suggestedTag?: string;
|
|
27
37
|
}
|
|
28
|
-
export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<GenerateCommitMessageResult>;
|
|
38
|
+
export declare function generateCommitMessageOnly(cwd: string, language: string, ai?: QuickCommitAiOptions): Promise<GenerateCommitMessageResult>;
|
|
29
39
|
interface TagHeadOptions {
|
|
30
40
|
cwd: string;
|
|
31
41
|
language: string;
|
|
@@ -46,5 +56,6 @@ interface PushOptions {
|
|
|
46
56
|
tagName?: string;
|
|
47
57
|
}
|
|
48
58
|
export declare function runPush(opts: PushOptions): Promise<PushResult>;
|
|
59
|
+
export declare function runQuickCommitWithFallback(opts: QuickCommitOptions): Promise<QuickCommitResult>;
|
|
49
60
|
export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
|
|
50
61
|
export {};
|