git-sync-tui 0.1.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 (3) hide show
  1. package/README.md +68 -0
  2. package/dist/cli.js +499 -0
  3. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # git-sync-tui
2
+
3
+ Interactive TUI tool for cross-repo git commit synchronization.
4
+
5
+ Cherry-pick commits from remote branches with an interactive terminal UI — select specific commits, preview changes, and sync with `--no-commit` mode for review before committing.
6
+
7
+ ## Features
8
+
9
+ - **Multi-select commits** — pick non-consecutive commits with Space/Enter
10
+ - **`--no-commit` mode** — changes are staged, not committed, so you can review and edit before committing
11
+ - **Diff preview** — see `--stat` summary of selected commits before executing
12
+ - **Branch search** — filter branches by keyword
13
+ - **Conflict handling** — shows conflicted files when cherry-pick fails
14
+ - **Language agnostic** — works in any git repo (Node.js, Go, Python, Java, etc.)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g git-sync-tui
20
+ ```
21
+
22
+ Requires Node.js >= 20.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # Run in any git repository
28
+ git-sync-tui
29
+ ```
30
+
31
+ ### Workflow
32
+
33
+ ```
34
+ [Select remote] → [Select branch] → [Multi-select commits] → [Preview stat] → [Confirm] → [Cherry-pick --no-commit] → [Review & commit manually]
35
+ ```
36
+
37
+ ### Keyboard Shortcuts
38
+
39
+ | Key | Action |
40
+ |-----|--------|
41
+ | `↑` / `↓` | Navigate |
42
+ | `Space` | Toggle commit selection |
43
+ | `Enter` | Confirm selection |
44
+ | `y` / `n` | Confirm / cancel execution |
45
+
46
+ ### After sync
47
+
48
+ Changes are staged in your working tree (not committed). You can:
49
+
50
+ ```bash
51
+ git diff --cached # Review changes
52
+ git commit -m "sync: ..." # Commit when ready
53
+ git reset HEAD # Or discard all changes
54
+ ```
55
+
56
+ ## Development
57
+
58
+ ```bash
59
+ git clone https://github.com/KiWi233333/git-sync-tui.git
60
+ cd git-sync-tui
61
+ npm install
62
+ npm start # Run with tsx
63
+ npm run build # Build with tsup
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+ import meow from "meow";
6
+
7
+ // src/app.tsx
8
+ import { useState as useState5 } from "react";
9
+ import { Box as Box6, Text as Text6 } from "ink";
10
+
11
+ // src/components/remote-select.tsx
12
+ import { Box, Text } from "ink";
13
+ import { Select, Spinner } from "@inkjs/ui";
14
+
15
+ // src/hooks/use-git.ts
16
+ import { useState, useEffect, useCallback } from "react";
17
+
18
+ // src/utils/git.ts
19
+ import simpleGit from "simple-git";
20
+ var gitInstance = null;
21
+ function getGit(cwd) {
22
+ if (!gitInstance || cwd) {
23
+ gitInstance = simpleGit(cwd);
24
+ }
25
+ return gitInstance;
26
+ }
27
+ async function getRemotes() {
28
+ const git = getGit();
29
+ const remotes = await git.getRemotes(true);
30
+ return remotes.map((r) => ({
31
+ name: r.name,
32
+ fetchUrl: r.refs.fetch
33
+ }));
34
+ }
35
+ async function getRemoteBranches(remote) {
36
+ const git = getGit();
37
+ try {
38
+ await git.fetch(remote);
39
+ } catch {
40
+ }
41
+ const result = await git.branch(["-r"]);
42
+ const prefix = `${remote}/`;
43
+ return result.all.filter((b) => b.startsWith(prefix) && !b.includes("HEAD")).map((b) => b.replace(prefix, "")).sort();
44
+ }
45
+ async function getCommits(remote, branch, count = 30) {
46
+ const git = getGit();
47
+ const ref = `${remote}/${branch}`;
48
+ const log = await git.log({
49
+ from: void 0,
50
+ to: ref,
51
+ maxCount: count,
52
+ format: {
53
+ hash: "%H",
54
+ shortHash: "%h",
55
+ message: "%s",
56
+ author: "%an",
57
+ date: "%ar"
58
+ }
59
+ });
60
+ return log.all.map((entry) => ({
61
+ hash: entry.hash,
62
+ shortHash: entry.hash.substring(0, 7),
63
+ message: entry.message || "",
64
+ author: entry.author || "",
65
+ date: entry.date || ""
66
+ }));
67
+ }
68
+ async function getMultiCommitStat(hashes) {
69
+ if (hashes.length === 0) return "";
70
+ const git = getGit();
71
+ try {
72
+ const stats = [];
73
+ for (const hash of hashes) {
74
+ const result = await git.raw(["diff-tree", "--stat", "--no-commit-id", "-r", hash]);
75
+ if (result.trim()) stats.push(`${hash.substring(0, 7)}:
76
+ ${result.trim()}`);
77
+ }
78
+ return stats.join("\n\n");
79
+ } catch {
80
+ return "(\u65E0\u6CD5\u83B7\u53D6 stat \u4FE1\u606F)";
81
+ }
82
+ }
83
+ async function cherryPick(hashes) {
84
+ const git = getGit();
85
+ try {
86
+ const orderedHashes = [...hashes].reverse();
87
+ for (const hash of orderedHashes) {
88
+ await git.raw(["cherry-pick", "--no-commit", hash]);
89
+ }
90
+ return { success: true };
91
+ } catch (err) {
92
+ try {
93
+ const status = await git.status();
94
+ const conflictFiles = status.conflicted;
95
+ return {
96
+ success: false,
97
+ error: err.message,
98
+ conflictFiles: conflictFiles.length > 0 ? conflictFiles : void 0
99
+ };
100
+ } catch {
101
+ return { success: false, error: err.message };
102
+ }
103
+ }
104
+ }
105
+ async function getStagedStat() {
106
+ const git = getGit();
107
+ try {
108
+ const result = await git.raw(["diff", "--cached", "--stat"]);
109
+ return result.trim();
110
+ } catch {
111
+ return "";
112
+ }
113
+ }
114
+
115
+ // src/hooks/use-git.ts
116
+ function useAsync(fn, deps = []) {
117
+ const [state, setState] = useState({
118
+ data: null,
119
+ loading: true,
120
+ error: null
121
+ });
122
+ const load = useCallback(async () => {
123
+ setState({ data: null, loading: true, error: null });
124
+ try {
125
+ const data = await fn();
126
+ setState({ data, loading: false, error: null });
127
+ } catch (err) {
128
+ setState({ data: null, loading: false, error: err.message });
129
+ }
130
+ }, deps);
131
+ useEffect(() => {
132
+ load();
133
+ }, [load]);
134
+ return { ...state, reload: load };
135
+ }
136
+ function useRemotes() {
137
+ return useAsync(() => getRemotes(), []);
138
+ }
139
+ function useBranches(remote) {
140
+ return useAsync(
141
+ () => remote ? getRemoteBranches(remote) : Promise.resolve([]),
142
+ [remote]
143
+ );
144
+ }
145
+ function useCommits(remote, branch, count = 30) {
146
+ return useAsync(
147
+ () => remote && branch ? getCommits(remote, branch, count) : Promise.resolve([]),
148
+ [remote, branch, count]
149
+ );
150
+ }
151
+ function useCommitStat(hashes) {
152
+ const [stat, setStat] = useState("");
153
+ const [loading, setLoading] = useState(false);
154
+ useEffect(() => {
155
+ if (hashes.length === 0) {
156
+ setStat("");
157
+ return;
158
+ }
159
+ setLoading(true);
160
+ getMultiCommitStat(hashes).then((s) => {
161
+ setStat(s);
162
+ setLoading(false);
163
+ }).catch(() => {
164
+ setStat("(\u83B7\u53D6\u5931\u8D25)");
165
+ setLoading(false);
166
+ });
167
+ }, [hashes.join(",")]);
168
+ return { stat, loading };
169
+ }
170
+
171
+ // src/components/remote-select.tsx
172
+ import { jsx, jsxs } from "react/jsx-runtime";
173
+ function RemoteSelect({ onSelect }) {
174
+ const { data: remotes, loading, error } = useRemotes();
175
+ if (loading) {
176
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Spinner, { label: "\u6B63\u5728\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5217\u8868..." }) });
177
+ }
178
+ if (error) {
179
+ return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
180
+ "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
181
+ error
182
+ ] });
183
+ }
184
+ if (!remotes || remotes.length === 0) {
185
+ return /* @__PURE__ */ jsx(Text, { color: "red", children: "\u672A\u627E\u5230\u4EFB\u4F55\u8FDC\u7A0B\u4ED3\u5E93\uFF0C\u8BF7\u5148 git remote add" });
186
+ }
187
+ const options = remotes.map((r) => ({
188
+ label: `${r.name} ${r.fetchUrl}`,
189
+ value: r.name
190
+ }));
191
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
192
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[1/5] \u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
193
+ /* @__PURE__ */ jsx(Select, { options, onChange: onSelect })
194
+ ] });
195
+ }
196
+
197
+ // src/components/branch-select.tsx
198
+ import { useState as useState2, useMemo } from "react";
199
+ import { Box as Box2, Text as Text2 } from "ink";
200
+ import { Select as Select2, Spinner as Spinner2, TextInput } from "@inkjs/ui";
201
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
202
+ function BranchSelect({ remote, onSelect }) {
203
+ const { data: branches, loading, error } = useBranches(remote);
204
+ const [filter, setFilter] = useState2("");
205
+ const filteredOptions = useMemo(() => {
206
+ if (!branches) return [];
207
+ const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
208
+ return filtered.map((b) => ({ label: b, value: b }));
209
+ }, [branches, filter]);
210
+ if (loading) {
211
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Spinner2, { label: `\u6B63\u5728\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` }) });
212
+ }
213
+ if (error) {
214
+ return /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
215
+ "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
216
+ error
217
+ ] });
218
+ }
219
+ if (!branches || branches.length === 0) {
220
+ return /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
221
+ }
222
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
223
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
224
+ "[2/5] \u9009\u62E9\u5206\u652F (",
225
+ remote,
226
+ ")"
227
+ ] }),
228
+ /* @__PURE__ */ jsxs2(Box2, { children: [
229
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u641C\u7D22: " }),
230
+ /* @__PURE__ */ jsx2(
231
+ TextInput,
232
+ {
233
+ placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4\u5206\u652F...",
234
+ onChange: setFilter
235
+ }
236
+ )
237
+ ] }),
238
+ /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
239
+ "\u5171 ",
240
+ branches.length,
241
+ " \u4E2A\u5206\u652F",
242
+ filter ? `\uFF0C\u5339\u914D ${filteredOptions.length} \u4E2A` : ""
243
+ ] }),
244
+ filteredOptions.length > 0 ? /* @__PURE__ */ jsx2(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u65E0\u5339\u914D\u5206\u652F" })
245
+ ] });
246
+ }
247
+
248
+ // src/components/commit-list.tsx
249
+ import { useState as useState3 } from "react";
250
+ import { Box as Box3, Text as Text3 } from "ink";
251
+ import { MultiSelect, Spinner as Spinner3 } from "@inkjs/ui";
252
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
253
+ function CommitList({ remote, branch, onSelect }) {
254
+ const { data: commits, loading, error } = useCommits(remote, branch, 30);
255
+ const [selectedHashes, setSelectedHashes] = useState3([]);
256
+ const { stat, loading: statLoading } = useCommitStat(selectedHashes);
257
+ if (loading) {
258
+ return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Spinner3, { label: `\u6B63\u5728\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` }) });
259
+ }
260
+ if (error) {
261
+ return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
262
+ "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
263
+ error
264
+ ] });
265
+ }
266
+ if (!commits || commits.length === 0) {
267
+ return /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u8BE5\u5206\u652F\u6CA1\u6709 commit" });
268
+ }
269
+ const options = commits.map((c) => ({
270
+ label: `${c.shortHash} ${c.message} (${c.author}, ${c.date})`,
271
+ value: c.hash
272
+ }));
273
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
274
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "[3/5] \u9009\u62E9\u8981\u540C\u6B65\u7684 commit (Space \u9009\u62E9, Enter \u786E\u8BA4)" }),
275
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
276
+ remote,
277
+ "/",
278
+ branch,
279
+ " \u6700\u8FD1 ",
280
+ commits.length,
281
+ " \u4E2A commit"
282
+ ] }),
283
+ /* @__PURE__ */ jsx3(
284
+ MultiSelect,
285
+ {
286
+ options,
287
+ onChange: setSelectedHashes,
288
+ onSubmit: (hashes) => {
289
+ if (hashes.length > 0) {
290
+ onSelect(hashes, commits);
291
+ }
292
+ }
293
+ }
294
+ ),
295
+ selectedHashes.length > 0 && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
296
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "yellow", children: [
297
+ "\u5DF2\u9009 ",
298
+ selectedHashes.length,
299
+ " \u4E2A commit \u2014 diff --stat \u9884\u89C8:"
300
+ ] }),
301
+ statLoading ? /* @__PURE__ */ jsx3(Spinner3, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx3(Text3, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" })
302
+ ] })
303
+ ] });
304
+ }
305
+
306
+ // src/components/confirm-panel.tsx
307
+ import { Box as Box4, Text as Text4, useInput } from "ink";
308
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
309
+ function ConfirmPanel({ commits, selectedHashes, onConfirm, onCancel }) {
310
+ useInput((input) => {
311
+ if (input === "y" || input === "Y") {
312
+ onConfirm();
313
+ } else if (input === "n" || input === "N" || input === "q") {
314
+ onCancel();
315
+ }
316
+ });
317
+ const selectedCommits = selectedHashes.map((hash) => commits.find((c) => c.hash === hash)).filter(Boolean);
318
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
319
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "[4/5] \u786E\u8BA4\u6267\u884C" }),
320
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
321
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
322
+ "\u5C06 cherry-pick --no-commit \u4EE5\u4E0B ",
323
+ selectedCommits.length,
324
+ " \u4E2A commit:"
325
+ ] }),
326
+ selectedCommits.map((c) => /* @__PURE__ */ jsxs4(Text4, { children: [
327
+ /* @__PURE__ */ jsxs4(Text4, { color: "green", children: [
328
+ " ",
329
+ c.shortHash
330
+ ] }),
331
+ /* @__PURE__ */ jsxs4(Text4, { children: [
332
+ " ",
333
+ c.message
334
+ ] }),
335
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
336
+ " (",
337
+ c.author,
338
+ ")"
339
+ ] })
340
+ ] }, c.hash))
341
+ ] }),
342
+ /* @__PURE__ */ jsxs4(Box4, { children: [
343
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u26A0 " }),
344
+ /* @__PURE__ */ jsx4(Text4, { children: "\u4F7F\u7528 --no-commit \u6A21\u5F0F\uFF0C\u6539\u52A8\u5C06\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit" })
345
+ ] }),
346
+ /* @__PURE__ */ jsxs4(Box4, { children: [
347
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
348
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: "[y]" }),
349
+ /* @__PURE__ */ jsx4(Text4, { children: " \u786E\u8BA4 / " }),
350
+ /* @__PURE__ */ jsx4(Text4, { color: "red", children: "[n]" }),
351
+ /* @__PURE__ */ jsx4(Text4, { children: " \u53D6\u6D88" })
352
+ ] })
353
+ ] });
354
+ }
355
+
356
+ // src/components/result-panel.tsx
357
+ import { useState as useState4, useEffect as useEffect3 } from "react";
358
+ import { Box as Box5, Text as Text5 } from "ink";
359
+ import { Spinner as Spinner4 } from "@inkjs/ui";
360
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
361
+ function ResultPanel({ selectedHashes, onDone }) {
362
+ const [phase, setPhase] = useState4("executing");
363
+ const [result, setResult] = useState4(null);
364
+ const [stagedStat, setStagedStat] = useState4("");
365
+ useEffect3(() => {
366
+ async function run() {
367
+ const res = await cherryPick(selectedHashes);
368
+ setResult(res);
369
+ if (res.success) {
370
+ const stat = await getStagedStat();
371
+ setStagedStat(stat);
372
+ setPhase("done");
373
+ } else {
374
+ setPhase("error");
375
+ }
376
+ }
377
+ run();
378
+ }, []);
379
+ if (phase === "executing") {
380
+ return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Spinner4, { label: `\u6B63\u5728\u6267\u884C cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` }) });
381
+ }
382
+ if (phase === "error" && result) {
383
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
384
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "[5/5] Cherry-pick \u9047\u5230\u51B2\u7A81" }),
385
+ result.conflictFiles && result.conflictFiles.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [
386
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "\u51B2\u7A81\u6587\u4EF6:" }),
387
+ result.conflictFiles.map((f) => /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
388
+ " ",
389
+ f
390
+ ] }, f))
391
+ ] }),
392
+ /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u8BF7\u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
393
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "\u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" })
394
+ ] });
395
+ }
396
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
397
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "green", children: "[5/5] \u540C\u6B65\u5B8C\u6210!" }),
398
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
399
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "\u6682\u5B58\u533A\u53D8\u66F4\u6982\u89C8 (git diff --cached --stat):" }),
400
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" })
401
+ ] }),
402
+ /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
403
+ /* @__PURE__ */ jsx5(Text5, { children: "\u8BF7\u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
404
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " git diff --cached # \u67E5\u770B\u8BE6\u7EC6 diff" }),
405
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: ' git commit -m "\u540C\u6B65 commit" # \u63D0\u4EA4' }),
406
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " git reset HEAD # \u6216\u653E\u5F03\u6240\u6709\u6539\u52A8" })
407
+ ] });
408
+ }
409
+
410
+ // src/app.tsx
411
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
412
+ function App() {
413
+ const [step, setStep] = useState5("remote");
414
+ const [remote, setRemote] = useState5("");
415
+ const [branch, setBranch] = useState5("");
416
+ const [selectedHashes, setSelectedHashes] = useState5([]);
417
+ const [commits, setCommits] = useState5([]);
418
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
419
+ /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
420
+ /* @__PURE__ */ jsx6(Text6, { bold: true, inverse: true, color: "white", children: " git-sync-tui " }),
421
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
422
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "\u4EA4\u4E92\u5F0F commit \u540C\u6B65\u5DE5\u5177 (cherry-pick --no-commit)" })
423
+ ] }),
424
+ step === "remote" && /* @__PURE__ */ jsx6(
425
+ RemoteSelect,
426
+ {
427
+ onSelect: (r) => {
428
+ setRemote(r);
429
+ setStep("branch");
430
+ }
431
+ }
432
+ ),
433
+ step === "branch" && /* @__PURE__ */ jsx6(
434
+ BranchSelect,
435
+ {
436
+ remote,
437
+ onSelect: (b) => {
438
+ setBranch(b);
439
+ setStep("commits");
440
+ }
441
+ }
442
+ ),
443
+ step === "commits" && /* @__PURE__ */ jsx6(
444
+ CommitList,
445
+ {
446
+ remote,
447
+ branch,
448
+ onSelect: (hashes, loadedCommits) => {
449
+ setSelectedHashes(hashes);
450
+ setCommits(loadedCommits);
451
+ setStep("confirm");
452
+ }
453
+ }
454
+ ),
455
+ step === "confirm" && /* @__PURE__ */ jsx6(
456
+ ConfirmPanel,
457
+ {
458
+ commits,
459
+ selectedHashes,
460
+ onConfirm: () => setStep("result"),
461
+ onCancel: () => setStep("commits")
462
+ }
463
+ ),
464
+ step === "result" && /* @__PURE__ */ jsx6(
465
+ ResultPanel,
466
+ {
467
+ selectedHashes,
468
+ onDone: () => process.exit(0)
469
+ }
470
+ )
471
+ ] });
472
+ }
473
+
474
+ // src/cli.tsx
475
+ import { jsx as jsx7 } from "react/jsx-runtime";
476
+ var cli = meow(
477
+ `
478
+ \u7528\u6CD5
479
+ $ git-sync-tui
480
+
481
+ \u9009\u9879
482
+ --help \u663E\u793A\u5E2E\u52A9
483
+ --version \u663E\u793A\u7248\u672C
484
+
485
+ \u8BF4\u660E
486
+ \u4EA4\u4E92\u5F0F TUI \u5DE5\u5177\uFF0C\u4ECE\u8FDC\u7A0B\u5206\u652F\u6311\u9009 commit \u540C\u6B65\u5230\u5F53\u524D\u5206\u652F\u3002
487
+ \u4F7F\u7528 cherry-pick --no-commit \u6A21\u5F0F\uFF0C\u540C\u6B65\u540E\u53EF\u5BA1\u67E5\u518D\u63D0\u4EA4\u3002
488
+
489
+ \u5FEB\u6377\u952E
490
+ Space \u9009\u62E9/\u53D6\u6D88 commit
491
+ Enter \u786E\u8BA4\u9009\u62E9
492
+ \u2191/\u2193 \u5BFC\u822A
493
+ y/n \u786E\u8BA4/\u53D6\u6D88\u6267\u884C
494
+ `,
495
+ {
496
+ importMeta: import.meta
497
+ }
498
+ );
499
+ render(/* @__PURE__ */ jsx7(App, {}));
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "git-sync-tui",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Interactive TUI tool for cross-repo git commit synchronization (cherry-pick --no-commit)",
6
+ "author": "KiWi233333",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/KiWi233333/git-sync-tui.git"
10
+ },
11
+ "homepage": "https://github.com/KiWi233333/git-sync-tui#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/KiWi233333/git-sync-tui/issues"
14
+ },
15
+ "bin": {
16
+ "git-sync-tui": "dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "scripts": {
25
+ "start": "tsx src/cli.tsx",
26
+ "dev": "tsx watch src/cli.tsx",
27
+ "build": "tsup",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "git",
32
+ "sync",
33
+ "cherry-pick",
34
+ "tui",
35
+ "cli",
36
+ "ink",
37
+ "interactive",
38
+ "no-commit"
39
+ ],
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@inkjs/ui": "^2.0.0",
43
+ "ink": "^5.1.0",
44
+ "meow": "^13.0.0",
45
+ "react": "^18.3.1",
46
+ "simple-git": "^3.27.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.5.0",
50
+ "@types/react": "^18.3.0",
51
+ "tsup": "^8.5.1",
52
+ "tsx": "^4.19.0",
53
+ "typescript": "^5.7.0"
54
+ }
55
+ }