git-sync-tui 0.1.3 → 0.1.4

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 +318 -90
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -156,38 +156,53 @@ async function addRemote(name, url) {
156
156
  const git = getGit();
157
157
  await git.addRemote(name, url);
158
158
  }
159
- async function getRemoteBranches(remote) {
159
+ async function getRemoteBranches(remote2) {
160
160
  const git = getGit();
161
+ let fetchOk = false;
161
162
  try {
162
- await git.fetch(remote);
163
+ await git.fetch(remote2);
164
+ fetchOk = true;
163
165
  } catch {
164
166
  }
165
167
  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();
168
+ const prefix = `${remote2}/`;
169
+ const branches = result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
170
+ if (branches.length > 0) return branches;
171
+ try {
172
+ const lsResult = await git.raw(["ls-remote", "--heads", remote2]);
173
+ if (!lsResult.trim()) return [];
174
+ return lsResult.trim().split("\n").map((line) => line.replace(/^.*refs\/heads\//, "")).filter(Boolean).sort();
175
+ } catch {
176
+ if (!fetchOk) {
177
+ throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4ED3\u5E93 '${remote2}'\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u4ED3\u5E93\u5730\u5740`);
178
+ }
179
+ return [];
180
+ }
168
181
  }
169
- async function getCommits(remote, branch, count = 30) {
182
+ async function getCommits(remote2, branch2, count2 = 30) {
170
183
  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"
184
+ const ref = `${remote2}/${branch2}`;
185
+ try {
186
+ await git.raw(["rev-parse", "--verify", ref]);
187
+ } catch {
188
+ try {
189
+ await git.fetch(remote2, branch2);
190
+ } catch {
191
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6 ${ref}\uFF0C\u8BF7\u68C0\u67E5\u8FDC\u7A0B\u4ED3\u5E93\u8FDE\u63A5`);
182
192
  }
193
+ }
194
+ const result = await git.raw([
195
+ "log",
196
+ ref,
197
+ `--max-count=${count2}`,
198
+ "--format=%H%n%h%n%s%n%an%n%ar%n---"
199
+ ]);
200
+ if (!result.trim()) return [];
201
+ const entries = result.trim().split("\n---\n").filter(Boolean);
202
+ return entries.map((block) => {
203
+ const [hash, shortHash, message, author, date] = block.split("\n");
204
+ return { hash, shortHash, message: message || "", author: author || "", date: date || "" };
183
205
  });
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
- }));
191
206
  }
192
207
  async function getMultiCommitStat(hashes) {
193
208
  if (hashes.length === 0) return "";
@@ -405,16 +420,16 @@ function useAsync(fn, deps = []) {
405
420
  function useRemotes() {
406
421
  return useAsync(() => getRemotes(), []);
407
422
  }
408
- function useBranches(remote) {
423
+ function useBranches(remote2) {
409
424
  return useAsync(
410
- () => remote ? getRemoteBranches(remote) : Promise.resolve([]),
411
- [remote]
425
+ () => remote2 ? getRemoteBranches(remote2) : Promise.resolve([]),
426
+ [remote2]
412
427
  );
413
428
  }
414
- function useCommits(remote, branch, count = 30) {
429
+ function useCommits(remote2, branch2, count2 = 30) {
415
430
  return useAsync(
416
- () => remote && branch ? getCommits(remote, branch, count) : Promise.resolve([]),
417
- [remote, branch, count]
431
+ () => remote2 && branch2 ? getCommits(remote2, branch2, count2) : Promise.resolve([]),
432
+ [remote2, branch2, count2]
418
433
  );
419
434
  }
420
435
  function useCommitStat(hashes) {
@@ -445,7 +460,7 @@ function extractRemoteName(url) {
445
460
  return lastSegment;
446
461
  }
447
462
  function RemoteSelect({ onSelect, onBack }) {
448
- const { data: remotes, loading, error, reload } = useRemotes();
463
+ const { data: remotes, loading, error: error2, reload } = useRemotes();
449
464
  const [phase, setPhase] = useState3("list");
450
465
  const [customUrl, setCustomUrl] = useState3("");
451
466
  const [addError, setAddError] = useState3(null);
@@ -463,10 +478,10 @@ function RemoteSelect({ onSelect, onBack }) {
463
478
  if (loading) {
464
479
  return /* @__PURE__ */ jsx4(Spinner2, { label: "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93..." });
465
480
  }
466
- if (error) {
481
+ if (error2) {
467
482
  return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
468
483
  "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
469
- error
484
+ error2
470
485
  ] });
471
486
  }
472
487
  if (phase === "adding") {
@@ -573,8 +588,8 @@ import { useState as useState4, useMemo } from "react";
573
588
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
574
589
  import { Select as Select2, Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
575
590
  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);
591
+ function BranchSelect({ remote: remote2, onSelect, onBack }) {
592
+ const { data: branches, loading, error: error2 } = useBranches(remote2);
578
593
  const [filter, setFilter] = useState4("");
579
594
  useInput4((_input, key) => {
580
595
  if (key.escape) onBack?.();
@@ -585,19 +600,19 @@ function BranchSelect({ remote, onSelect, onBack }) {
585
600
  return filtered.map((b) => ({ label: b, value: b }));
586
601
  }, [branches, filter]);
587
602
  if (loading) {
588
- return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` });
603
+ return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote2} \u7684\u5206\u652F\u5217\u8868...` });
589
604
  }
590
- if (error) {
605
+ if (error2) {
591
606
  return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
592
607
  "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
593
- error
608
+ error2
594
609
  ] });
595
610
  }
596
611
  if (!branches || branches.length === 0) {
597
612
  return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
598
613
  }
599
614
  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` }),
615
+ /* @__PURE__ */ jsx5(SectionHeader, { title: `\u9009\u62E9\u5206\u652F`, subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
601
616
  /* @__PURE__ */ jsxs5(Box5, { children: [
602
617
  /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "/ " }),
603
618
  /* @__PURE__ */ jsx5(
@@ -621,8 +636,8 @@ import { useState as useState5, useMemo as useMemo2, useRef } from "react";
621
636
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
622
637
  import { Spinner as Spinner4 } from "@inkjs/ui";
623
638
  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);
639
+ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
640
+ const { data: commits2, loading, error: error2 } = useCommits(remote2, branch2, 30);
626
641
  const [selectedIndex, setSelectedIndex] = useState5(0);
627
642
  const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
628
643
  const [shiftMode, setShiftMode] = useState5(false);
@@ -631,8 +646,8 @@ function CommitList({ remote, branch, onSelect, onBack }) {
631
646
  const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
632
647
  const { stat, loading: statLoading } = useCommitStat(selectedArray);
633
648
  const toggleCurrent = () => {
634
- if (!commits || commits.length === 0) return;
635
- const hash = commits[selectedIndex].hash;
649
+ if (!commits2 || commits2.length === 0) return;
650
+ const hash = commits2[selectedIndex].hash;
636
651
  setSelectedHashes((prev) => {
637
652
  const next = new Set(prev);
638
653
  if (next.has(hash)) {
@@ -646,49 +661,49 @@ function CommitList({ remote, branch, onSelect, onBack }) {
646
661
  });
647
662
  };
648
663
  const selectRange = (anchor, current) => {
649
- if (!commits) return;
664
+ if (!commits2) return;
650
665
  const start = Math.min(anchor, current);
651
666
  const end = Math.max(anchor, current);
652
667
  setSelectedHashes((prev) => {
653
668
  const next = new Set(prev);
654
669
  for (let i = start; i <= end; i++) {
655
- next.add(commits[i].hash);
670
+ next.add(commits2[i].hash);
656
671
  }
657
672
  return next;
658
673
  });
659
674
  };
660
675
  const toggleAll = () => {
661
- if (!commits || commits.length === 0) return;
676
+ if (!commits2 || commits2.length === 0) return;
662
677
  setSelectedHashes((prev) => {
663
- if (prev.size === commits.length) {
678
+ if (prev.size === commits2.length) {
664
679
  anchorIndexRef.current = null;
665
680
  return /* @__PURE__ */ new Set();
666
681
  }
667
- return new Set(commits.map((c) => c.hash));
682
+ return new Set(commits2.map((c) => c.hash));
668
683
  });
669
684
  };
670
685
  const invertSelection = () => {
671
- if (!commits || commits.length === 0) return;
686
+ if (!commits2 || commits2.length === 0) return;
672
687
  setSelectedHashes((prev) => {
673
688
  const next = /* @__PURE__ */ new Set();
674
- for (const c of commits) {
689
+ for (const c of commits2) {
675
690
  if (!prev.has(c.hash)) next.add(c.hash);
676
691
  }
677
692
  return next;
678
693
  });
679
694
  };
680
695
  const selectToCurrent = () => {
681
- if (!commits || commits.length === 0) return;
696
+ if (!commits2 || commits2.length === 0) return;
682
697
  setSelectedHashes((prev) => {
683
698
  const next = new Set(prev);
684
699
  for (let i = 0; i <= selectedIndex; i++) {
685
- next.add(commits[i].hash);
700
+ next.add(commits2[i].hash);
686
701
  }
687
702
  return next;
688
703
  });
689
704
  };
690
705
  useInput5((input, key) => {
691
- if (!commits || commits.length === 0) return;
706
+ if (!commits2 || commits2.length === 0) return;
692
707
  if (key.shift) {
693
708
  if (!shiftMode) {
694
709
  setShiftMode(true);
@@ -701,7 +716,7 @@ function CommitList({ remote, branch, onSelect, onBack }) {
701
716
  setSelectedIndex(newIndex);
702
717
  selectRange(anchorIndexRef.current, newIndex);
703
718
  } else if (key.downArrow) {
704
- const newIndex = Math.min(commits.length - 1, selectedIndex + 1);
719
+ const newIndex = Math.min(commits2.length - 1, selectedIndex + 1);
705
720
  setSelectedIndex(newIndex);
706
721
  selectRange(anchorIndexRef.current, newIndex);
707
722
  } else if (input === " ") {
@@ -717,7 +732,7 @@ function CommitList({ remote, branch, onSelect, onBack }) {
717
732
  if (key.upArrow) {
718
733
  setSelectedIndex((prev) => Math.max(0, prev - 1));
719
734
  } else if (key.downArrow) {
720
- setSelectedIndex((prev) => Math.min(commits.length - 1, prev + 1));
735
+ setSelectedIndex((prev) => Math.min(commits2.length - 1, prev + 1));
721
736
  } else if (input === " ") {
722
737
  toggleCurrent();
723
738
  } else if (input === "a" || input === "A") {
@@ -730,35 +745,35 @@ function CommitList({ remote, branch, onSelect, onBack }) {
730
745
  onBack?.();
731
746
  } else if (key.return) {
732
747
  if (selectedHashes.size > 0) {
733
- onSelect(Array.from(selectedHashes), commits);
748
+ onSelect(Array.from(selectedHashes), commits2);
734
749
  }
735
750
  }
736
751
  });
737
752
  if (loading) {
738
- return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` });
753
+ return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote2}/${branch2} \u7684 commit \u5217\u8868...` });
739
754
  }
740
- if (error) {
755
+ if (error2) {
741
756
  return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
742
757
  "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
743
- error
758
+ error2
744
759
  ] });
745
760
  }
746
- if (!commits || commits.length === 0) {
761
+ if (!commits2 || commits2.length === 0) {
747
762
  return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
748
763
  }
749
764
  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);
765
+ const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits2.length - visibleCount));
766
+ const visibleCommits = commits2.slice(startIdx, startIdx + visibleCount);
752
767
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
753
768
  /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
754
769
  /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
755
770
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
756
- remote,
771
+ remote2,
757
772
  "/",
758
- branch
773
+ branch2
759
774
  ] }),
760
775
  /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
761
- commits.length,
776
+ commits2.length,
762
777
  " commits"
763
778
  ] }),
764
779
  /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
@@ -797,9 +812,9 @@ function CommitList({ remote, branch, onSelect, onBack }) {
797
812
  ] })
798
813
  ] }, c.hash);
799
814
  }),
800
- startIdx + visibleCount < commits.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
815
+ startIdx + visibleCount < commits2.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
801
816
  " \u2193 ",
802
- commits.length - startIdx - visibleCount,
817
+ commits2.length - startIdx - visibleCount,
803
818
  " more"
804
819
  ] })
805
820
  ] }),
@@ -820,7 +835,7 @@ function CommitList({ remote, branch, onSelect, onBack }) {
820
835
  // src/components/confirm-panel.tsx
821
836
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
822
837
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
823
- function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
838
+ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
824
839
  useInput6((input, key) => {
825
840
  if (key.escape) {
826
841
  onCancel();
@@ -832,7 +847,7 @@ function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggle
832
847
  onToggleMainline();
833
848
  }
834
849
  });
835
- const selectedCommits = selectedHashes.map((hash) => commits.find((c) => c.hash === hash)).filter(Boolean);
850
+ const selectedCommits = selectedHashes.map((hash) => commits2.find((c) => c.hash === hash)).filter(Boolean);
836
851
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
837
852
  /* @__PURE__ */ jsx7(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
838
853
  /* @__PURE__ */ jsx7(StatusPanel, { type: "info", title: `cherry-pick --no-commit \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs7(Box7, { children: [
@@ -966,14 +981,15 @@ var STEP_NUMBER = {
966
981
  result: 5
967
982
  };
968
983
  var STEP_DEBOUNCE = 100;
969
- function App() {
984
+ function App({ initialRemote, initialBranch }) {
970
985
  const { exit } = useApp();
986
+ const entryStep = initialRemote && initialBranch ? "commits" : initialRemote ? "branch" : "remote";
971
987
  const [step, setStepRaw] = useState7("checking");
972
988
  const [inputReady, setInputReady] = useState7(true);
973
- const [remote, setRemote] = useState7("");
974
- const [branch, setBranch] = useState7("");
989
+ const [remote2, setRemote] = useState7(initialRemote || "");
990
+ const [branch2, setBranch] = useState7(initialBranch || "");
975
991
  const [selectedHashes, setSelectedHashes] = useState7([]);
976
- const [commits, setCommits] = useState7([]);
992
+ const [commits2, setCommits] = useState7([]);
977
993
  const [hasMerge, setHasMerge] = useState7(false);
978
994
  const [useMainline, setUseMainline] = useState7(false);
979
995
  const [stashed, setStashed] = useState7(false);
@@ -1020,7 +1036,7 @@ function App() {
1020
1036
  }
1021
1037
  const clean = await isWorkingDirClean();
1022
1038
  if (!mountedRef.current) return;
1023
- setStep(clean ? "remote" : "stash-prompt");
1039
+ setStep(clean ? entryStep : "stash-prompt");
1024
1040
  }
1025
1041
  check();
1026
1042
  const onSignal = () => {
@@ -1045,7 +1061,7 @@ function App() {
1045
1061
  stashedRef.current = true;
1046
1062
  await writeStashGuard();
1047
1063
  }
1048
- if (mountedRef.current) setStep("remote");
1064
+ if (mountedRef.current) setStep(entryStep);
1049
1065
  };
1050
1066
  const doStashRecover = async () => {
1051
1067
  const entry = await findStashEntry();
@@ -1055,13 +1071,13 @@ function App() {
1055
1071
  await removeStashGuard();
1056
1072
  if (!mountedRef.current) return;
1057
1073
  const clean = await isWorkingDirClean();
1058
- if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1074
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1059
1075
  };
1060
1076
  const skipStashRecover = async () => {
1061
1077
  await removeStashGuard();
1062
1078
  if (!mountedRef.current) return;
1063
1079
  const clean = await isWorkingDirClean();
1064
- if (mountedRef.current) setStep(clean ? "remote" : "stash-prompt");
1080
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1065
1081
  };
1066
1082
  const goBack = useCallback2((fromStep) => {
1067
1083
  const backMap = {
@@ -1092,7 +1108,7 @@ function App() {
1092
1108
  StashPrompt,
1093
1109
  {
1094
1110
  onConfirm: doStash,
1095
- onSkip: () => setStep("remote")
1111
+ onSkip: () => setStep(entryStep)
1096
1112
  }
1097
1113
  ),
1098
1114
  step === "remote" && inputReady && /* @__PURE__ */ jsx9(
@@ -1108,7 +1124,7 @@ function App() {
1108
1124
  step === "branch" && inputReady && /* @__PURE__ */ jsx9(
1109
1125
  BranchSelect,
1110
1126
  {
1111
- remote,
1127
+ remote: remote2,
1112
1128
  onSelect: (b) => {
1113
1129
  setBranch(b);
1114
1130
  setStep("commits");
@@ -1119,8 +1135,8 @@ function App() {
1119
1135
  step === "commits" && inputReady && /* @__PURE__ */ jsx9(
1120
1136
  CommitList,
1121
1137
  {
1122
- remote,
1123
- branch,
1138
+ remote: remote2,
1139
+ branch: branch2,
1124
1140
  onSelect: async (hashes, loadedCommits) => {
1125
1141
  setSelectedHashes(hashes);
1126
1142
  setCommits(loadedCommits);
@@ -1134,7 +1150,7 @@ function App() {
1134
1150
  step === "confirm" && inputReady && /* @__PURE__ */ jsx9(
1135
1151
  ConfirmPanel,
1136
1152
  {
1137
- commits,
1153
+ commits: commits2,
1138
1154
  selectedHashes,
1139
1155
  hasMerge,
1140
1156
  useMainline,
@@ -1159,32 +1175,244 @@ function App() {
1159
1175
  ] });
1160
1176
  }
1161
1177
 
1178
+ // src/cli-runner.ts
1179
+ import { createInterface } from "readline";
1180
+ function log(msg) {
1181
+ process.stdout.write(msg + "\n");
1182
+ }
1183
+ function error(msg) {
1184
+ process.stderr.write(msg + "\n");
1185
+ }
1186
+ function padEnd(str, len) {
1187
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
1188
+ }
1189
+ async function confirm(message) {
1190
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1191
+ return new Promise((resolve) => {
1192
+ rl.question(`${message} [y/N] `, (answer) => {
1193
+ rl.close();
1194
+ resolve(answer.trim().toLowerCase() === "y");
1195
+ });
1196
+ });
1197
+ }
1198
+ async function handleStash(noStash2) {
1199
+ const clean = await isWorkingDirClean();
1200
+ if (clean) {
1201
+ log("\u2714 \u5DE5\u4F5C\u533A\u5E72\u51C0");
1202
+ return false;
1203
+ }
1204
+ if (noStash2) {
1205
+ log("\u25B2 \u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u53D8\u66F4\uFF08--no-stash \u8DF3\u8FC7 stash\uFF09");
1206
+ return false;
1207
+ }
1208
+ log("\u25B2 \u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u53D8\u66F4\uFF0C\u81EA\u52A8 stash...");
1209
+ const ok = await stash();
1210
+ if (ok) {
1211
+ await writeStashGuard();
1212
+ log("\u2714 \u5DF2 stash \u5DE5\u4F5C\u533A\u53D8\u66F4");
1213
+ return true;
1214
+ }
1215
+ error("\u2716 stash \u5931\u8D25");
1216
+ process.exit(1);
1217
+ return false;
1218
+ }
1219
+ async function restoreStash(stashed) {
1220
+ if (!stashed) return;
1221
+ const ok = await stashPop();
1222
+ await removeStashGuard();
1223
+ if (ok) {
1224
+ log("\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)");
1225
+ } else {
1226
+ error("\u25B2 stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: git stash pop");
1227
+ }
1228
+ }
1229
+ async function validateRemote(name) {
1230
+ const remotes = await getRemotes();
1231
+ if (!remotes.some((r) => r.name === name)) {
1232
+ const available = remotes.map((r) => r.name).join(", ");
1233
+ error(`\u2716 \u8FDC\u7A0B\u4ED3\u5E93 '${name}' \u4E0D\u5B58\u5728`);
1234
+ if (available) error(` \u53EF\u7528: ${available}`);
1235
+ process.exit(1);
1236
+ }
1237
+ log(`\u2714 \u8FDC\u7A0B\u4ED3\u5E93 '${name}'`);
1238
+ }
1239
+ async function validateBranch(remote2, branch2) {
1240
+ const branches = await getRemoteBranches(remote2);
1241
+ if (!branches.includes(branch2)) {
1242
+ error(`\u2716 \u5206\u652F '${branch2}' \u4E0D\u5B58\u5728\u4E8E ${remote2}`);
1243
+ const similar = branches.filter((b) => b.toLowerCase().includes(branch2.toLowerCase())).slice(0, 5);
1244
+ if (similar.length > 0) error(` \u7C7B\u4F3C: ${similar.join(", ")}`);
1245
+ process.exit(1);
1246
+ }
1247
+ log(`\u2714 \u5206\u652F '${remote2}/${branch2}'`);
1248
+ }
1249
+ function formatCommitLine(c) {
1250
+ return ` ${c.shortHash} ${padEnd(c.message.slice(0, 60), 62)} ${padEnd(c.author, 16)} ${c.date}`;
1251
+ }
1252
+ async function runList(opts) {
1253
+ await validateRemote(opts.remote);
1254
+ log(`\u83B7\u53D6 ${opts.remote}/${opts.branch} \u7684 commit \u5217\u8868...`);
1255
+ await validateBranch(opts.remote, opts.branch);
1256
+ const commits2 = await getCommits(opts.remote, opts.branch, opts.count);
1257
+ if (commits2.length === 0) {
1258
+ log("(\u65E0 commit)");
1259
+ return;
1260
+ }
1261
+ log(`
1262
+ Commits on ${opts.remote}/${opts.branch} (${commits2.length}):`);
1263
+ for (const c of commits2) {
1264
+ log(formatCommitLine(c));
1265
+ }
1266
+ }
1267
+ async function runExec(opts) {
1268
+ const stashed = await handleStash(opts.noStash);
1269
+ await validateRemote(opts.remote);
1270
+ await validateBranch(opts.remote, opts.branch);
1271
+ const allCommits = await getCommits(opts.remote, opts.branch, opts.count);
1272
+ const hashes = opts.commits;
1273
+ const resolved = [];
1274
+ for (const input of hashes) {
1275
+ const match = allCommits.find(
1276
+ (c) => c.hash === input || c.shortHash === input || c.hash.startsWith(input)
1277
+ );
1278
+ if (!match) {
1279
+ error(`\u2716 commit '${input}' \u672A\u627E\u5230\u5728 ${opts.remote}/${opts.branch}`);
1280
+ await restoreStash(stashed);
1281
+ process.exit(1);
1282
+ }
1283
+ resolved.push(match);
1284
+ }
1285
+ const resolvedHashes = resolved.map((c) => c.hash);
1286
+ const hasMerge = await hasMergeCommits(resolvedHashes);
1287
+ if (hasMerge && !opts.mainline) {
1288
+ log("\u25B2 \u68C0\u6D4B\u5230 merge commit\uFF0C\u5EFA\u8BAE\u6DFB\u52A0 --mainline (-m) \u53C2\u6570");
1289
+ }
1290
+ log(`
1291
+ Cherry-pick ${resolved.length} \u4E2A commit (--no-commit${opts.mainline ? " -m 1" : ""}):`);
1292
+ for (const c of resolved) {
1293
+ log(formatCommitLine(c));
1294
+ }
1295
+ log("");
1296
+ if (!opts.yes) {
1297
+ const ok = await confirm("\u786E\u8BA4\u6267\u884C?");
1298
+ if (!ok) {
1299
+ log("\u5DF2\u53D6\u6D88");
1300
+ await restoreStash(stashed);
1301
+ process.exit(0);
1302
+ }
1303
+ }
1304
+ const result = await cherryPick(resolvedHashes, opts.mainline);
1305
+ if (result.success) {
1306
+ log("\u2714 Cherry-pick \u5B8C\u6210");
1307
+ const stat = await getStagedStat();
1308
+ if (stat) {
1309
+ log(`
1310
+ \u6682\u5B58\u533A\u53D8\u66F4 (git diff --cached --stat):
1311
+ ${stat}`);
1312
+ }
1313
+ log("\n\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)");
1314
+ log(" \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:");
1315
+ log(" git diff --cached # \u67E5\u770B\u8BE6\u7EC6 diff");
1316
+ log(' git commit -m "sync: ..." # \u63D0\u4EA4');
1317
+ log(" git reset HEAD # \u6216\u653E\u5F03");
1318
+ } else {
1319
+ error("\u2716 Cherry-pick \u9047\u5230\u51B2\u7A81");
1320
+ if (result.conflictFiles && result.conflictFiles.length > 0) {
1321
+ error("\u51B2\u7A81\u6587\u4EF6:");
1322
+ for (const f of result.conflictFiles) {
1323
+ error(` ${f}`);
1324
+ }
1325
+ }
1326
+ error("\n\u25B8 \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit");
1327
+ error("\u25B8 \u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C");
1328
+ }
1329
+ await restoreStash(stashed);
1330
+ if (!result.success) {
1331
+ process.exit(1);
1332
+ }
1333
+ }
1334
+ async function runCli(opts) {
1335
+ if (opts.list) {
1336
+ await runList(opts);
1337
+ } else {
1338
+ await runExec(opts);
1339
+ }
1340
+ }
1341
+
1162
1342
  // src/cli.tsx
1163
1343
  import { jsx as jsx10 } from "react/jsx-runtime";
1164
1344
  var cli = meow(
1165
1345
  `
1166
1346
  \u7528\u6CD5
1167
- $ git-sync-tui
1347
+ $ git-sync-tui [options]
1168
1348
 
1169
1349
  \u9009\u9879
1170
- --help \u663E\u793A\u5E2E\u52A9
1171
- --version \u663E\u793A\u7248\u672C
1350
+ -r, --remote <name> \u6307\u5B9A\u8FDC\u7A0B\u4ED3\u5E93\u540D\u79F0
1351
+ -b, --branch <name> \u6307\u5B9A\u8FDC\u7A0B\u5206\u652F\u540D\u79F0
1352
+ -c, --commits <hashes> \u6307\u5B9A commit hash\uFF08\u9017\u53F7\u5206\u9694\uFF09
1353
+ -n, --count <number> \u663E\u793A commit \u6570\u91CF\uFF08\u9ED8\u8BA4 30\uFF09
1354
+ -m, --mainline \u5BF9 merge commit \u4F7F\u7528 -m 1
1355
+ -y, --yes \u8DF3\u8FC7\u786E\u8BA4\u76F4\u63A5\u6267\u884C
1356
+ --no-stash \u8DF3\u8FC7 stash \u63D0\u793A
1357
+ --list \u5217\u51FA\u8FDC\u7A0B\u5206\u652F\u7684 commit \u540E\u9000\u51FA
1172
1358
 
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
1359
+ \u6A21\u5F0F
1360
+ \u65E0\u53C2\u6570 \u4EA4\u4E92\u5F0F TUI \u6A21\u5F0F
1361
+ -r -b --list \u5217\u51FA commit\uFF08\u7EAF\u6587\u672C\uFF09
1362
+ -r -b -c CLI \u6A21\u5F0F\uFF0C\u786E\u8BA4\u540E\u6267\u884C
1363
+ -r -b -c --yes CLI \u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C
1364
+ \u4EC5 -r \u6216 -r -b TUI \u6A21\u5F0F\uFF0C\u8DF3\u8FC7\u5DF2\u6307\u5B9A\u6B65\u9AA4
1176
1365
 
1177
- \u5FEB\u6377\u952E
1366
+ TUI \u5FEB\u6377\u952E
1178
1367
  Space \u9009\u62E9/\u53D6\u6D88 commit
1179
1368
  Shift+\u2191/\u2193 \u8FDE\u7EED\u9009\u62E9
1180
1369
  a \u5168\u9009/\u53D6\u6D88\u5168\u9009
1181
1370
  i \u53CD\u9009
1182
1371
  r \u9009\u81F3\u5F00\u5934
1183
1372
  Enter \u786E\u8BA4\u9009\u62E9
1373
+ Esc \u8FD4\u56DE\u4E0A\u4E00\u6B65
1184
1374
  y/n \u786E\u8BA4/\u53D6\u6D88\u6267\u884C
1375
+
1376
+ \u793A\u4F8B
1377
+ $ git-sync-tui # TUI \u6A21\u5F0F
1378
+ $ git-sync-tui -r upstream -b main --list # \u5217\u51FA commits
1379
+ $ git-sync-tui -r upstream -b main -c abc1234 --yes # \u76F4\u63A5\u6267\u884C
1380
+ $ git-sync-tui -r upstream -b main -c abc1234,def5678 # \u786E\u8BA4\u540E\u6267\u884C
1381
+ $ git-sync-tui -r upstream # TUI \u6A21\u5F0F\uFF0C\u8DF3\u8FC7\u9009\u62E9\u4ED3\u5E93
1185
1382
  `,
1186
1383
  {
1187
- importMeta: import.meta
1384
+ importMeta: import.meta,
1385
+ flags: {
1386
+ remote: { type: "string", shortFlag: "r" },
1387
+ branch: { type: "string", shortFlag: "b" },
1388
+ commits: { type: "string", shortFlag: "c" },
1389
+ count: { type: "number", shortFlag: "n", default: 30 },
1390
+ mainline: { type: "boolean", shortFlag: "m", default: false },
1391
+ yes: { type: "boolean", shortFlag: "y", default: false },
1392
+ noStash: { type: "boolean", default: false },
1393
+ list: { type: "boolean", default: false }
1394
+ }
1188
1395
  }
1189
1396
  );
1190
- render(/* @__PURE__ */ jsx10(App, {}));
1397
+ var { remote, branch, commits, count, mainline, yes, noStash, list } = cli.flags;
1398
+ var commitList = commits ? commits.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1399
+ var hasAllParams = !!(remote && branch && commitList && commitList.length > 0);
1400
+ var isListMode = !!(list && remote && branch);
1401
+ var isCliMode = hasAllParams || isListMode;
1402
+ if (isCliMode) {
1403
+ runCli({
1404
+ remote,
1405
+ branch,
1406
+ commits: commitList,
1407
+ count,
1408
+ mainline,
1409
+ yes,
1410
+ noStash,
1411
+ list
1412
+ }).catch((err) => {
1413
+ console.error("\u2716 " + (err.message || err));
1414
+ process.exit(1);
1415
+ });
1416
+ } else {
1417
+ render(/* @__PURE__ */ jsx10(App, { initialRemote: remote, initialBranch: branch }));
1418
+ }
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.4",
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",