@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.
Files changed (62) hide show
  1. package/dist/auth.d.ts +19 -5
  2. package/dist/auth.js +83 -45
  3. package/dist/build-info.json +3 -3
  4. package/dist/cert.d.ts +1 -1
  5. package/dist/cert.js +124 -74
  6. package/dist/config.js +25 -8
  7. package/dist/express-async.d.ts +6 -0
  8. package/dist/express-async.js +28 -0
  9. package/dist/git-quick-commit.d.ts +2 -0
  10. package/dist/git-quick-commit.js +215 -76
  11. package/dist/git-utils.d.ts +4 -0
  12. package/dist/git-utils.js +60 -11
  13. package/dist/git-worktree.d.ts +8 -1
  14. package/dist/git-worktree.js +406 -41
  15. package/dist/models.d.ts +34 -4
  16. package/dist/models.js +334 -48
  17. package/dist/process-manager.d.ts +22 -30
  18. package/dist/process-manager.js +374 -441
  19. package/dist/provider-history-scanner.d.ts +54 -0
  20. package/dist/provider-history-scanner.js +354 -0
  21. package/dist/request-limits.d.ts +1 -0
  22. package/dist/request-limits.js +8 -0
  23. package/dist/resume-policy.d.ts +2 -0
  24. package/dist/resume-policy.js +5 -0
  25. package/dist/runtime-config.d.ts +16 -0
  26. package/dist/runtime-config.js +49 -0
  27. package/dist/server-file-routes.d.ts +17 -0
  28. package/dist/server-file-routes.js +653 -0
  29. package/dist/server-session-routes.d.ts +16 -3
  30. package/dist/server-session-routes.js +170 -149
  31. package/dist/server-settings-routes.d.ts +43 -0
  32. package/dist/server-settings-routes.js +225 -0
  33. package/dist/server-update-routes.d.ts +61 -0
  34. package/dist/server-update-routes.js +215 -0
  35. package/dist/server.d.ts +6 -4
  36. package/dist/server.js +350 -1313
  37. package/dist/session-logger.d.ts +32 -2
  38. package/dist/session-logger.js +145 -15
  39. package/dist/session-registry.d.ts +27 -0
  40. package/dist/session-registry.js +153 -0
  41. package/dist/session-transport.d.ts +31 -0
  42. package/dist/session-transport.js +82 -0
  43. package/dist/storage.d.ts +24 -6
  44. package/dist/storage.js +291 -44
  45. package/dist/structured-claude-adapter.d.ts +19 -0
  46. package/dist/structured-claude-adapter.js +117 -0
  47. package/dist/structured-codex-adapter.d.ts +3 -0
  48. package/dist/structured-codex-adapter.js +29 -0
  49. package/dist/structured-opencode-adapter.d.ts +11 -0
  50. package/dist/structured-opencode-adapter.js +115 -0
  51. package/dist/structured-provider-common.d.ts +11 -0
  52. package/dist/structured-provider-common.js +77 -0
  53. package/dist/structured-session-manager.d.ts +32 -35
  54. package/dist/structured-session-manager.js +551 -605
  55. package/dist/types.d.ts +10 -0
  56. package/dist/update-helper.js +5 -1
  57. package/dist/web-ui/content/scripts.js +32 -32
  58. package/dist/web-ui/embedded-assets.d.ts +1 -1
  59. package/dist/web-ui/embedded-assets.js +2 -2
  60. package/dist/ws-broadcast.d.ts +16 -1
  61. package/dist/ws-broadcast.js +124 -58
  62. package/package.json +2 -1
@@ -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-session-manager.js";
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 = runGit(["diff", "--cached", "--submodule=log"], cwd, 5000);
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 = runGit(["diff", "--cached", "--name-only"], cwd, 3000);
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 = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
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
- runGit(["add", "-A"], cwd, 5000);
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 = runGit(["show", "HEAD", "--no-color", "--submodule=log"], cwd, 5000);
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 = runGit(["show", "HEAD", "--name-only"], cwd, 3000);
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 = runGit(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd) || undefined;
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
- runGit(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd);
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 = resolvePushRemote(cwd);
747
+ const pushRemote = await resolvePushRemoteAsync(cwd);
609
748
  try {
610
749
  if (pushCommits) {
611
750
  if (hasUpstream) {
612
- runGit(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
751
+ await runGitAsync(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
613
752
  }
614
753
  else {
615
- runGit(["push", "-u", recurseFlag, pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
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
- runGit(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
761
+ await runGitAsync(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
623
762
  }
624
763
  }
625
764
  else {
626
- runGit(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
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
- assertGitWorkTree(cwd);
777
+ await assertGitWorkTreeAsync(cwd);
639
778
  let headHash;
640
779
  try {
641
- headHash = runGit(["rev-parse", "HEAD"], cwd);
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 = runGit(["log", "-1", "--pretty=format:%s"], cwd);
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
- runGit(["rev-parse", "--verify", `refs/tags/${tagName}`], cwd);
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
- runGit(["tag", tagName], cwd);
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
- assertGitWorkTree(cwd);
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 = runGit(["symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`], subCwd);
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 = runGit(["branch", "--show-current"], subCwd);
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
- runGit(["rev-parse", "--verify", `refs/remotes/${remote}/main`], subCwd);
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 = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], parentCwd);
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
- runGit(["add", "-A"], subCwd, 5000);
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 = runGitAllowEmpty(["diff", "--cached", "--name-only"], subCwd).trim();
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
- runGit(["commit", "-m", message], subCwd, 10_000);
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 = runGit(["rev-parse", "--short", "HEAD"], subCwd);
964
+ hash = await runGitAsync(["rev-parse", "--short", "HEAD"], subCwd);
826
965
  }
827
966
  catch { /* ignore */ }
828
- const remote = resolvePushRemote(subCwd);
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
- runGit(["rev-parse", "--verify", `refs/tags/${tagName}`], subCwd);
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
- runGit(["tag", tagName], subCwd);
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
- runGit(["push", info.remote, `HEAD:refs/heads/${info.branch}`], subCwd, GIT_PUSH_TIMEOUT_MS);
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
- runGit(["push", info.remote, `refs/tags/${opts.tagName}`], subCwd, GIT_PUSH_TIMEOUT_MS);
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 = runGit(["rev-parse", "--show-toplevel"], cwd);
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 = runGitAllowEmpty(["config", "-f", `${base}/.gitmodules`, "--get-regexp", "\\.path$"], base);
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 = resolvePushRemote(subCwd);
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 runGit(["rev-parse", "HEAD"], cwd);
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 = runGit(["log", "-1", "--pretty=format:%h%x09%s"], cwd);
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 runGit(["describe", "--tags", "--exact-match", "HEAD"], cwd) || undefined;
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
- assertGitWorkTree(opts.cwd);
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
- assertGitWorkTree(cwd);
1223
+ await assertGitWorkTreeAsync(cwd);
1085
1224
  // 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
1086
1225
  // AI 生成 message 时也基于这个 staged diff。
1087
1226
  try {
1088
- runGit(["add", "-A"], cwd, 5000);
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 = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
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 = runGitAllowEmpty(["status", "--porcelain=v2", "--untracked-files=all"], cwd);
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
- runGit(["add", "-A"], cwd, 5000);
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 = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
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
- runGit(["commit", "-m", message], cwd, 10_000);
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 = runGit(["rev-parse", "--short", "HEAD"], cwd);
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
- runGit(["tag", tagName], cwd);
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.