git-sync-tui 0.1.3 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +623 -184
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import { render } from "ink";
11
11
  import meow from "meow";
12
12
 
13
13
  // src/app.tsx
14
- import { useState as useState7, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback2 } from "react";
14
+ import { useState as useState7, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
15
15
  import { Box as Box9, useApp } from "ink";
16
16
  import { Spinner as Spinner6 } from "@inkjs/ui";
17
17
 
@@ -32,14 +32,18 @@ function StepProgress({ current }) {
32
32
  " ",
33
33
  isActive ? /* @__PURE__ */ jsx(Text, { bold: true, children: label }) : label
34
34
  ] }),
35
- !isLast && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " \u2500 " })
35
+ !isLast && /* @__PURE__ */ jsxs(Text, { color: isDone ? "green" : "gray", dimColor: !isDone, children: [
36
+ " ",
37
+ "\u2500\u2500\u2500",
38
+ " "
39
+ ] })
36
40
  ] }, label);
37
41
  }) });
38
42
  }
39
43
  function SectionHeader({ title, subtitle }) {
40
44
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
41
45
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
42
- "\u25B8 ",
46
+ "\u25BE ",
43
47
  title
44
48
  ] }),
45
49
  subtitle && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
@@ -50,10 +54,8 @@ function SectionHeader({ title, subtitle }) {
50
54
  }
51
55
  function KeyHints({ hints }) {
52
56
  return /* @__PURE__ */ jsx(Box, { gap: 1, flexWrap: "wrap", children: hints.map(({ key, label }) => /* @__PURE__ */ jsxs(Box, { children: [
53
- /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "[" }),
54
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: key }),
55
- /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "]" }),
56
- /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
57
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "gray", color: "white", bold: true, children: ` ${key} ` }),
58
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
57
59
  " ",
58
60
  label
59
61
  ] })
@@ -61,7 +63,7 @@ function KeyHints({ hints }) {
61
63
  }
62
64
  function InlineKeys({ hints }) {
63
65
  return /* @__PURE__ */ jsx(Box, { gap: 1, children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
64
- /* @__PURE__ */ jsxs(Text, { color: "green", children: [
66
+ /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
65
67
  "[",
66
68
  key,
67
69
  "]"
@@ -92,13 +94,12 @@ function StatusPanel({ type, title, children }) {
92
94
  }
93
95
  function AppHeader({ step, stashed }) {
94
96
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
95
- /* @__PURE__ */ jsxs(Box, { children: [
96
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C7 " }),
97
- /* @__PURE__ */ jsx(Text, { bold: true, children: "git-sync-tui" }),
98
- /* @__PURE__ */ jsx(Text, { color: "gray", children: " cherry-pick --no-commit" }),
99
- stashed && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " \u25AA stashed" })
97
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
98
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "cyan", color: "white", bold: true, children: " git-sync-tui " }),
99
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "cherry-pick --no-commit" }),
100
+ stashed && /* @__PURE__ */ jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " STASHED " })
100
101
  ] }),
101
- /* @__PURE__ */ jsxs(Box, { children: [
102
+ /* @__PURE__ */ jsxs(Box, { marginTop: 0, children: [
102
103
  /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " " }),
103
104
  /* @__PURE__ */ jsx(StepProgress, { current: step })
104
105
  ] })
@@ -156,38 +157,66 @@ async function addRemote(name, url) {
156
157
  const git = getGit();
157
158
  await git.addRemote(name, url);
158
159
  }
159
- async function getRemoteBranches(remote) {
160
+ async function getRemoteBranches(remote2) {
160
161
  const git = getGit();
161
162
  try {
162
- await git.fetch(remote);
163
+ const lsResult = await git.raw(["ls-remote", "--heads", remote2]);
164
+ if (lsResult.trim()) {
165
+ return lsResult.trim().split("\n").map((line) => line.replace(/^.*refs\/heads\//, "")).filter(Boolean).sort();
166
+ }
163
167
  } catch {
164
168
  }
165
169
  const result = await git.branch(["-r"]);
166
- const prefix = `${remote}/`;
167
- return result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
170
+ const prefix = `${remote2}/`;
171
+ const branches = result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
172
+ if (branches.length > 0) return branches;
173
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93 '${remote2}' \u7684\u5206\u652F\u5217\u8868\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u4ED3\u5E93\u5730\u5740`);
168
174
  }
169
- async function getCommits(remote, branch, count = 30) {
175
+ async function getCommits(remote2, branch2, count2 = 100) {
170
176
  const git = getGit();
171
- const ref = `${remote}/${branch}`;
172
- const log = await git.log({
173
- from: void 0,
174
- to: ref,
175
- maxCount: count,
176
- format: {
177
- hash: "%H",
178
- shortHash: "%h",
179
- message: "%s",
180
- author: "%an",
181
- date: "%ar"
177
+ const ref = `${remote2}/${branch2}`;
178
+ try {
179
+ await git.raw(["rev-parse", "--verify", ref]);
180
+ } catch {
181
+ try {
182
+ await git.fetch(remote2, branch2);
183
+ } catch {
184
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6 ${ref}\uFF0C\u8BF7\u68C0\u67E5\u8FDC\u7A0B\u4ED3\u5E93\u8FDE\u63A5`);
182
185
  }
186
+ }
187
+ const result = await git.raw([
188
+ "log",
189
+ ref,
190
+ `--max-count=${count2}`,
191
+ "--format=%H%n%h%n%s%n%an%n%ar%n---"
192
+ ]);
193
+ if (!result.trim()) return [];
194
+ const entries = result.trim().split("\n---\n").filter(Boolean);
195
+ return entries.map((block) => {
196
+ const [hash, shortHash, message, author, date] = block.split("\n");
197
+ return { hash, shortHash, message: message || "", author: author || "", date: date || "" };
183
198
  });
184
- return log.all.map((entry) => ({
185
- hash: entry.hash,
186
- shortHash: entry.hash.substring(0, 7),
187
- message: entry.message || "",
188
- author: entry.author || "",
189
- date: entry.date || ""
190
- }));
199
+ }
200
+ async function getUnsyncedCommits(remote2, branch2, count2 = 100) {
201
+ const git = getGit();
202
+ const ref = `${remote2}/${branch2}`;
203
+ const allCommits = await getCommits(remote2, branch2, count2);
204
+ try {
205
+ const result = await git.raw([
206
+ "cherry",
207
+ "HEAD",
208
+ ref
209
+ ]);
210
+ const unsyncedHashes = new Set(
211
+ result.trim().split("\n").filter((line) => line.startsWith("+ ")).map((line) => line.substring(2).trim())
212
+ );
213
+ return allCommits.map((c) => ({
214
+ ...c,
215
+ synced: !unsyncedHashes.has(c.hash)
216
+ }));
217
+ } catch {
218
+ return allCommits;
219
+ }
191
220
  }
192
221
  async function getMultiCommitStat(hashes) {
193
222
  if (hashes.length === 0) return "";
@@ -378,10 +407,10 @@ function StashRecovery({ timestamp, onRecover, onSkip }) {
378
407
  // src/components/remote-select.tsx
379
408
  import { useState as useState3 } from "react";
380
409
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
381
- import { Select, Spinner as Spinner2, TextInput } from "@inkjs/ui";
410
+ import { Spinner as Spinner2, TextInput } from "@inkjs/ui";
382
411
 
383
412
  // src/hooks/use-git.ts
384
- import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
413
+ import { useState as useState2, useEffect as useEffect2, useCallback, useRef } from "react";
385
414
  function useAsync(fn, deps = []) {
386
415
  const [state, setState] = useState2({
387
416
  data: null,
@@ -405,17 +434,55 @@ function useAsync(fn, deps = []) {
405
434
  function useRemotes() {
406
435
  return useAsync(() => getRemotes(), []);
407
436
  }
408
- function useBranches(remote) {
437
+ function useBranches(remote2) {
409
438
  return useAsync(
410
- () => remote ? getRemoteBranches(remote) : Promise.resolve([]),
411
- [remote]
439
+ () => remote2 ? getRemoteBranches(remote2) : Promise.resolve([]),
440
+ [remote2]
412
441
  );
413
442
  }
414
- function useCommits(remote, branch, count = 30) {
415
- return useAsync(
416
- () => remote && branch ? getCommits(remote, branch, count) : Promise.resolve([]),
417
- [remote, branch, count]
418
- );
443
+ function useCommits(remote2, branch2, pageSize = 100) {
444
+ const [data, setData] = useState2(null);
445
+ const [loading, setLoading] = useState2(true);
446
+ const [loadingMore, setLoadingMore] = useState2(false);
447
+ const [error2, setError] = useState2(null);
448
+ const [hasMore, setHasMore] = useState2(true);
449
+ const loadedRef = useRef(0);
450
+ useEffect2(() => {
451
+ if (!remote2 || !branch2) {
452
+ setData([]);
453
+ setLoading(false);
454
+ setHasMore(false);
455
+ return;
456
+ }
457
+ setData(null);
458
+ setLoading(true);
459
+ setError(null);
460
+ setHasMore(true);
461
+ loadedRef.current = 0;
462
+ getUnsyncedCommits(remote2, branch2, pageSize).then((commits2) => {
463
+ setData(commits2);
464
+ setLoading(false);
465
+ loadedRef.current = commits2.length;
466
+ setHasMore(commits2.length >= pageSize);
467
+ }).catch((err) => {
468
+ setError(err.message);
469
+ setLoading(false);
470
+ });
471
+ }, [remote2, branch2, pageSize]);
472
+ const loadMore = useCallback(async () => {
473
+ if (!remote2 || !branch2 || loadingMore || !hasMore) return;
474
+ setLoadingMore(true);
475
+ try {
476
+ const nextCount = loadedRef.current + pageSize;
477
+ const allCommits = await getUnsyncedCommits(remote2, branch2, nextCount);
478
+ setData(allCommits);
479
+ setHasMore(allCommits.length >= nextCount);
480
+ loadedRef.current = allCommits.length;
481
+ } catch {
482
+ }
483
+ setLoadingMore(false);
484
+ }, [remote2, branch2, pageSize, loadingMore, hasMore]);
485
+ return { data, loading, loadingMore, error: error2, hasMore, loadMore };
419
486
  }
420
487
  function useCommitStat(hashes) {
421
488
  const [stat, setStat] = useState2("");
@@ -445,28 +512,45 @@ function extractRemoteName(url) {
445
512
  return lastSegment;
446
513
  }
447
514
  function RemoteSelect({ onSelect, onBack }) {
448
- const { data: remotes, loading, error, reload } = useRemotes();
515
+ const { data: remotes, loading, error: error2, reload } = useRemotes();
449
516
  const [phase, setPhase] = useState3("list");
517
+ const [cursorIndex, setCursorIndex] = useState3(0);
450
518
  const [customUrl, setCustomUrl] = useState3("");
451
519
  const [addError, setAddError] = useState3(null);
452
- useInput3((_input, key) => {
520
+ const totalItems = (remotes?.length || 0) + 1;
521
+ useInput3((input, key) => {
522
+ if (phase !== "list") {
523
+ if (key.escape) {
524
+ if (phase === "input-name") {
525
+ setPhase("input-url");
526
+ } else if (phase === "input-url") {
527
+ setPhase("list");
528
+ }
529
+ }
530
+ return;
531
+ }
453
532
  if (key.escape) {
454
- if (phase === "input-name") {
533
+ onBack?.();
534
+ } else if (key.upArrow) {
535
+ setCursorIndex((prev) => Math.max(0, prev - 1));
536
+ } else if (key.downArrow) {
537
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
538
+ } else if (key.return) {
539
+ if (remotes && cursorIndex < remotes.length) {
540
+ onSelect(remotes[cursorIndex].name);
541
+ } else {
455
542
  setPhase("input-url");
456
- } else if (phase === "input-url") {
457
- setPhase("list");
458
- } else if (phase === "list") {
459
- onBack?.();
460
543
  }
461
544
  }
462
545
  });
463
546
  if (loading) {
464
547
  return /* @__PURE__ */ jsx4(Spinner2, { label: "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93..." });
465
548
  }
466
- if (error) {
549
+ if (error2) {
467
550
  return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
468
- "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
469
- error
551
+ "\u2716 ",
552
+ "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
553
+ error2
470
554
  ] });
471
555
  }
472
556
  if (phase === "adding") {
@@ -496,7 +580,10 @@ function RemoteSelect({ onSelect, onBack }) {
496
580
  }
497
581
  )
498
582
  ] }),
499
- /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u652F\u6301 HTTPS / SSH \u5730\u5740" })
583
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
584
+ " ",
585
+ "\u652F\u6301 HTTPS / SSH \u5730\u5740"
586
+ ] })
500
587
  ] });
501
588
  }
502
589
  if (phase === "input-name") {
@@ -540,99 +627,187 @@ function RemoteSelect({ onSelect, onBack }) {
540
627
  ] })
541
628
  ] });
542
629
  }
543
- const options = [
544
- ...(remotes || []).map((r) => ({
545
- label: `${r.name} ${r.fetchUrl}`,
546
- value: r.name
547
- })),
548
- {
549
- label: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93...",
550
- value: "__add_custom__"
551
- }
552
- ];
553
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
630
+ const maxNameLen = Math.max(...(remotes || []).map((r) => r.name.length), 0);
631
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
554
632
  /* @__PURE__ */ jsx4(SectionHeader, { title: "\u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
555
- /* @__PURE__ */ jsx4(
556
- Select,
557
- {
558
- options,
559
- onChange: (value) => {
560
- if (value === "__add_custom__") {
561
- setPhase("input-url");
562
- } else {
563
- onSelect(value);
564
- }
565
- }
566
- }
567
- )
633
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
634
+ (remotes || []).map((r, i) => {
635
+ const isCursor = i === cursorIndex;
636
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
637
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
638
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
639
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : "white", bold: isCursor, children: r.name.padEnd(maxNameLen + 2) }),
640
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: r.fetchUrl })
641
+ ] }, r.name);
642
+ }),
643
+ /* @__PURE__ */ jsxs4(Box4, { children: [
644
+ /* @__PURE__ */ jsx4(Text4, { color: cursorIndex === (remotes?.length || 0) ? "cyan" : "gray", children: cursorIndex === (remotes?.length || 0) ? "\u203A" : " " }),
645
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
646
+ /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: cursorIndex !== (remotes?.length || 0), children: "+ \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." })
647
+ ] })
648
+ ] })
568
649
  ] });
569
650
  }
570
651
 
571
652
  // src/components/branch-select.tsx
572
653
  import { useState as useState4, useMemo } from "react";
573
654
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
574
- import { Select as Select2, Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
655
+ import { Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
575
656
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
576
- function BranchSelect({ remote, onSelect, onBack }) {
577
- const { data: branches, loading, error } = useBranches(remote);
657
+ function BranchSelect({ remote: remote2, onSelect, onBack }) {
658
+ const { data: branches, loading, error: error2 } = useBranches(remote2);
578
659
  const [filter, setFilter] = useState4("");
579
- useInput4((_input, key) => {
580
- if (key.escape) onBack?.();
581
- });
582
- const filteredOptions = useMemo(() => {
660
+ const [cursorIndex, setCursorIndex] = useState4(0);
661
+ const filteredBranches = useMemo(() => {
583
662
  if (!branches) return [];
584
- const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
585
- return filtered.map((b) => ({ label: b, value: b }));
663
+ return filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
586
664
  }, [branches, filter]);
665
+ const visibleCount = 10;
666
+ const startIdx = Math.max(0, Math.min(cursorIndex - Math.floor(visibleCount / 2), filteredBranches.length - visibleCount));
667
+ const visibleBranches = filteredBranches.slice(startIdx, startIdx + visibleCount);
668
+ useInput4((input, key) => {
669
+ if (key.escape) {
670
+ onBack?.();
671
+ } else if (key.upArrow) {
672
+ setCursorIndex((prev) => Math.max(0, prev - 1));
673
+ } else if (key.downArrow) {
674
+ setCursorIndex((prev) => Math.min(filteredBranches.length - 1, prev + 1));
675
+ } else if (key.return) {
676
+ if (filteredBranches.length > 0) {
677
+ onSelect(filteredBranches[cursorIndex]);
678
+ }
679
+ }
680
+ });
587
681
  if (loading) {
588
- return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` });
682
+ return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote2} \u7684\u5206\u652F\u5217\u8868...` });
589
683
  }
590
- if (error) {
684
+ if (error2) {
591
685
  return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
592
- "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
593
- error
686
+ "\u2716 ",
687
+ "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
688
+ error2
594
689
  ] });
595
690
  }
596
691
  if (!branches || branches.length === 0) {
597
- return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
692
+ return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
693
+ "\u2716 ",
694
+ "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F"
695
+ ] });
598
696
  }
599
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
600
- /* @__PURE__ */ jsx5(SectionHeader, { title: `\u9009\u62E9\u5206\u652F`, subtitle: `${remote} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
601
- /* @__PURE__ */ jsxs5(Box5, { children: [
602
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "/ " }),
697
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
698
+ /* @__PURE__ */ jsx5(SectionHeader, { title: "\u9009\u62E9\u5206\u652F", subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
699
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
700
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "/ " }),
603
701
  /* @__PURE__ */ jsx5(
604
702
  TextInput2,
605
703
  {
606
704
  placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4...",
607
- onChange: setFilter
705
+ onChange: (val) => {
706
+ setFilter(val);
707
+ setCursorIndex(0);
708
+ }
608
709
  }
609
710
  ),
610
711
  filter && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
611
712
  " \xB7 \u5339\u914D ",
612
- filteredOptions.length
713
+ filteredBranches.length
613
714
  ] })
614
715
  ] }),
615
- filteredOptions.length > 0 ? /* @__PURE__ */ jsx5(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B2 \u65E0\u5339\u914D\u5206\u652F" })
716
+ filteredBranches.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
717
+ startIdx > 0 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
718
+ " ",
719
+ "\u2191 ",
720
+ startIdx,
721
+ " more"
722
+ ] }),
723
+ visibleBranches.map((b, i) => {
724
+ const actualIdx = startIdx + i;
725
+ const isCursor = actualIdx === cursorIndex;
726
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
727
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "gray", children: isCursor ? "\u203A" : " " }),
728
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
729
+ /* @__PURE__ */ jsx5(Text5, { color: isCursor ? "cyan" : "white", bold: isCursor, children: b })
730
+ ] }, b);
731
+ }),
732
+ startIdx + visibleCount < filteredBranches.length && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
733
+ " ",
734
+ "\u2193 ",
735
+ filteredBranches.length - startIdx - visibleCount,
736
+ " more"
737
+ ] })
738
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
739
+ "\u25B2 ",
740
+ "\u65E0\u5339\u914D\u5206\u652F"
741
+ ] })
616
742
  ] });
617
743
  }
618
744
 
619
745
  // src/components/commit-list.tsx
620
- import { useState as useState5, useMemo as useMemo2, useRef } from "react";
746
+ import { useState as useState5, useMemo as useMemo2, useRef as useRef2, useEffect as useEffect3 } from "react";
621
747
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
622
748
  import { Spinner as Spinner4 } from "@inkjs/ui";
623
749
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
624
- function CommitList({ remote, branch, onSelect, onBack }) {
625
- const { data: commits, loading, error } = useCommits(remote, branch, 30);
750
+ function StatPanel({ stat, loading, count: count2 }) {
751
+ const STAT_HEIGHT = 8;
752
+ const [scrollOffset, setScrollOffset] = useState5(0);
753
+ const lines = useMemo2(() => stat ? stat.split("\n") : [], [stat]);
754
+ const canScroll = lines.length > STAT_HEIGHT;
755
+ useEffect3(() => {
756
+ setScrollOffset(0);
757
+ }, [stat]);
758
+ useInput5((input) => {
759
+ if (!canScroll) return;
760
+ if (input === "j") {
761
+ setScrollOffset((prev) => Math.min(prev + 1, lines.length - STAT_HEIGHT));
762
+ } else if (input === "k") {
763
+ setScrollOffset((prev) => Math.max(0, prev - 1));
764
+ }
765
+ });
766
+ const visibleLines = canScroll ? lines.slice(scrollOffset, scrollOffset + STAT_HEIGHT) : lines;
767
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
768
+ /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
769
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
770
+ "\u25C6 ",
771
+ "\u5DF2\u9009 ",
772
+ count2,
773
+ " \u4E2A commit \xB7 diff --stat"
774
+ ] }),
775
+ canScroll && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
776
+ scrollOffset + 1,
777
+ "-",
778
+ Math.min(scrollOffset + STAT_HEIGHT, lines.length),
779
+ "/",
780
+ lines.length,
781
+ " [j/k]"
782
+ ] })
783
+ ] }),
784
+ /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: STAT_HEIGHT, children: loading ? /* @__PURE__ */ jsx6(Spinner4, { label: "\u52A0\u8F7D\u4E2D..." }) : visibleLines.length > 0 ? visibleLines.map((line, i) => /* @__PURE__ */ jsx6(Text6, { color: "gray", children: line }, scrollOffset + i)) : /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "(\u65E0\u53D8\u66F4)" }) })
785
+ ] });
786
+ }
787
+ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
788
+ const { data: commits2, loading, loadingMore, error: error2, hasMore, loadMore } = useCommits(remote2, branch2, 100);
626
789
  const [selectedIndex, setSelectedIndex] = useState5(0);
627
790
  const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
628
791
  const [shiftMode, setShiftMode] = useState5(false);
629
- const anchorIndexRef = useRef(null);
792
+ const anchorIndexRef = useRef2(null);
630
793
  const selectedKey = useMemo2(() => Array.from(selectedHashes).sort().join(","), [selectedHashes]);
631
794
  const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
632
795
  const { stat, loading: statLoading } = useCommitStat(selectedArray);
796
+ const syncedCount = useMemo2(() => {
797
+ if (!commits2) return 0;
798
+ return commits2.filter((c) => c.synced).length;
799
+ }, [commits2]);
800
+ useEffect3(() => {
801
+ if (!commits2 || !hasMore || loadingMore) return;
802
+ if (selectedIndex >= commits2.length - 5) {
803
+ loadMore();
804
+ }
805
+ }, [selectedIndex, commits2?.length, hasMore, loadingMore, loadMore]);
633
806
  const toggleCurrent = () => {
634
- if (!commits || commits.length === 0) return;
635
- const hash = commits[selectedIndex].hash;
807
+ if (!commits2 || commits2.length === 0) return;
808
+ const commit = commits2[selectedIndex];
809
+ if (commit.synced) return;
810
+ const hash = commit.hash;
636
811
  setSelectedHashes((prev) => {
637
812
  const next = new Set(prev);
638
813
  if (next.has(hash)) {
@@ -646,49 +821,54 @@ function CommitList({ remote, branch, onSelect, onBack }) {
646
821
  });
647
822
  };
648
823
  const selectRange = (anchor, current) => {
649
- if (!commits) return;
824
+ if (!commits2) return;
650
825
  const start = Math.min(anchor, current);
651
826
  const end = Math.max(anchor, current);
652
827
  setSelectedHashes((prev) => {
653
828
  const next = new Set(prev);
654
829
  for (let i = start; i <= end; i++) {
655
- next.add(commits[i].hash);
830
+ if (!commits2[i].synced) {
831
+ next.add(commits2[i].hash);
832
+ }
656
833
  }
657
834
  return next;
658
835
  });
659
836
  };
660
837
  const toggleAll = () => {
661
- if (!commits || commits.length === 0) return;
838
+ if (!commits2 || commits2.length === 0) return;
839
+ const unsyncedCommits = commits2.filter((c) => !c.synced);
662
840
  setSelectedHashes((prev) => {
663
- if (prev.size === commits.length) {
841
+ if (prev.size === unsyncedCommits.length) {
664
842
  anchorIndexRef.current = null;
665
843
  return /* @__PURE__ */ new Set();
666
844
  }
667
- return new Set(commits.map((c) => c.hash));
845
+ return new Set(unsyncedCommits.map((c) => c.hash));
668
846
  });
669
847
  };
670
848
  const invertSelection = () => {
671
- if (!commits || commits.length === 0) return;
849
+ if (!commits2 || commits2.length === 0) return;
672
850
  setSelectedHashes((prev) => {
673
851
  const next = /* @__PURE__ */ new Set();
674
- for (const c of commits) {
675
- if (!prev.has(c.hash)) next.add(c.hash);
852
+ for (const c of commits2) {
853
+ if (!c.synced && !prev.has(c.hash)) next.add(c.hash);
676
854
  }
677
855
  return next;
678
856
  });
679
857
  };
680
858
  const selectToCurrent = () => {
681
- if (!commits || commits.length === 0) return;
859
+ if (!commits2 || commits2.length === 0) return;
682
860
  setSelectedHashes((prev) => {
683
861
  const next = new Set(prev);
684
862
  for (let i = 0; i <= selectedIndex; i++) {
685
- next.add(commits[i].hash);
863
+ if (!commits2[i].synced) {
864
+ next.add(commits2[i].hash);
865
+ }
686
866
  }
687
867
  return next;
688
868
  });
689
869
  };
690
870
  useInput5((input, key) => {
691
- if (!commits || commits.length === 0) return;
871
+ if (!commits2 || commits2.length === 0) return;
692
872
  if (key.shift) {
693
873
  if (!shiftMode) {
694
874
  setShiftMode(true);
@@ -701,7 +881,7 @@ function CommitList({ remote, branch, onSelect, onBack }) {
701
881
  setSelectedIndex(newIndex);
702
882
  selectRange(anchorIndexRef.current, newIndex);
703
883
  } else if (key.downArrow) {
704
- const newIndex = Math.min(commits.length - 1, selectedIndex + 1);
884
+ const newIndex = Math.min(commits2.length - 1, selectedIndex + 1);
705
885
  setSelectedIndex(newIndex);
706
886
  selectRange(anchorIndexRef.current, newIndex);
707
887
  } else if (input === " ") {
@@ -717,7 +897,7 @@ function CommitList({ remote, branch, onSelect, onBack }) {
717
897
  if (key.upArrow) {
718
898
  setSelectedIndex((prev) => Math.max(0, prev - 1));
719
899
  } else if (key.downArrow) {
720
- setSelectedIndex((prev) => Math.min(commits.length - 1, prev + 1));
900
+ setSelectedIndex((prev) => Math.min(commits2.length - 1, prev + 1));
721
901
  } else if (input === " ") {
722
902
  toggleCurrent();
723
903
  } else if (input === "a" || input === "A") {
@@ -730,46 +910,62 @@ function CommitList({ remote, branch, onSelect, onBack }) {
730
910
  onBack?.();
731
911
  } else if (key.return) {
732
912
  if (selectedHashes.size > 0) {
733
- onSelect(Array.from(selectedHashes), commits);
913
+ onSelect(Array.from(selectedHashes), commits2);
734
914
  }
735
915
  }
736
916
  });
737
917
  if (loading) {
738
- return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` });
918
+ return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote2}/${branch2} \u7684 commit \u5217\u8868...` });
739
919
  }
740
- if (error) {
920
+ if (error2) {
741
921
  return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
742
- "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
743
- error
922
+ "\u2716 ",
923
+ "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
924
+ error2
744
925
  ] });
745
926
  }
746
- if (!commits || commits.length === 0) {
747
- return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
927
+ if (!commits2 || commits2.length === 0) {
928
+ return /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
929
+ "\u25B2 ",
930
+ "\u8BE5\u5206\u652F\u6CA1\u6709 commit"
931
+ ] });
748
932
  }
749
933
  const visibleCount = 10;
750
- const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits.length - visibleCount));
751
- const visibleCommits = commits.slice(startIdx, startIdx + visibleCount);
934
+ const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits2.length - visibleCount));
935
+ const visibleCommits = commits2.slice(startIdx, startIdx + visibleCount);
936
+ const unsyncedTotal = commits2.length - syncedCount;
752
937
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
753
938
  /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
754
939
  /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
755
940
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
756
- remote,
941
+ remote2,
757
942
  "/",
758
- branch
943
+ branch2
759
944
  ] }),
760
945
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
761
- commits.length,
762
- " commits"
946
+ commits2.length,
947
+ " commits",
948
+ hasMore ? "+" : ""
949
+ ] }),
950
+ syncedCount > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
951
+ syncedCount,
952
+ " \u5DF2\u540C\u6B65"
953
+ ] }),
954
+ /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
955
+ unsyncedTotal,
956
+ " \u5F85\u540C\u6B65"
763
957
  ] }),
764
958
  /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
765
959
  "\u5DF2\u9009 ",
766
960
  selectedHashes.size
767
961
  ] }),
768
- shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" })
962
+ shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" }),
963
+ loadingMore && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "\u52A0\u8F7D\u4E2D..." })
769
964
  ] }),
770
965
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
771
966
  startIdx > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
772
- " \u2191 ",
967
+ " ",
968
+ "\u2191 ",
773
969
  startIdx,
774
970
  " more"
775
971
  ] }),
@@ -778,30 +974,60 @@ function CommitList({ remote, branch, onSelect, onBack }) {
778
974
  const isSelected = selectedHashes.has(c.hash);
779
975
  const isCursor = actualIdx === selectedIndex;
780
976
  const isAnchor = actualIdx === anchorIndexRef.current;
977
+ const isSynced = !!c.synced;
781
978
  return /* @__PURE__ */ jsxs6(Box6, { children: [
782
- /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
783
- isCursor ? "\u25B8 " : " ",
784
- isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
785
- " "
786
- ] }),
787
- /* @__PURE__ */ jsx6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: "yellow", children: c.shortHash }),
788
- /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
789
- " ",
790
- c.message
791
- ] }),
979
+ /* @__PURE__ */ jsxs6(
980
+ Text6,
981
+ {
982
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
983
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
984
+ dimColor: isSynced,
985
+ children: [
986
+ isCursor ? "\u25B8 " : " ",
987
+ isSynced ? "\u2713" : isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
988
+ " "
989
+ ]
990
+ }
991
+ ),
992
+ /* @__PURE__ */ jsx6(
993
+ Text6,
994
+ {
995
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
996
+ color: isSynced ? "gray" : "yellow",
997
+ dimColor: isSynced,
998
+ children: c.shortHash
999
+ }
1000
+ ),
1001
+ /* @__PURE__ */ jsxs6(
1002
+ Text6,
1003
+ {
1004
+ backgroundColor: isCursor ? isSynced ? "gray" : "blue" : void 0,
1005
+ color: isSynced ? "gray" : isSelected ? "green" : "white",
1006
+ dimColor: isSynced,
1007
+ children: [
1008
+ " ",
1009
+ c.message
1010
+ ]
1011
+ }
1012
+ ),
792
1013
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
793
1014
  " ",
794
1015
  c.author,
795
1016
  " \xB7 ",
796
1017
  c.date
797
- ] })
1018
+ ] }),
1019
+ isSynced && /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: " [\u5DF2\u540C\u6B65]" })
798
1020
  ] }, c.hash);
799
1021
  }),
800
- startIdx + visibleCount < commits.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
801
- " \u2193 ",
802
- commits.length - startIdx - visibleCount,
1022
+ startIdx + visibleCount < commits2.length ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1023
+ " ",
1024
+ "\u2193 ",
1025
+ commits2.length - startIdx - visibleCount,
803
1026
  " more"
804
- ] })
1027
+ ] }) : hasMore ? /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
1028
+ " ",
1029
+ "\u2193 \u6EDA\u52A8\u52A0\u8F7D\u66F4\u591A..."
1030
+ ] }) : null
805
1031
  ] }),
806
1032
  /* @__PURE__ */ jsx6(KeyHints, { hints: [
807
1033
  { key: "\u2191\u2193", label: "\u5BFC\u822A" },
@@ -813,14 +1039,14 @@ function CommitList({ remote, branch, onSelect, onBack }) {
813
1039
  { key: "Enter", label: "\u786E\u8BA4" },
814
1040
  { key: "Esc", label: "\u8FD4\u56DE" }
815
1041
  ] }),
816
- selectedHashes.size > 0 && /* @__PURE__ */ jsx6(StatusPanel, { type: "info", title: `\u5DF2\u9009 ${selectedHashes.size} \u4E2A commit \xB7 diff --stat`, children: statLoading ? /* @__PURE__ */ jsx6(Spinner4, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx6(Text6, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" }) })
1042
+ selectedHashes.size > 0 && /* @__PURE__ */ jsx6(StatPanel, { stat, loading: statLoading, count: selectedHashes.size })
817
1043
  ] });
818
1044
  }
819
1045
 
820
1046
  // src/components/confirm-panel.tsx
821
1047
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
822
1048
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
823
- function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
1049
+ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
824
1050
  useInput6((input, key) => {
825
1051
  if (key.escape) {
826
1052
  onCancel();
@@ -832,7 +1058,7 @@ function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggle
832
1058
  onToggleMainline();
833
1059
  }
834
1060
  });
835
- const selectedCommits = selectedHashes.map((hash) => commits.find((c) => c.hash === hash)).filter(Boolean);
1061
+ const selectedCommits = selectedHashes.map((hash) => commits2.find((c) => c.hash === hash)).filter(Boolean);
836
1062
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
837
1063
  /* @__PURE__ */ jsx7(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
838
1064
  /* @__PURE__ */ jsx7(StatusPanel, { type: "info", title: `cherry-pick --no-commit \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs7(Box7, { children: [
@@ -875,7 +1101,7 @@ function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggle
875
1101
  }
876
1102
 
877
1103
  // src/components/result-panel.tsx
878
- import { useState as useState6, useEffect as useEffect3 } from "react";
1104
+ import { useState as useState6, useEffect as useEffect4 } from "react";
879
1105
  import { Box as Box8, Text as Text8 } from "ink";
880
1106
  import { Spinner as Spinner5 } from "@inkjs/ui";
881
1107
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -892,7 +1118,7 @@ function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, on
892
1118
  if (ok) onStashRestored();
893
1119
  return ok;
894
1120
  };
895
- useEffect3(() => {
1121
+ useEffect4(() => {
896
1122
  async function run() {
897
1123
  const res = await cherryPick(selectedHashes, useMainline);
898
1124
  setResult(res);
@@ -966,22 +1192,23 @@ var STEP_NUMBER = {
966
1192
  result: 5
967
1193
  };
968
1194
  var STEP_DEBOUNCE = 100;
969
- function App() {
1195
+ function App({ initialRemote, initialBranch }) {
970
1196
  const { exit } = useApp();
1197
+ const entryStep = initialRemote && initialBranch ? "commits" : initialRemote ? "branch" : "remote";
971
1198
  const [step, setStepRaw] = useState7("checking");
972
1199
  const [inputReady, setInputReady] = useState7(true);
973
- const [remote, setRemote] = useState7("");
974
- const [branch, setBranch] = useState7("");
1200
+ const [remote2, setRemote] = useState7(initialRemote || "");
1201
+ const [branch2, setBranch] = useState7(initialBranch || "");
975
1202
  const [selectedHashes, setSelectedHashes] = useState7([]);
976
- const [commits, setCommits] = useState7([]);
1203
+ const [commits2, setCommits] = useState7([]);
977
1204
  const [hasMerge, setHasMerge] = useState7(false);
978
1205
  const [useMainline, setUseMainline] = useState7(false);
979
1206
  const [stashed, setStashed] = useState7(false);
980
1207
  const [guardTimestamp, setGuardTimestamp] = useState7();
981
- const stashedRef = useRef2(false);
982
- const stashRestoredRef = useRef2(false);
983
- const mountedRef = useRef2(true);
984
- const debounceTimer = useRef2(null);
1208
+ const stashedRef = useRef3(false);
1209
+ const stashRestoredRef = useRef3(false);
1210
+ const mountedRef = useRef3(true);
1211
+ const debounceTimer = useRef3(null);
985
1212
  const setStep = useCallback2((newStep) => {
986
1213
  setInputReady(false);
987
1214
  setStepRaw(newStep);
@@ -1008,7 +1235,7 @@ function App() {
1008
1235
  stashRestoredRef.current = true;
1009
1236
  removeStashGuard();
1010
1237
  }, []);
1011
- useEffect4(() => {
1238
+ useEffect5(() => {
1012
1239
  mountedRef.current = true;
1013
1240
  async function check() {
1014
1241
  const guard = await checkStashGuard();
@@ -1020,7 +1247,7 @@ function App() {
1020
1247
  }
1021
1248
  const clean = await isWorkingDirClean();
1022
1249
  if (!mountedRef.current) return;
1023
- setStep(clean ? "remote" : "stash-prompt");
1250
+ setStep(clean ? entryStep : "stash-prompt");
1024
1251
  }
1025
1252
  check();
1026
1253
  const onSignal = () => {
@@ -1045,7 +1272,7 @@ function App() {
1045
1272
  stashedRef.current = true;
1046
1273
  await writeStashGuard();
1047
1274
  }
1048
- if (mountedRef.current) setStep("remote");
1275
+ if (mountedRef.current) setStep(entryStep);
1049
1276
  };
1050
1277
  const doStashRecover = async () => {
1051
1278
  const entry = await findStashEntry();
@@ -1055,13 +1282,13 @@ function App() {
1055
1282
  await removeStashGuard();
1056
1283
  if (!mountedRef.current) return;
1057
1284
  const clean = await isWorkingDirClean();
1058
- if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1285
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1059
1286
  };
1060
1287
  const skipStashRecover = async () => {
1061
1288
  await removeStashGuard();
1062
1289
  if (!mountedRef.current) return;
1063
1290
  const clean = await isWorkingDirClean();
1064
- if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1291
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1065
1292
  };
1066
1293
  const goBack = useCallback2((fromStep) => {
1067
1294
  const backMap = {
@@ -1092,7 +1319,7 @@ function App() {
1092
1319
  StashPrompt,
1093
1320
  {
1094
1321
  onConfirm: doStash,
1095
- onSkip: () => setStep("remote")
1322
+ onSkip: () => setStep(entryStep)
1096
1323
  }
1097
1324
  ),
1098
1325
  step === "remote" && inputReady && /* @__PURE__ */ jsx9(
@@ -1108,7 +1335,7 @@ function App() {
1108
1335
  step === "branch" && inputReady && /* @__PURE__ */ jsx9(
1109
1336
  BranchSelect,
1110
1337
  {
1111
- remote,
1338
+ remote: remote2,
1112
1339
  onSelect: (b) => {
1113
1340
  setBranch(b);
1114
1341
  setStep("commits");
@@ -1119,8 +1346,8 @@ function App() {
1119
1346
  step === "commits" && inputReady && /* @__PURE__ */ jsx9(
1120
1347
  CommitList,
1121
1348
  {
1122
- remote,
1123
- branch,
1349
+ remote: remote2,
1350
+ branch: branch2,
1124
1351
  onSelect: async (hashes, loadedCommits) => {
1125
1352
  setSelectedHashes(hashes);
1126
1353
  setCommits(loadedCommits);
@@ -1134,7 +1361,7 @@ function App() {
1134
1361
  step === "confirm" && inputReady && /* @__PURE__ */ jsx9(
1135
1362
  ConfirmPanel,
1136
1363
  {
1137
- commits,
1364
+ commits: commits2,
1138
1365
  selectedHashes,
1139
1366
  hasMerge,
1140
1367
  useMainline,
@@ -1159,32 +1386,244 @@ function App() {
1159
1386
  ] });
1160
1387
  }
1161
1388
 
1389
+ // src/cli-runner.ts
1390
+ import { createInterface } from "readline";
1391
+ function log(msg) {
1392
+ process.stdout.write(msg + "\n");
1393
+ }
1394
+ function error(msg) {
1395
+ process.stderr.write(msg + "\n");
1396
+ }
1397
+ function padEnd(str, len) {
1398
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
1399
+ }
1400
+ async function confirm(message) {
1401
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1402
+ return new Promise((resolve) => {
1403
+ rl.question(`${message} [y/N] `, (answer) => {
1404
+ rl.close();
1405
+ resolve(answer.trim().toLowerCase() === "y");
1406
+ });
1407
+ });
1408
+ }
1409
+ async function handleStash(noStash2) {
1410
+ const clean = await isWorkingDirClean();
1411
+ if (clean) {
1412
+ log("\u2714 \u5DE5\u4F5C\u533A\u5E72\u51C0");
1413
+ return false;
1414
+ }
1415
+ if (noStash2) {
1416
+ log("\u25B2 \u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u53D8\u66F4\uFF08--no-stash \u8DF3\u8FC7 stash\uFF09");
1417
+ return false;
1418
+ }
1419
+ log("\u25B2 \u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u53D8\u66F4\uFF0C\u81EA\u52A8 stash...");
1420
+ const ok = await stash();
1421
+ if (ok) {
1422
+ await writeStashGuard();
1423
+ log("\u2714 \u5DF2 stash \u5DE5\u4F5C\u533A\u53D8\u66F4");
1424
+ return true;
1425
+ }
1426
+ error("\u2716 stash \u5931\u8D25");
1427
+ process.exit(1);
1428
+ return false;
1429
+ }
1430
+ async function restoreStash(stashed) {
1431
+ if (!stashed) return;
1432
+ const ok = await stashPop();
1433
+ await removeStashGuard();
1434
+ if (ok) {
1435
+ log("\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)");
1436
+ } else {
1437
+ error("\u25B2 stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: git stash pop");
1438
+ }
1439
+ }
1440
+ async function validateRemote(name) {
1441
+ const remotes = await getRemotes();
1442
+ if (!remotes.some((r) => r.name === name)) {
1443
+ const available = remotes.map((r) => r.name).join(", ");
1444
+ error(`\u2716 \u8FDC\u7A0B\u4ED3\u5E93 '${name}' \u4E0D\u5B58\u5728`);
1445
+ if (available) error(` \u53EF\u7528: ${available}`);
1446
+ process.exit(1);
1447
+ }
1448
+ log(`\u2714 \u8FDC\u7A0B\u4ED3\u5E93 '${name}'`);
1449
+ }
1450
+ async function validateBranch(remote2, branch2) {
1451
+ const branches = await getRemoteBranches(remote2);
1452
+ if (!branches.includes(branch2)) {
1453
+ error(`\u2716 \u5206\u652F '${branch2}' \u4E0D\u5B58\u5728\u4E8E ${remote2}`);
1454
+ const similar = branches.filter((b) => b.toLowerCase().includes(branch2.toLowerCase())).slice(0, 5);
1455
+ if (similar.length > 0) error(` \u7C7B\u4F3C: ${similar.join(", ")}`);
1456
+ process.exit(1);
1457
+ }
1458
+ log(`\u2714 \u5206\u652F '${remote2}/${branch2}'`);
1459
+ }
1460
+ function formatCommitLine(c) {
1461
+ return ` ${c.shortHash} ${padEnd(c.message.slice(0, 60), 62)} ${padEnd(c.author, 16)} ${c.date}`;
1462
+ }
1463
+ async function runList(opts) {
1464
+ await validateRemote(opts.remote);
1465
+ log(`\u83B7\u53D6 ${opts.remote}/${opts.branch} \u7684 commit \u5217\u8868...`);
1466
+ await validateBranch(opts.remote, opts.branch);
1467
+ const commits2 = await getCommits(opts.remote, opts.branch, opts.count);
1468
+ if (commits2.length === 0) {
1469
+ log("(\u65E0 commit)");
1470
+ return;
1471
+ }
1472
+ log(`
1473
+ Commits on ${opts.remote}/${opts.branch} (${commits2.length}):`);
1474
+ for (const c of commits2) {
1475
+ log(formatCommitLine(c));
1476
+ }
1477
+ }
1478
+ async function runExec(opts) {
1479
+ const stashed = await handleStash(opts.noStash);
1480
+ await validateRemote(opts.remote);
1481
+ await validateBranch(opts.remote, opts.branch);
1482
+ const allCommits = await getCommits(opts.remote, opts.branch, opts.count);
1483
+ const hashes = opts.commits;
1484
+ const resolved = [];
1485
+ for (const input of hashes) {
1486
+ const match = allCommits.find(
1487
+ (c) => c.hash === input || c.shortHash === input || c.hash.startsWith(input)
1488
+ );
1489
+ if (!match) {
1490
+ error(`\u2716 commit '${input}' \u672A\u627E\u5230\u5728 ${opts.remote}/${opts.branch}`);
1491
+ await restoreStash(stashed);
1492
+ process.exit(1);
1493
+ }
1494
+ resolved.push(match);
1495
+ }
1496
+ const resolvedHashes = resolved.map((c) => c.hash);
1497
+ const hasMerge = await hasMergeCommits(resolvedHashes);
1498
+ if (hasMerge && !opts.mainline) {
1499
+ log("\u25B2 \u68C0\u6D4B\u5230 merge commit\uFF0C\u5EFA\u8BAE\u6DFB\u52A0 --mainline (-m) \u53C2\u6570");
1500
+ }
1501
+ log(`
1502
+ Cherry-pick ${resolved.length} \u4E2A commit (--no-commit${opts.mainline ? " -m 1" : ""}):`);
1503
+ for (const c of resolved) {
1504
+ log(formatCommitLine(c));
1505
+ }
1506
+ log("");
1507
+ if (!opts.yes) {
1508
+ const ok = await confirm("\u786E\u8BA4\u6267\u884C?");
1509
+ if (!ok) {
1510
+ log("\u5DF2\u53D6\u6D88");
1511
+ await restoreStash(stashed);
1512
+ process.exit(0);
1513
+ }
1514
+ }
1515
+ const result = await cherryPick(resolvedHashes, opts.mainline);
1516
+ if (result.success) {
1517
+ log("\u2714 Cherry-pick \u5B8C\u6210");
1518
+ const stat = await getStagedStat();
1519
+ if (stat) {
1520
+ log(`
1521
+ \u6682\u5B58\u533A\u53D8\u66F4 (git diff --cached --stat):
1522
+ ${stat}`);
1523
+ }
1524
+ log("\n\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)");
1525
+ log(" \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:");
1526
+ log(" git diff --cached # \u67E5\u770B\u8BE6\u7EC6 diff");
1527
+ log(' git commit -m "sync: ..." # \u63D0\u4EA4');
1528
+ log(" git reset HEAD # \u6216\u653E\u5F03");
1529
+ } else {
1530
+ error("\u2716 Cherry-pick \u9047\u5230\u51B2\u7A81");
1531
+ if (result.conflictFiles && result.conflictFiles.length > 0) {
1532
+ error("\u51B2\u7A81\u6587\u4EF6:");
1533
+ for (const f of result.conflictFiles) {
1534
+ error(` ${f}`);
1535
+ }
1536
+ }
1537
+ error("\n\u25B8 \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit");
1538
+ error("\u25B8 \u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C");
1539
+ }
1540
+ await restoreStash(stashed);
1541
+ if (!result.success) {
1542
+ process.exit(1);
1543
+ }
1544
+ }
1545
+ async function runCli(opts) {
1546
+ if (opts.list) {
1547
+ await runList(opts);
1548
+ } else {
1549
+ await runExec(opts);
1550
+ }
1551
+ }
1552
+
1162
1553
  // src/cli.tsx
1163
1554
  import { jsx as jsx10 } from "react/jsx-runtime";
1164
1555
  var cli = meow(
1165
1556
  `
1166
1557
  \u7528\u6CD5
1167
- $ git-sync-tui
1558
+ $ git-sync-tui [options]
1168
1559
 
1169
1560
  \u9009\u9879
1170
- --help \u663E\u793A\u5E2E\u52A9
1171
- --version \u663E\u793A\u7248\u672C
1561
+ -r, --remote <name> \u6307\u5B9A\u8FDC\u7A0B\u4ED3\u5E93\u540D\u79F0
1562
+ -b, --branch <name> \u6307\u5B9A\u8FDC\u7A0B\u5206\u652F\u540D\u79F0
1563
+ -c, --commits <hashes> \u6307\u5B9A commit hash\uFF08\u9017\u53F7\u5206\u9694\uFF09
1564
+ -n, --count <number> \u663E\u793A commit \u6570\u91CF\uFF08\u9ED8\u8BA4 100\uFF09
1565
+ -m, --mainline \u5BF9 merge commit \u4F7F\u7528 -m 1
1566
+ -y, --yes \u8DF3\u8FC7\u786E\u8BA4\u76F4\u63A5\u6267\u884C
1567
+ --no-stash \u8DF3\u8FC7 stash \u63D0\u793A
1568
+ --list \u5217\u51FA\u8FDC\u7A0B\u5206\u652F\u7684 commit \u540E\u9000\u51FA
1172
1569
 
1173
- \u8BF4\u660E
1174
- \u4EA4\u4E92\u5F0F TUI \u5DE5\u5177\uFF0C\u4ECE\u8FDC\u7A0B\u5206\u652F\u6311\u9009 commit \u540C\u6B65\u5230\u5F53\u524D\u5206\u652F\u3002
1175
- \u4F7F\u7528 cherry-pick --no-commit \u6A21\u5F0F\uFF0C\u540C\u6B65\u540E\u53EF\u5BA1\u67E5\u518D\u63D0\u4EA4\u3002
1570
+ \u6A21\u5F0F
1571
+ \u65E0\u53C2\u6570 \u4EA4\u4E92\u5F0F TUI \u6A21\u5F0F
1572
+ -r -b --list \u5217\u51FA commit\uFF08\u7EAF\u6587\u672C\uFF09
1573
+ -r -b -c CLI \u6A21\u5F0F\uFF0C\u786E\u8BA4\u540E\u6267\u884C
1574
+ -r -b -c --yes CLI \u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C
1575
+ \u4EC5 -r \u6216 -r -b TUI \u6A21\u5F0F\uFF0C\u8DF3\u8FC7\u5DF2\u6307\u5B9A\u6B65\u9AA4
1176
1576
 
1177
- \u5FEB\u6377\u952E
1577
+ TUI \u5FEB\u6377\u952E
1178
1578
  Space \u9009\u62E9/\u53D6\u6D88 commit
1179
1579
  Shift+\u2191/\u2193 \u8FDE\u7EED\u9009\u62E9
1180
1580
  a \u5168\u9009/\u53D6\u6D88\u5168\u9009
1181
1581
  i \u53CD\u9009
1182
1582
  r \u9009\u81F3\u5F00\u5934
1183
1583
  Enter \u786E\u8BA4\u9009\u62E9
1584
+ Esc \u8FD4\u56DE\u4E0A\u4E00\u6B65
1184
1585
  y/n \u786E\u8BA4/\u53D6\u6D88\u6267\u884C
1586
+
1587
+ \u793A\u4F8B
1588
+ $ git-sync-tui # TUI \u6A21\u5F0F
1589
+ $ git-sync-tui -r upstream -b main --list # \u5217\u51FA commits
1590
+ $ git-sync-tui -r upstream -b main -c abc1234 --yes # \u76F4\u63A5\u6267\u884C
1591
+ $ git-sync-tui -r upstream -b main -c abc1234,def5678 # \u786E\u8BA4\u540E\u6267\u884C
1592
+ $ git-sync-tui -r upstream # TUI \u6A21\u5F0F\uFF0C\u8DF3\u8FC7\u9009\u62E9\u4ED3\u5E93
1185
1593
  `,
1186
1594
  {
1187
- importMeta: import.meta
1595
+ importMeta: import.meta,
1596
+ flags: {
1597
+ remote: { type: "string", shortFlag: "r" },
1598
+ branch: { type: "string", shortFlag: "b" },
1599
+ commits: { type: "string", shortFlag: "c" },
1600
+ count: { type: "number", shortFlag: "n", default: 100 },
1601
+ mainline: { type: "boolean", shortFlag: "m", default: false },
1602
+ yes: { type: "boolean", shortFlag: "y", default: false },
1603
+ noStash: { type: "boolean", default: false },
1604
+ list: { type: "boolean", default: false }
1605
+ }
1188
1606
  }
1189
1607
  );
1190
- render(/* @__PURE__ */ jsx10(App, {}));
1608
+ var { remote, branch, commits, count, mainline, yes, noStash, list } = cli.flags;
1609
+ var commitList = commits ? commits.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1610
+ var hasAllParams = !!(remote && branch && commitList && commitList.length > 0);
1611
+ var isListMode = !!(list && remote && branch);
1612
+ var isCliMode = hasAllParams || isListMode;
1613
+ if (isCliMode) {
1614
+ runCli({
1615
+ remote,
1616
+ branch,
1617
+ commits: commitList,
1618
+ count,
1619
+ mainline,
1620
+ yes,
1621
+ noStash,
1622
+ list
1623
+ }).catch((err) => {
1624
+ console.error("\u2716 " + (err.message || err));
1625
+ process.exit(1);
1626
+ });
1627
+ } else {
1628
+ render(/* @__PURE__ */ jsx10(App, { initialRemote: remote, initialBranch: branch }));
1629
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-sync-tui",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "packageManager": "pnpm@10.32.1",
6
6
  "description": "Interactive TUI tool for cross-repo git commit synchronization (cherry-pick --no-commit)",
7
7
  "author": "KiWi233333",