@co0ontty/wand 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- package/package.json +2 -1
package/dist/git-quick-commit.js
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
4
4
|
import { buildChildEnv } from "./env-utils.js";
|
|
5
|
-
import { runGit as runGitBase, runGitRaw as runGitRawBase, getGitErrorMessage } from "./git-utils.js";
|
|
6
|
-
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-
|
|
5
|
+
import { runGit as runGitBase, runGitAsync as runGitAsyncBase, runGitRaw as runGitRawBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
|
|
6
|
+
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
|
|
7
7
|
const GIT_TIMEOUT_MS = 1500;
|
|
8
8
|
const GIT_PUSH_TIMEOUT_MS = 30_000;
|
|
9
9
|
const MAX_FILE_ENTRIES = 200;
|
|
@@ -20,6 +20,12 @@ function runGit(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
20
20
|
function runGitAllowEmpty(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
21
21
|
return runGitRawBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
22
22
|
}
|
|
23
|
+
function runGitAsync(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
24
|
+
return runGitAsyncBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
25
|
+
}
|
|
26
|
+
function runGitAllowEmptyAsync(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
27
|
+
return runGitRawAsyncBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
28
|
+
}
|
|
23
29
|
/** Throws `QuickCommitError` if `cwd` isn't an existing path inside a git work tree. */
|
|
24
30
|
function assertGitWorkTree(cwd) {
|
|
25
31
|
if (!cwd || !existsSync(cwd)) {
|
|
@@ -36,6 +42,20 @@ function assertGitWorkTree(cwd) {
|
|
|
36
42
|
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
37
43
|
}
|
|
38
44
|
}
|
|
45
|
+
async function assertGitWorkTreeAsync(cwd) {
|
|
46
|
+
if (!cwd || !existsSync(cwd))
|
|
47
|
+
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
48
|
+
try {
|
|
49
|
+
if (await runGitAsync(["rev-parse", "--is-inside-work-tree"], cwd) !== "true") {
|
|
50
|
+
throw new QuickCommitError("当前目录不在 git 仓库内。", "NOT_A_GIT_REPO");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof QuickCommitError)
|
|
55
|
+
throw error;
|
|
56
|
+
throw new QuickCommitError(getGitErrorMessage(error), "NOT_A_GIT_REPO");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
39
59
|
/**
|
|
40
60
|
* Resolve the remote to push to. Prefers `branch.<name>.remote` config for the
|
|
41
61
|
* current branch, falls back to `origin`. Never throws.
|
|
@@ -52,6 +72,15 @@ function resolvePushRemote(cwd) {
|
|
|
52
72
|
}
|
|
53
73
|
return "origin";
|
|
54
74
|
}
|
|
75
|
+
async function resolvePushRemoteAsync(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const branch = await runGitAsync(["branch", "--show-current"], cwd);
|
|
78
|
+
if (branch)
|
|
79
|
+
return await runGitAsync(["config", "--get", `branch.${branch}.remote`], cwd) || "origin";
|
|
80
|
+
}
|
|
81
|
+
catch { /* use origin */ }
|
|
82
|
+
return "origin";
|
|
83
|
+
}
|
|
55
84
|
function unquotePath(raw) {
|
|
56
85
|
if (raw.startsWith("\"") && raw.endsWith("\"")) {
|
|
57
86
|
return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
@@ -253,6 +282,116 @@ export function getGitStatus(cwd) {
|
|
|
253
282
|
hasSubmodule: allEntries.some((e) => e.isSubmodule) || repoDeclaresSubmodule(repoRoot),
|
|
254
283
|
};
|
|
255
284
|
}
|
|
285
|
+
async function repoDeclaresSubmoduleAsync(repoRoot) {
|
|
286
|
+
if (!repoRoot)
|
|
287
|
+
return false;
|
|
288
|
+
const gitmodules = `${repoRoot}/.gitmodules`;
|
|
289
|
+
if (!existsSync(gitmodules))
|
|
290
|
+
return false;
|
|
291
|
+
try {
|
|
292
|
+
const out = (await runGitAllowEmptyAsync(["config", "-f", gitmodules, "--get-regexp", "\\.path$"], repoRoot)).trim();
|
|
293
|
+
return out.length > 0;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Non-blocking status implementation used by HTTP routes. */
|
|
300
|
+
export async function getGitStatusAsync(cwd) {
|
|
301
|
+
if (!cwd || !existsSync(cwd))
|
|
302
|
+
return { isGit: false, error: "工作目录不存在。" };
|
|
303
|
+
try {
|
|
304
|
+
if (await runGitAsync(["rev-parse", "--is-inside-work-tree"], cwd) !== "true")
|
|
305
|
+
return { isGit: false };
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return { isGit: false };
|
|
309
|
+
}
|
|
310
|
+
let repoRoot;
|
|
311
|
+
try {
|
|
312
|
+
repoRoot = await runGitAsync(["rev-parse", "--show-toplevel"], cwd);
|
|
313
|
+
}
|
|
314
|
+
catch { /* optional */ }
|
|
315
|
+
let branch;
|
|
316
|
+
try {
|
|
317
|
+
branch = await runGitAsync(["branch", "--show-current"], cwd);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
return { isGit: true, repoRoot, error: getGitErrorMessage(error) };
|
|
321
|
+
}
|
|
322
|
+
if (!branch) {
|
|
323
|
+
try {
|
|
324
|
+
branch = `HEAD (${await runGitAsync(["rev-parse", "--short", "HEAD"], cwd)})`;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
branch = "HEAD";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let head;
|
|
331
|
+
let initialCommit = false;
|
|
332
|
+
try {
|
|
333
|
+
head = await runGitAsync(["rev-parse", "HEAD"], cwd);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
initialCommit = true;
|
|
337
|
+
}
|
|
338
|
+
let porcelain;
|
|
339
|
+
try {
|
|
340
|
+
porcelain = await runGitAllowEmptyAsync(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return { isGit: true, branch, repoRoot, head, initialCommit, error: getGitErrorMessage(error) };
|
|
344
|
+
}
|
|
345
|
+
const allEntries = parsePorcelainV2(porcelain);
|
|
346
|
+
let upstream;
|
|
347
|
+
let ahead;
|
|
348
|
+
let behind;
|
|
349
|
+
let lastCommit;
|
|
350
|
+
let latestTag;
|
|
351
|
+
if (!initialCommit) {
|
|
352
|
+
try {
|
|
353
|
+
upstream = await runGitAsync(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd) || undefined;
|
|
354
|
+
}
|
|
355
|
+
catch { /* optional */ }
|
|
356
|
+
if (upstream) {
|
|
357
|
+
try {
|
|
358
|
+
const [behindRaw, aheadRaw] = (await runGitAsync(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], cwd)).split(/\s+/);
|
|
359
|
+
const parsedBehind = Number.parseInt(behindRaw, 10);
|
|
360
|
+
const parsedAhead = Number.parseInt(aheadRaw, 10);
|
|
361
|
+
if (!Number.isNaN(parsedBehind))
|
|
362
|
+
behind = parsedBehind;
|
|
363
|
+
if (!Number.isNaN(parsedAhead))
|
|
364
|
+
ahead = parsedAhead;
|
|
365
|
+
}
|
|
366
|
+
catch { /* optional */ }
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const parts = (await runGitAsync(["log", "-1", "--pretty=format:%H%x09%h%x09%s"], cwd)).split("\t");
|
|
370
|
+
if (parts.length >= 3)
|
|
371
|
+
lastCommit = { hash: parts[0], shortHash: parts[1], subject: parts.slice(2).join("\t") };
|
|
372
|
+
}
|
|
373
|
+
catch { /* optional */ }
|
|
374
|
+
try {
|
|
375
|
+
latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
376
|
+
}
|
|
377
|
+
catch { /* optional */ }
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
isGit: true,
|
|
381
|
+
branch,
|
|
382
|
+
modifiedCount: allEntries.length,
|
|
383
|
+
files: allEntries.slice(0, MAX_FILE_ENTRIES),
|
|
384
|
+
head,
|
|
385
|
+
repoRoot,
|
|
386
|
+
initialCommit,
|
|
387
|
+
upstream,
|
|
388
|
+
ahead,
|
|
389
|
+
behind,
|
|
390
|
+
lastCommit,
|
|
391
|
+
latestTag,
|
|
392
|
+
hasSubmodule: allEntries.some((entry) => entry.isSubmodule) || await repoDeclaresSubmoduleAsync(repoRoot),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
256
395
|
export class QuickCommitError extends Error {
|
|
257
396
|
code;
|
|
258
397
|
constructor(message, code) {
|
|
@@ -419,17 +558,17 @@ async function callAiText(prompt, cwd, language, opts) {
|
|
|
419
558
|
return callOpenCodeText(prompt, cwd, opts);
|
|
420
559
|
return callClaudeText(prompt, cwd, language, opts.model);
|
|
421
560
|
}
|
|
422
|
-
function collectStagedDiff(cwd) {
|
|
561
|
+
async function collectStagedDiff(cwd) {
|
|
423
562
|
let diff;
|
|
424
563
|
try {
|
|
425
|
-
diff =
|
|
564
|
+
diff = await runGitAsync(["diff", "--cached", "--submodule=log"], cwd, 5000);
|
|
426
565
|
}
|
|
427
566
|
catch {
|
|
428
567
|
diff = "";
|
|
429
568
|
}
|
|
430
569
|
if (!diff) {
|
|
431
570
|
try {
|
|
432
|
-
diff =
|
|
571
|
+
diff = await runGitAsync(["diff", "--cached", "--name-only"], cwd, 3000);
|
|
433
572
|
}
|
|
434
573
|
catch {
|
|
435
574
|
diff = "(no diff available)";
|
|
@@ -441,7 +580,7 @@ function collectStagedDiff(cwd) {
|
|
|
441
580
|
return diff;
|
|
442
581
|
}
|
|
443
582
|
async function generateCommitMessage(cwd, language, ai = {}) {
|
|
444
|
-
const diff = collectStagedDiff(cwd);
|
|
583
|
+
const diff = await collectStagedDiff(cwd);
|
|
445
584
|
const lang = language.trim() || "中文";
|
|
446
585
|
const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
|
|
447
586
|
const raw = await callAiText(prompt, cwd, language, ai);
|
|
@@ -479,10 +618,10 @@ function sanitizeSuggestedTag(value) {
|
|
|
479
618
|
return cleaned;
|
|
480
619
|
}
|
|
481
620
|
async function generateCommitMessageWithTag(cwd, language, ai = {}) {
|
|
482
|
-
const diff = collectStagedDiff(cwd);
|
|
621
|
+
const diff = await collectStagedDiff(cwd);
|
|
483
622
|
let latestTag;
|
|
484
623
|
try {
|
|
485
|
-
latestTag =
|
|
624
|
+
latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
486
625
|
}
|
|
487
626
|
catch {
|
|
488
627
|
latestTag = undefined;
|
|
@@ -523,7 +662,7 @@ export async function generateCommitMessageOnly(cwd, language, ai = {}) {
|
|
|
523
662
|
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
524
663
|
}
|
|
525
664
|
try {
|
|
526
|
-
|
|
665
|
+
await runGitAsync(["add", "-A"], cwd, 5000);
|
|
527
666
|
}
|
|
528
667
|
catch {
|
|
529
668
|
// best-effort staging so the diff is complete
|
|
@@ -537,14 +676,14 @@ export async function generateCommitMessageOnly(cwd, language, ai = {}) {
|
|
|
537
676
|
async function generateTagAfterCommit(cwd, language, commitMessage, ai = {}) {
|
|
538
677
|
let diff;
|
|
539
678
|
try {
|
|
540
|
-
diff =
|
|
679
|
+
diff = await runGitAsync(["show", "HEAD", "--no-color", "--submodule=log"], cwd, 5000);
|
|
541
680
|
}
|
|
542
681
|
catch {
|
|
543
682
|
diff = "";
|
|
544
683
|
}
|
|
545
684
|
if (!diff) {
|
|
546
685
|
try {
|
|
547
|
-
diff =
|
|
686
|
+
diff = await runGitAsync(["show", "HEAD", "--name-only"], cwd, 3000);
|
|
548
687
|
}
|
|
549
688
|
catch {
|
|
550
689
|
diff = "(no diff available)";
|
|
@@ -556,7 +695,7 @@ async function generateTagAfterCommit(cwd, language, commitMessage, ai = {}) {
|
|
|
556
695
|
let latestTag;
|
|
557
696
|
try {
|
|
558
697
|
// We just made a commit, so look for the most recent tag reachable from HEAD~1.
|
|
559
|
-
latestTag =
|
|
698
|
+
latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd) || undefined;
|
|
560
699
|
}
|
|
561
700
|
catch {
|
|
562
701
|
latestTag = undefined;
|
|
@@ -592,38 +731,38 @@ ${diff}`;
|
|
|
592
731
|
* Push current branch (with upstream auto-setup) and/or tags.
|
|
593
732
|
* Errors are returned via `error` so callers can present partial-success states.
|
|
594
733
|
*/
|
|
595
|
-
function doPush(opts) {
|
|
734
|
+
async function doPush(opts) {
|
|
596
735
|
const { cwd, pushCommits, pushTags } = opts;
|
|
597
736
|
const recurseFlag = `--recurse-submodules=${opts.recurseSubmodules ?? "check"}`;
|
|
598
737
|
let pushedCommits = false;
|
|
599
738
|
let pushedTags = false;
|
|
600
739
|
let hasUpstream = false;
|
|
601
740
|
try {
|
|
602
|
-
|
|
741
|
+
await runGitAsync(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
|
|
603
742
|
hasUpstream = true;
|
|
604
743
|
}
|
|
605
744
|
catch {
|
|
606
745
|
hasUpstream = false;
|
|
607
746
|
}
|
|
608
|
-
const pushRemote =
|
|
747
|
+
const pushRemote = await resolvePushRemoteAsync(cwd);
|
|
609
748
|
try {
|
|
610
749
|
if (pushCommits) {
|
|
611
750
|
if (hasUpstream) {
|
|
612
|
-
|
|
751
|
+
await runGitAsync(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
613
752
|
}
|
|
614
753
|
else {
|
|
615
|
-
|
|
754
|
+
await runGitAsync(["push", "-u", recurseFlag, pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
616
755
|
}
|
|
617
756
|
pushedCommits = true;
|
|
618
757
|
}
|
|
619
758
|
if (pushTags) {
|
|
620
759
|
if (Array.isArray(pushTags)) {
|
|
621
760
|
for (const name of pushTags) {
|
|
622
|
-
|
|
761
|
+
await runGitAsync(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
623
762
|
}
|
|
624
763
|
}
|
|
625
764
|
else {
|
|
626
|
-
|
|
765
|
+
await runGitAsync(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
|
|
627
766
|
}
|
|
628
767
|
pushedTags = true;
|
|
629
768
|
}
|
|
@@ -635,10 +774,10 @@ function doPush(opts) {
|
|
|
635
774
|
}
|
|
636
775
|
export async function runTagHead(opts) {
|
|
637
776
|
const { cwd, language, tag, autoTag, push } = opts;
|
|
638
|
-
|
|
777
|
+
await assertGitWorkTreeAsync(cwd);
|
|
639
778
|
let headHash;
|
|
640
779
|
try {
|
|
641
|
-
headHash =
|
|
780
|
+
headHash = await runGitAsync(["rev-parse", "HEAD"], cwd);
|
|
642
781
|
}
|
|
643
782
|
catch {
|
|
644
783
|
throw new QuickCommitError("仓库还没有任何 commit,无法打 tag。", "NO_COMMIT");
|
|
@@ -647,7 +786,7 @@ export async function runTagHead(opts) {
|
|
|
647
786
|
if (!tagName && autoTag) {
|
|
648
787
|
let headSubject = "";
|
|
649
788
|
try {
|
|
650
|
-
headSubject =
|
|
789
|
+
headSubject = await runGitAsync(["log", "-1", "--pretty=format:%s"], cwd);
|
|
651
790
|
}
|
|
652
791
|
catch {
|
|
653
792
|
headSubject = "";
|
|
@@ -664,7 +803,7 @@ export async function runTagHead(opts) {
|
|
|
664
803
|
}
|
|
665
804
|
// Refuse to overwrite an existing tag — surface a clear error code.
|
|
666
805
|
try {
|
|
667
|
-
|
|
806
|
+
await runGitAsync(["rev-parse", "--verify", `refs/tags/${tagName}`], cwd);
|
|
668
807
|
throw new QuickCommitError(`tag \`${tagName}\` 已存在。`, "TAG_EXISTS");
|
|
669
808
|
}
|
|
670
809
|
catch (error) {
|
|
@@ -673,7 +812,7 @@ export async function runTagHead(opts) {
|
|
|
673
812
|
// not found — good, proceed
|
|
674
813
|
}
|
|
675
814
|
try {
|
|
676
|
-
|
|
815
|
+
await runGitAsync(["tag", tagName], cwd);
|
|
677
816
|
}
|
|
678
817
|
catch (error) {
|
|
679
818
|
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
@@ -681,7 +820,7 @@ export async function runTagHead(opts) {
|
|
|
681
820
|
let pushed = false;
|
|
682
821
|
let pushError;
|
|
683
822
|
if (push) {
|
|
684
|
-
const outcome = doPush({ cwd, pushCommits: false, pushTags: [tagName] });
|
|
823
|
+
const outcome = await doPush({ cwd, pushCommits: false, pushTags: [tagName] });
|
|
685
824
|
pushed = outcome.pushedTags;
|
|
686
825
|
pushError = outcome.error;
|
|
687
826
|
}
|
|
@@ -694,7 +833,7 @@ export async function runTagHead(opts) {
|
|
|
694
833
|
}
|
|
695
834
|
export async function runPush(opts) {
|
|
696
835
|
const { cwd, pushCommits = true, pushTags = false, submodule = false, tagName } = opts;
|
|
697
|
-
|
|
836
|
+
await assertGitWorkTreeAsync(cwd);
|
|
698
837
|
if (!pushCommits && !pushTags) {
|
|
699
838
|
throw new QuickCommitError("没有要推送的内容。", "NOTHING_TO_PUSH");
|
|
700
839
|
}
|
|
@@ -703,9 +842,9 @@ export async function runPush(opts) {
|
|
|
703
842
|
// status 看不到,但本地可能仍领先远端。父仓库随后用 recurse=no 推送。
|
|
704
843
|
let subPushErrors = [];
|
|
705
844
|
if (submodule) {
|
|
706
|
-
const { base, infos } = collectSubmodulesForPush(cwd);
|
|
845
|
+
const { base, infos } = await collectSubmodulesForPush(cwd);
|
|
707
846
|
if (infos.length > 0) {
|
|
708
|
-
const subPush = pushSubmodules(base, infos, { pushTags: !!(pushTags && tagName), tagName });
|
|
847
|
+
const subPush = await pushSubmodules(base, infos, { pushTags: !!(pushTags && tagName), tagName });
|
|
709
848
|
subPushErrors = subPush.errors;
|
|
710
849
|
}
|
|
711
850
|
}
|
|
@@ -720,7 +859,7 @@ export async function runPush(opts) {
|
|
|
720
859
|
error: subPushErrors.join(";"),
|
|
721
860
|
};
|
|
722
861
|
}
|
|
723
|
-
const outcome = doPush({ cwd, pushCommits, pushTags, recurseSubmodules: submodule ? "no" : "check" });
|
|
862
|
+
const outcome = await doPush({ cwd, pushCommits, pushTags, recurseSubmodules: submodule ? "no" : "check" });
|
|
724
863
|
return {
|
|
725
864
|
ok: !outcome.error && subPushErrors.length === 0,
|
|
726
865
|
pushedCommits: outcome.pushedCommits,
|
|
@@ -734,10 +873,10 @@ export async function runPush(opts) {
|
|
|
734
873
|
* `--recurse-submodules=on-demand` 会以「HEAD does not match the named branch」整体失败。
|
|
735
874
|
* 优先取远端默认分支(`<remote>/HEAD` 指向)→ 当前命名分支 → 回退 main/master。
|
|
736
875
|
*/
|
|
737
|
-
function resolveSubmodulePushBranch(subCwd, remote) {
|
|
876
|
+
async function resolveSubmodulePushBranch(subCwd, remote) {
|
|
738
877
|
const prefix = `${remote}/`;
|
|
739
878
|
try {
|
|
740
|
-
const ref =
|
|
879
|
+
const ref = await runGitAsync(["symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`], subCwd);
|
|
741
880
|
const name = (ref.startsWith(prefix) ? ref.slice(prefix.length) : ref).trim();
|
|
742
881
|
if (name)
|
|
743
882
|
return name;
|
|
@@ -746,7 +885,7 @@ function resolveSubmodulePushBranch(subCwd, remote) {
|
|
|
746
885
|
// ignore — fall through
|
|
747
886
|
}
|
|
748
887
|
try {
|
|
749
|
-
const cur =
|
|
888
|
+
const cur = await runGitAsync(["branch", "--show-current"], subCwd);
|
|
750
889
|
if (cur)
|
|
751
890
|
return cur;
|
|
752
891
|
}
|
|
@@ -754,7 +893,7 @@ function resolveSubmodulePushBranch(subCwd, remote) {
|
|
|
754
893
|
// ignore
|
|
755
894
|
}
|
|
756
895
|
try {
|
|
757
|
-
|
|
896
|
+
await runGitAsync(["rev-parse", "--verify", `refs/remotes/${remote}/main`], subCwd);
|
|
758
897
|
return "main";
|
|
759
898
|
}
|
|
760
899
|
catch {
|
|
@@ -770,12 +909,12 @@ function resolveSubmodulePushBranch(subCwd, remote) {
|
|
|
770
909
|
* 任一 submodule 提交失败都会被收集为非致命错误,不阻塞父仓库继续 commit;
|
|
771
910
|
* 调用方可以在结果里读到具体哪几个 submodule 失败。
|
|
772
911
|
*/
|
|
773
|
-
function commitDirtySubmodules(parentCwd, message) {
|
|
912
|
+
async function commitDirtySubmodules(parentCwd, message) {
|
|
774
913
|
const commits = [];
|
|
775
914
|
const errors = [];
|
|
776
915
|
let porcelain;
|
|
777
916
|
try {
|
|
778
|
-
porcelain =
|
|
917
|
+
porcelain = await runGitAllowEmptyAsync(["status", "--porcelain=v2", "--untracked-files=all"], parentCwd);
|
|
779
918
|
}
|
|
780
919
|
catch {
|
|
781
920
|
return { commits, errors };
|
|
@@ -796,7 +935,7 @@ function commitDirtySubmodules(parentCwd, message) {
|
|
|
796
935
|
continue;
|
|
797
936
|
}
|
|
798
937
|
try {
|
|
799
|
-
|
|
938
|
+
await runGitAsync(["add", "-A"], subCwd, 5000);
|
|
800
939
|
}
|
|
801
940
|
catch (error) {
|
|
802
941
|
errors.push(`submodule ${entry.path} add 失败:${getGitErrorMessage(error)}`);
|
|
@@ -806,7 +945,7 @@ function commitDirtySubmodules(parentCwd, message) {
|
|
|
806
945
|
// 文件都过滤掉了,会得到一个空 diff,此时跳过避免空 commit。
|
|
807
946
|
let staged;
|
|
808
947
|
try {
|
|
809
|
-
staged =
|
|
948
|
+
staged = (await runGitAllowEmptyAsync(["diff", "--cached", "--name-only"], subCwd)).trim();
|
|
810
949
|
}
|
|
811
950
|
catch {
|
|
812
951
|
staged = "";
|
|
@@ -814,7 +953,7 @@ function commitDirtySubmodules(parentCwd, message) {
|
|
|
814
953
|
if (!staged)
|
|
815
954
|
continue;
|
|
816
955
|
try {
|
|
817
|
-
|
|
956
|
+
await runGitAsync(["commit", "-m", message], subCwd, 10_000);
|
|
818
957
|
}
|
|
819
958
|
catch (error) {
|
|
820
959
|
errors.push(`submodule ${entry.path} commit 失败:${getGitErrorMessage(error)}`);
|
|
@@ -822,23 +961,23 @@ function commitDirtySubmodules(parentCwd, message) {
|
|
|
822
961
|
}
|
|
823
962
|
let hash = "";
|
|
824
963
|
try {
|
|
825
|
-
hash =
|
|
964
|
+
hash = await runGitAsync(["rev-parse", "--short", "HEAD"], subCwd);
|
|
826
965
|
}
|
|
827
966
|
catch { /* ignore */ }
|
|
828
|
-
const remote =
|
|
829
|
-
const branch = resolveSubmodulePushBranch(subCwd, remote);
|
|
967
|
+
const remote = await resolvePushRemoteAsync(subCwd);
|
|
968
|
+
const branch = await resolveSubmodulePushBranch(subCwd, remote);
|
|
830
969
|
commits.push({ path: entry.path, hash, remote, branch });
|
|
831
970
|
}
|
|
832
971
|
return { commits, errors };
|
|
833
972
|
}
|
|
834
973
|
/** 对刚提交的 submodule 打上与父仓库同名的 tag。已存在同名 tag 则记为非致命跳过。 */
|
|
835
|
-
function tagSubmodules(parentCwd, subInfos, tagName) {
|
|
974
|
+
async function tagSubmodules(parentCwd, subInfos, tagName) {
|
|
836
975
|
const tagged = [];
|
|
837
976
|
const errors = [];
|
|
838
977
|
for (const info of subInfos) {
|
|
839
978
|
const subCwd = `${parentCwd}/${info.path}`;
|
|
840
979
|
try {
|
|
841
|
-
|
|
980
|
+
await runGitAsync(["rev-parse", "--verify", `refs/tags/${tagName}`], subCwd);
|
|
842
981
|
errors.push(`submodule ${info.path} 已存在 tag ${tagName},跳过`);
|
|
843
982
|
continue;
|
|
844
983
|
}
|
|
@@ -846,7 +985,7 @@ function tagSubmodules(parentCwd, subInfos, tagName) {
|
|
|
846
985
|
// 不存在 → 继续打 tag
|
|
847
986
|
}
|
|
848
987
|
try {
|
|
849
|
-
|
|
988
|
+
await runGitAsync(["tag", tagName], subCwd);
|
|
850
989
|
tagged.push(info.path);
|
|
851
990
|
}
|
|
852
991
|
catch (error) {
|
|
@@ -860,7 +999,7 @@ function tagSubmodules(parentCwd, subInfos, tagName) {
|
|
|
860
999
|
* 解决 detached HEAD 无法直接 push 的问题;`pushTags` 时连带把同名 tag 推上去。
|
|
861
1000
|
* 单个 submodule 失败收集为非致命错误。
|
|
862
1001
|
*/
|
|
863
|
-
function pushSubmodules(parentCwd, subInfos, opts) {
|
|
1002
|
+
async function pushSubmodules(parentCwd, subInfos, opts) {
|
|
864
1003
|
const pushed = [];
|
|
865
1004
|
const errors = [];
|
|
866
1005
|
for (const info of subInfos) {
|
|
@@ -870,7 +1009,7 @@ function pushSubmodules(parentCwd, subInfos, opts) {
|
|
|
870
1009
|
continue;
|
|
871
1010
|
}
|
|
872
1011
|
try {
|
|
873
|
-
|
|
1012
|
+
await runGitAsync(["push", info.remote, `HEAD:refs/heads/${info.branch}`], subCwd, GIT_PUSH_TIMEOUT_MS);
|
|
874
1013
|
}
|
|
875
1014
|
catch (error) {
|
|
876
1015
|
errors.push(`submodule ${info.path} 推送失败:${getGitErrorMessage(error)}`);
|
|
@@ -878,7 +1017,7 @@ function pushSubmodules(parentCwd, subInfos, opts) {
|
|
|
878
1017
|
}
|
|
879
1018
|
if (opts.pushTags && opts.tagName) {
|
|
880
1019
|
try {
|
|
881
|
-
|
|
1020
|
+
await runGitAsync(["push", info.remote, `refs/tags/${opts.tagName}`], subCwd, GIT_PUSH_TIMEOUT_MS);
|
|
882
1021
|
}
|
|
883
1022
|
catch (error) {
|
|
884
1023
|
errors.push(`submodule ${info.path} 推送 tag 失败:${getGitErrorMessage(error)}`);
|
|
@@ -893,17 +1032,17 @@ function pushSubmodules(parentCwd, subInfos, opts) {
|
|
|
893
1032
|
* remote + 目标分支。用于结果面板的 Push & Close:此时 submodule 多已 clean,
|
|
894
1033
|
* status 里看不到,但本地 HEAD 可能仍领先远端,需要逐个尝试推送(up-to-date 则 no-op)。
|
|
895
1034
|
*/
|
|
896
|
-
function collectSubmodulesForPush(cwd) {
|
|
1035
|
+
async function collectSubmodulesForPush(cwd) {
|
|
897
1036
|
let base;
|
|
898
1037
|
try {
|
|
899
|
-
base =
|
|
1038
|
+
base = await runGitAsync(["rev-parse", "--show-toplevel"], cwd);
|
|
900
1039
|
}
|
|
901
1040
|
catch {
|
|
902
1041
|
base = cwd;
|
|
903
1042
|
}
|
|
904
1043
|
let raw;
|
|
905
1044
|
try {
|
|
906
|
-
raw =
|
|
1045
|
+
raw = await runGitAllowEmptyAsync(["config", "-f", `${base}/.gitmodules`, "--get-regexp", "\\.path$"], base);
|
|
907
1046
|
}
|
|
908
1047
|
catch {
|
|
909
1048
|
return { base, infos: [] };
|
|
@@ -920,8 +1059,8 @@ function collectSubmodulesForPush(cwd) {
|
|
|
920
1059
|
const subCwd = `${base}/${rel}`;
|
|
921
1060
|
if (!existsSync(subCwd))
|
|
922
1061
|
continue;
|
|
923
|
-
const remote =
|
|
924
|
-
const branch = resolveSubmodulePushBranch(subCwd, remote);
|
|
1062
|
+
const remote = await resolvePushRemoteAsync(subCwd);
|
|
1063
|
+
const branch = await resolveSubmodulePushBranch(subCwd, remote);
|
|
925
1064
|
infos.push({ path: rel, hash: "", remote, branch });
|
|
926
1065
|
}
|
|
927
1066
|
return { base, infos };
|
|
@@ -961,17 +1100,17 @@ function buildFallbackPrompt(opts, priorError) {
|
|
|
961
1100
|
"完成后只输出一行 JSON:{\"ok\":true,\"message\":\"...\",\"tag\":\"...\"}。失败时输出一行 JSON:{\"ok\":false,\"error\":\"...\"}。",
|
|
962
1101
|
].join("\n");
|
|
963
1102
|
}
|
|
964
|
-
function getHead(cwd) {
|
|
1103
|
+
async function getHead(cwd) {
|
|
965
1104
|
try {
|
|
966
|
-
return
|
|
1105
|
+
return await runGitAsync(["rev-parse", "HEAD"], cwd);
|
|
967
1106
|
}
|
|
968
1107
|
catch {
|
|
969
1108
|
return null;
|
|
970
1109
|
}
|
|
971
1110
|
}
|
|
972
|
-
function getHeadSummary(cwd) {
|
|
1111
|
+
async function getHeadSummary(cwd) {
|
|
973
1112
|
try {
|
|
974
|
-
const raw =
|
|
1113
|
+
const raw = await runGitAsync(["log", "-1", "--pretty=format:%h%x09%s"], cwd);
|
|
975
1114
|
const parts = raw.split("\t");
|
|
976
1115
|
return { hash: parts[0] ?? "", message: parts.slice(1).join("\t") };
|
|
977
1116
|
}
|
|
@@ -979,17 +1118,17 @@ function getHeadSummary(cwd) {
|
|
|
979
1118
|
return { hash: "", message: "" };
|
|
980
1119
|
}
|
|
981
1120
|
}
|
|
982
|
-
function getLatestTagAtHead(cwd) {
|
|
1121
|
+
async function getLatestTagAtHead(cwd) {
|
|
983
1122
|
try {
|
|
984
|
-
return
|
|
1123
|
+
return await runGitAsync(["describe", "--tags", "--exact-match", "HEAD"], cwd) || undefined;
|
|
985
1124
|
}
|
|
986
1125
|
catch {
|
|
987
1126
|
return undefined;
|
|
988
1127
|
}
|
|
989
1128
|
}
|
|
990
1129
|
async function runQuickCommitFallbackCli(opts, priorError) {
|
|
991
|
-
|
|
992
|
-
const beforeHead = getHead(opts.cwd);
|
|
1130
|
+
await assertGitWorkTreeAsync(opts.cwd);
|
|
1131
|
+
const beforeHead = await getHead(opts.cwd);
|
|
993
1132
|
const prompt = buildFallbackPrompt(opts, priorError);
|
|
994
1133
|
const provider = normalizeProvider(opts.provider);
|
|
995
1134
|
if (provider === "codex") {
|
|
@@ -1036,12 +1175,12 @@ async function runQuickCommitFallbackCli(opts, priorError) {
|
|
|
1036
1175
|
inheritEnv: opts.inheritEnv,
|
|
1037
1176
|
});
|
|
1038
1177
|
}
|
|
1039
|
-
const afterHead = getHead(opts.cwd);
|
|
1178
|
+
const afterHead = await getHead(opts.cwd);
|
|
1040
1179
|
if (!afterHead || afterHead === beforeHead) {
|
|
1041
1180
|
throw new QuickCommitError("CLI 兜底没有创建新的 commit。", "GIT_COMMIT_FAILED");
|
|
1042
1181
|
}
|
|
1043
|
-
const commit = getHeadSummary(opts.cwd);
|
|
1044
|
-
const tag = getLatestTagAtHead(opts.cwd);
|
|
1182
|
+
const commit = await getHeadSummary(opts.cwd);
|
|
1183
|
+
const tag = await getLatestTagAtHead(opts.cwd);
|
|
1045
1184
|
return {
|
|
1046
1185
|
ok: true,
|
|
1047
1186
|
commit,
|
|
@@ -1081,18 +1220,18 @@ export async function runQuickCommit(opts) {
|
|
|
1081
1220
|
thinkingEffort: opts.thinkingEffort,
|
|
1082
1221
|
inheritEnv: opts.inheritEnv,
|
|
1083
1222
|
};
|
|
1084
|
-
|
|
1223
|
+
await assertGitWorkTreeAsync(cwd);
|
|
1085
1224
|
// 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
|
|
1086
1225
|
// AI 生成 message 时也基于这个 staged diff。
|
|
1087
1226
|
try {
|
|
1088
|
-
|
|
1227
|
+
await runGitAsync(["add", "-A"], cwd, 5000);
|
|
1089
1228
|
}
|
|
1090
1229
|
catch (error) {
|
|
1091
1230
|
throw new QuickCommitError(`git add 失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
|
|
1092
1231
|
}
|
|
1093
1232
|
let stagedFiles;
|
|
1094
1233
|
try {
|
|
1095
|
-
stagedFiles =
|
|
1234
|
+
stagedFiles = (await runGitAllowEmptyAsync(["diff", "--cached", "--name-only"], cwd)).trim();
|
|
1096
1235
|
}
|
|
1097
1236
|
catch (error) {
|
|
1098
1237
|
throw new QuickCommitError(getGitErrorMessage(error), "GIT_DIFF_FAILED");
|
|
@@ -1102,7 +1241,7 @@ export async function runQuickCommit(opts) {
|
|
|
1102
1241
|
let parentHasStaged = stagedFiles.length > 0;
|
|
1103
1242
|
let submoduleHasDirty = false;
|
|
1104
1243
|
try {
|
|
1105
|
-
const porcelain =
|
|
1244
|
+
const porcelain = await runGitAllowEmptyAsync(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
|
|
1106
1245
|
submoduleHasDirty = parsePorcelainV2(porcelain).some((e) => e.isSubmodule && (e.submoduleState?.hasTrackedChanges || e.submoduleState?.hasUntracked));
|
|
1107
1246
|
}
|
|
1108
1247
|
catch { /* keep submoduleHasDirty=false */ }
|
|
@@ -1127,17 +1266,17 @@ export async function runQuickCommit(opts) {
|
|
|
1127
1266
|
// 父仓库随后再 add 一次,picks up 新的 submodule 指针。
|
|
1128
1267
|
let submoduleOutcome = { commits: [], errors: [] };
|
|
1129
1268
|
if (submodule) {
|
|
1130
|
-
submoduleOutcome = commitDirtySubmodules(cwd, message);
|
|
1269
|
+
submoduleOutcome = await commitDirtySubmodules(cwd, message);
|
|
1131
1270
|
if (submoduleOutcome.commits.length > 0) {
|
|
1132
1271
|
try {
|
|
1133
|
-
|
|
1272
|
+
await runGitAsync(["add", "-A"], cwd, 5000);
|
|
1134
1273
|
}
|
|
1135
1274
|
catch (error) {
|
|
1136
1275
|
throw new QuickCommitError(`父仓库 add submodule 指针失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
|
|
1137
1276
|
}
|
|
1138
1277
|
// 重新评估父仓库是否有 staged 内容:submodule 指针变了这里应为真。
|
|
1139
1278
|
try {
|
|
1140
|
-
stagedFiles =
|
|
1279
|
+
stagedFiles = (await runGitAllowEmptyAsync(["diff", "--cached", "--name-only"], cwd)).trim();
|
|
1141
1280
|
parentHasStaged = stagedFiles.length > 0;
|
|
1142
1281
|
}
|
|
1143
1282
|
catch { /* keep stale value */ }
|
|
@@ -1153,14 +1292,14 @@ export async function runQuickCommit(opts) {
|
|
|
1153
1292
|
: "没有任何改动可以提交。", "NOTHING_TO_COMMIT");
|
|
1154
1293
|
}
|
|
1155
1294
|
try {
|
|
1156
|
-
|
|
1295
|
+
await runGitAsync(["commit", "-m", message], cwd, 10_000);
|
|
1157
1296
|
}
|
|
1158
1297
|
catch (error) {
|
|
1159
1298
|
throw new QuickCommitError(`git commit 失败:${getGitErrorMessage(error)}`, "GIT_COMMIT_FAILED");
|
|
1160
1299
|
}
|
|
1161
1300
|
let commitHash;
|
|
1162
1301
|
try {
|
|
1163
|
-
commitHash =
|
|
1302
|
+
commitHash = await runGitAsync(["rev-parse", "--short", "HEAD"], cwd);
|
|
1164
1303
|
}
|
|
1165
1304
|
catch {
|
|
1166
1305
|
commitHash = "";
|
|
@@ -1172,14 +1311,14 @@ export async function runQuickCommit(opts) {
|
|
|
1172
1311
|
}
|
|
1173
1312
|
if (tagName) {
|
|
1174
1313
|
try {
|
|
1175
|
-
|
|
1314
|
+
await runGitAsync(["tag", tagName], cwd);
|
|
1176
1315
|
}
|
|
1177
1316
|
catch (error) {
|
|
1178
1317
|
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
1179
1318
|
}
|
|
1180
1319
|
// 纳入 submodule 时给刚提交的 submodule 打同名 tag(非致命,失败不阻断父仓库流程)。
|
|
1181
1320
|
if (submodule && submoduleOutcome.commits.length > 0) {
|
|
1182
|
-
tagSubmodules(cwd, submoduleOutcome.commits, tagName);
|
|
1321
|
+
await tagSubmodules(cwd, submoduleOutcome.commits, tagName);
|
|
1183
1322
|
}
|
|
1184
1323
|
}
|
|
1185
1324
|
let pushed = false;
|
|
@@ -1191,7 +1330,7 @@ export async function runQuickCommit(opts) {
|
|
|
1191
1330
|
const includeSub = !!submodule && submoduleOutcome.commits.length > 0;
|
|
1192
1331
|
let subPushErrors = [];
|
|
1193
1332
|
if (includeSub) {
|
|
1194
|
-
const subPush = pushSubmodules(cwd, submoduleOutcome.commits, { pushTags: !!tagName, tagName });
|
|
1333
|
+
const subPush = await pushSubmodules(cwd, submoduleOutcome.commits, { pushTags: !!tagName, tagName });
|
|
1195
1334
|
subPushErrors = subPush.errors;
|
|
1196
1335
|
}
|
|
1197
1336
|
if (subPushErrors.length > 0) {
|
|
@@ -1200,7 +1339,7 @@ export async function runQuickCommit(opts) {
|
|
|
1200
1339
|
pushError = subPushErrors.join(";");
|
|
1201
1340
|
}
|
|
1202
1341
|
else {
|
|
1203
|
-
const outcome = doPush({
|
|
1342
|
+
const outcome = await doPush({
|
|
1204
1343
|
cwd,
|
|
1205
1344
|
pushCommits: true,
|
|
1206
1345
|
// Push only the freshly-created tag — avoids surprising users by pushing stale local tags.
|