git-sync-tui 0.1.2 → 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 +852 -369
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,17 +1,113 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli.tsx
4
10
  import { render } from "ink";
5
11
  import meow from "meow";
6
12
 
7
13
  // src/app.tsx
8
- import { useState as useState6, useEffect as useEffect3, useRef as useRef2, useCallback as useCallback2 } from "react";
9
- import { Box as Box7, Text as Text7, useApp } from "ink";
10
- import { Spinner as Spinner5 } from "@inkjs/ui";
14
+ import { useState as useState7, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback2 } from "react";
15
+ import { Box as Box9, useApp } from "ink";
16
+ import { Spinner as Spinner6 } from "@inkjs/ui";
11
17
 
12
- // src/components/stash-prompt.tsx
13
- import { Box, Text, useInput } from "ink";
18
+ // src/components/ui.tsx
19
+ import React from "react";
20
+ import { Box, Text } from "ink";
14
21
  import { jsx, jsxs } from "react/jsx-runtime";
22
+ var STEP_LABELS = ["Remote", "Branch", "Commits", "Confirm", "Sync"];
23
+ function StepProgress({ current }) {
24
+ return /* @__PURE__ */ jsx(Box, { children: STEP_LABELS.map((label, i) => {
25
+ const step = i + 1;
26
+ const isActive = step === current;
27
+ const isDone = step < current;
28
+ const isLast = i === STEP_LABELS.length - 1;
29
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
30
+ /* @__PURE__ */ jsxs(Text, { color: isActive ? "cyan" : isDone ? "green" : "gray", dimColor: !isActive && !isDone, children: [
31
+ isDone ? "\u25CF" : isActive ? "\u25C6" : "\u25CB",
32
+ " ",
33
+ isActive ? /* @__PURE__ */ jsx(Text, { bold: true, children: label }) : label
34
+ ] }),
35
+ !isLast && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " \u2500 " })
36
+ ] }, label);
37
+ }) });
38
+ }
39
+ function SectionHeader({ title, subtitle }) {
40
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
41
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
42
+ "\u25B8 ",
43
+ title
44
+ ] }),
45
+ subtitle && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
46
+ " ",
47
+ subtitle
48
+ ] })
49
+ ] });
50
+ }
51
+ function KeyHints({ hints }) {
52
+ 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
+ " ",
58
+ label
59
+ ] })
60
+ ] }, key)) });
61
+ }
62
+ function InlineKeys({ hints }) {
63
+ return /* @__PURE__ */ jsx(Box, { gap: 1, children: hints.map(({ key, label }, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
64
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
65
+ "[",
66
+ key,
67
+ "]"
68
+ ] }),
69
+ /* @__PURE__ */ jsxs(Text, { children: [
70
+ " ",
71
+ label
72
+ ] }),
73
+ i < hints.length - 1 && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " / " })
74
+ ] }, key)) });
75
+ }
76
+ var PANEL_CONFIG = {
77
+ info: { icon: "\u25C6", color: "cyan", borderColor: "cyan" },
78
+ warn: { icon: "\u25B2", color: "yellow", borderColor: "yellow" },
79
+ error: { icon: "\u2716", color: "red", borderColor: "red" },
80
+ success: { icon: "\u2714", color: "green", borderColor: "green" }
81
+ };
82
+ function StatusPanel({ type, title, children }) {
83
+ const { icon, color, borderColor } = PANEL_CONFIG[type];
84
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor, paddingX: 1, children: [
85
+ /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
86
+ icon,
87
+ " ",
88
+ title
89
+ ] }),
90
+ children
91
+ ] });
92
+ }
93
+ function AppHeader({ step, stashed }) {
94
+ 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" })
100
+ ] }),
101
+ /* @__PURE__ */ jsxs(Box, { children: [
102
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " " }),
103
+ /* @__PURE__ */ jsx(StepProgress, { current: step })
104
+ ] })
105
+ ] });
106
+ }
107
+
108
+ // src/components/stash-prompt.tsx
109
+ import { Box as Box2, Text as Text2, useInput } from "ink";
110
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
15
111
  function StashPrompt({ onConfirm, onSkip }) {
16
112
  useInput((input) => {
17
113
  if (input === "y" || input === "Y") {
@@ -20,31 +116,27 @@ function StashPrompt({ onConfirm, onSkip }) {
20
116
  onSkip();
21
117
  }
22
118
  });
23
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
24
- /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [
25
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u68C0\u6D4B\u5230\u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u7684\u53D8\u66F4" }),
26
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "Cherry-pick \u64CD\u4F5C\u53EF\u80FD\u4F1A\u4E0E\u672A\u63D0\u4EA4\u7684\u5185\u5BB9\u51B2\u7A81" })
27
- ] }),
28
- /* @__PURE__ */ jsxs(Box, { children: [
29
- /* @__PURE__ */ jsx(Text, { bold: true, children: "\u662F\u5426\u81EA\u52A8 stash \u4FDD\u5B58\u5F53\u524D\u53D8\u66F4? " }),
30
- /* @__PURE__ */ jsx(Text, { color: "green", children: "[y]" }),
31
- /* @__PURE__ */ jsx(Text, { children: " \u662F / " }),
32
- /* @__PURE__ */ jsx(Text, { color: "red", children: "[n]" }),
33
- /* @__PURE__ */ jsx(Text, { children: " \u5426\uFF0C\u7EE7\u7EED\u64CD\u4F5C" })
119
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
120
+ /* @__PURE__ */ jsx2(StatusPanel, { type: "warn", title: "\u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u7684\u53D8\u66F4", children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: " Cherry-pick \u64CD\u4F5C\u53EF\u80FD\u4F1A\u4E0E\u672A\u63D0\u4EA4\u7684\u5185\u5BB9\u51B2\u7A81" }) }),
121
+ /* @__PURE__ */ jsxs2(Box2, { children: [
122
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "\u81EA\u52A8 stash \u4FDD\u5B58\u5F53\u524D\u53D8\u66F4? " }),
123
+ /* @__PURE__ */ jsx2(InlineKeys, { hints: [
124
+ { key: "y", label: "\u662F" },
125
+ { key: "n", label: "\u5426\uFF0C\u7EE7\u7EED" }
126
+ ] })
34
127
  ] })
35
128
  ] });
36
129
  }
37
130
 
38
- // src/components/remote-select.tsx
39
- import { useState as useState2 } from "react";
40
- import { Box as Box2, Text as Text2 } from "ink";
41
- import { Select, Spinner, TextInput } from "@inkjs/ui";
42
-
43
- // src/hooks/use-git.ts
44
- import { useState, useEffect, useCallback } from "react";
131
+ // src/components/stash-recovery.tsx
132
+ import { useState, useEffect } from "react";
133
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
134
+ import { Spinner } from "@inkjs/ui";
45
135
 
46
136
  // src/utils/git.ts
47
137
  import simpleGit from "simple-git";
138
+ import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
139
+ import { join } from "path";
48
140
  var gitInstance = null;
49
141
  function getGit(cwd) {
50
142
  if (!gitInstance || cwd) {
@@ -64,38 +156,53 @@ async function addRemote(name, url) {
64
156
  const git = getGit();
65
157
  await git.addRemote(name, url);
66
158
  }
67
- async function getRemoteBranches(remote) {
159
+ async function getRemoteBranches(remote2) {
68
160
  const git = getGit();
161
+ let fetchOk = false;
69
162
  try {
70
- await git.fetch(remote);
163
+ await git.fetch(remote2);
164
+ fetchOk = true;
71
165
  } catch {
72
166
  }
73
167
  const result = await git.branch(["-r"]);
74
- const prefix = `${remote}/`;
75
- 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
+ }
76
181
  }
77
- async function getCommits(remote, branch, count = 30) {
182
+ async function getCommits(remote2, branch2, count2 = 30) {
78
183
  const git = getGit();
79
- const ref = `${remote}/${branch}`;
80
- const log = await git.log({
81
- from: void 0,
82
- to: ref,
83
- maxCount: count,
84
- format: {
85
- hash: "%H",
86
- shortHash: "%h",
87
- message: "%s",
88
- author: "%an",
89
- 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`);
90
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 || "" };
91
205
  });
92
- return log.all.map((entry) => ({
93
- hash: entry.hash,
94
- shortHash: entry.hash.substring(0, 7),
95
- message: entry.message || "",
96
- author: entry.author || "",
97
- date: entry.date || ""
98
- }));
99
206
  }
100
207
  async function getMultiCommitStat(hashes) {
101
208
  if (hashes.length === 0) return "";
@@ -183,10 +290,115 @@ async function stashPop() {
183
290
  return false;
184
291
  }
185
292
  }
293
+ var STASH_GUARD_FILE = "git-sync-tui-stash-guard";
294
+ async function getGitDir() {
295
+ const git = getGit();
296
+ const dir = await git.revparse(["--git-dir"]);
297
+ return dir.trim();
298
+ }
299
+ async function writeStashGuard() {
300
+ try {
301
+ const gitDir = await getGitDir();
302
+ writeFileSync(join(gitDir, STASH_GUARD_FILE), (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
303
+ } catch {
304
+ }
305
+ }
306
+ async function removeStashGuard() {
307
+ try {
308
+ const gitDir = await getGitDir();
309
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
310
+ if (existsSync(guardPath)) {
311
+ unlinkSync(guardPath);
312
+ }
313
+ } catch {
314
+ }
315
+ }
316
+ function removeStashGuardSync() {
317
+ try {
318
+ const { execSync: execSync2 } = __require("child_process");
319
+ const gitDir = String(execSync2("git rev-parse --git-dir", { encoding: "utf-8" })).trim();
320
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
321
+ if (existsSync(guardPath)) {
322
+ unlinkSync(guardPath);
323
+ }
324
+ } catch {
325
+ }
326
+ }
327
+ async function checkStashGuard() {
328
+ try {
329
+ const gitDir = await getGitDir();
330
+ const guardPath = join(gitDir, STASH_GUARD_FILE);
331
+ if (existsSync(guardPath)) {
332
+ const timestamp = readFileSync(guardPath, "utf-8").trim();
333
+ return { exists: true, timestamp };
334
+ }
335
+ } catch {
336
+ }
337
+ return { exists: false };
338
+ }
339
+ async function findStashEntry() {
340
+ const git = getGit();
341
+ try {
342
+ const result = await git.stash(["list"]);
343
+ const lines = result.trim().split("\n");
344
+ for (const line of lines) {
345
+ if (line.includes("Auto-stash by git-sync-tui")) {
346
+ return line;
347
+ }
348
+ }
349
+ } catch {
350
+ }
351
+ return null;
352
+ }
353
+
354
+ // src/components/stash-recovery.tsx
355
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
356
+ function StashRecovery({ timestamp, onRecover, onSkip }) {
357
+ const [stashEntry, setStashEntry] = useState(void 0);
358
+ useEffect(() => {
359
+ findStashEntry().then(setStashEntry);
360
+ }, []);
361
+ useInput2((input) => {
362
+ if (input === "y" || input === "Y") {
363
+ onRecover();
364
+ } else if (input === "n" || input === "N") {
365
+ onSkip();
366
+ }
367
+ });
368
+ if (stashEntry === void 0) {
369
+ return /* @__PURE__ */ jsx3(Spinner, { label: "\u68C0\u67E5 stash \u8BB0\u5F55..." });
370
+ }
371
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
372
+ /* @__PURE__ */ jsxs3(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230\u4E0A\u6B21\u8FD0\u884C\u4E2D\u65AD", children: [
373
+ timestamp && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
374
+ " \u4E2D\u65AD\u65F6\u95F4: ",
375
+ timestamp
376
+ ] }),
377
+ stashEntry && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
378
+ " Stash: ",
379
+ stashEntry
380
+ ] }),
381
+ !stashEntry && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " \u672A\u627E\u5230\u5BF9\u5E94\u7684 stash \u6761\u76EE\uFF08\u53EF\u80FD\u5DF2\u624B\u52A8\u6062\u590D\uFF09" })
382
+ ] }),
383
+ /* @__PURE__ */ jsxs3(Box3, { children: [
384
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: stashEntry ? "\u6062\u590D stash? " : "\u6E05\u9664\u4E2D\u65AD\u6807\u8BB0? " }),
385
+ /* @__PURE__ */ jsx3(InlineKeys, { hints: [
386
+ { key: "y", label: "\u662F" },
387
+ { key: "n", label: "\u8DF3\u8FC7" }
388
+ ] })
389
+ ] })
390
+ ] });
391
+ }
392
+
393
+ // src/components/remote-select.tsx
394
+ import { useState as useState3 } from "react";
395
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
396
+ import { Select, Spinner as Spinner2, TextInput } from "@inkjs/ui";
186
397
 
187
398
  // src/hooks/use-git.ts
399
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
188
400
  function useAsync(fn, deps = []) {
189
- const [state, setState] = useState({
401
+ const [state, setState] = useState2({
190
402
  data: null,
191
403
  loading: true,
192
404
  error: null
@@ -200,7 +412,7 @@ function useAsync(fn, deps = []) {
200
412
  setState({ data: null, loading: false, error: err.message });
201
413
  }
202
414
  }, deps);
203
- useEffect(() => {
415
+ useEffect2(() => {
204
416
  load();
205
417
  }, [load]);
206
418
  return { ...state, reload: load };
@@ -208,22 +420,22 @@ function useAsync(fn, deps = []) {
208
420
  function useRemotes() {
209
421
  return useAsync(() => getRemotes(), []);
210
422
  }
211
- function useBranches(remote) {
423
+ function useBranches(remote2) {
212
424
  return useAsync(
213
- () => remote ? getRemoteBranches(remote) : Promise.resolve([]),
214
- [remote]
425
+ () => remote2 ? getRemoteBranches(remote2) : Promise.resolve([]),
426
+ [remote2]
215
427
  );
216
428
  }
217
- function useCommits(remote, branch, count = 30) {
429
+ function useCommits(remote2, branch2, count2 = 30) {
218
430
  return useAsync(
219
- () => remote && branch ? getCommits(remote, branch, count) : Promise.resolve([]),
220
- [remote, branch, count]
431
+ () => remote2 && branch2 ? getCommits(remote2, branch2, count2) : Promise.resolve([]),
432
+ [remote2, branch2, count2]
221
433
  );
222
434
  }
223
435
  function useCommitStat(hashes) {
224
- const [stat, setStat] = useState("");
225
- const [loading, setLoading] = useState(false);
226
- useEffect(() => {
436
+ const [stat, setStat] = useState2("");
437
+ const [loading, setLoading] = useState2(false);
438
+ useEffect2(() => {
227
439
  if (hashes.length === 0) {
228
440
  setStat("");
229
441
  return;
@@ -241,31 +453,50 @@ function useCommitStat(hashes) {
241
453
  }
242
454
 
243
455
  // src/components/remote-select.tsx
244
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
245
- function RemoteSelect({ onSelect }) {
246
- const { data: remotes, loading, error, reload } = useRemotes();
247
- const [phase, setPhase] = useState2("list");
248
- const [customUrl, setCustomUrl] = useState2("");
249
- const [addError, setAddError] = useState2(null);
456
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
457
+ function extractRemoteName(url) {
458
+ const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
459
+ const lastSegment = trimmed.split(/[/\\:]+/).filter(Boolean).pop() || "";
460
+ return lastSegment;
461
+ }
462
+ function RemoteSelect({ onSelect, onBack }) {
463
+ const { data: remotes, loading, error: error2, reload } = useRemotes();
464
+ const [phase, setPhase] = useState3("list");
465
+ const [customUrl, setCustomUrl] = useState3("");
466
+ const [addError, setAddError] = useState3(null);
467
+ useInput3((_input, key) => {
468
+ if (key.escape) {
469
+ if (phase === "input-name") {
470
+ setPhase("input-url");
471
+ } else if (phase === "input-url") {
472
+ setPhase("list");
473
+ } else if (phase === "list") {
474
+ onBack?.();
475
+ }
476
+ }
477
+ });
250
478
  if (loading) {
251
- return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Spinner, { label: "\u6B63\u5728\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5217\u8868..." }) });
479
+ return /* @__PURE__ */ jsx4(Spinner2, { label: "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93..." });
252
480
  }
253
- if (error) {
254
- return /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
255
- "\u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
256
- error
481
+ if (error2) {
482
+ return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
483
+ "\u2716 \u83B7\u53D6\u8FDC\u7A0B\u4ED3\u5E93\u5931\u8D25: ",
484
+ error2
257
485
  ] });
258
486
  }
259
487
  if (phase === "adding") {
260
- return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Spinner, { label: "\u6B63\u5728\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." }) });
488
+ return /* @__PURE__ */ jsx4(Spinner2, { label: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93..." });
261
489
  }
262
490
  if (phase === "input-url") {
263
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
264
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "[1/5] \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93" }),
265
- addError && /* @__PURE__ */ jsx2(Text2, { color: "red", children: addError }),
266
- /* @__PURE__ */ jsxs2(Box2, { children: [
267
- /* @__PURE__ */ jsx2(Text2, { children: "\u4ED3\u5E93\u5730\u5740: " }),
268
- /* @__PURE__ */ jsx2(
491
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
492
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93" }),
493
+ addError && /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
494
+ "\u2716 ",
495
+ addError
496
+ ] }),
497
+ /* @__PURE__ */ jsxs4(Box4, { children: [
498
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "URL \u25B8 " }),
499
+ /* @__PURE__ */ jsx4(
269
500
  TextInput,
270
501
  {
271
502
  placeholder: "https://github.com/user/repo.git",
@@ -280,23 +511,24 @@ function RemoteSelect({ onSelect }) {
280
511
  }
281
512
  )
282
513
  ] }),
283
- /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: "\u652F\u6301 HTTPS / SSH \u5730\u5740" })
514
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " \u652F\u6301 HTTPS / SSH \u5730\u5740" })
284
515
  ] });
285
516
  }
286
517
  if (phase === "input-name") {
287
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
288
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "[1/5] \u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93" }),
289
- /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
290
- "\u5730\u5740: ",
291
- customUrl
518
+ const defaultName = extractRemoteName(customUrl);
519
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
520
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u6DFB\u52A0\u8FDC\u7A0B\u4ED3\u5E93", subtitle: customUrl }),
521
+ addError && /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
522
+ "\u2716 ",
523
+ addError
292
524
  ] }),
293
- addError && /* @__PURE__ */ jsx2(Text2, { color: "red", children: addError }),
294
- /* @__PURE__ */ jsxs2(Box2, { children: [
295
- /* @__PURE__ */ jsx2(Text2, { children: "\u8FDC\u7A0B\u540D\u79F0: " }),
296
- /* @__PURE__ */ jsx2(
525
+ /* @__PURE__ */ jsxs4(Box4, { children: [
526
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u540D\u79F0 \u25B8 " }),
527
+ /* @__PURE__ */ jsx4(
297
528
  TextInput,
298
529
  {
299
- placeholder: "upstream",
530
+ placeholder: defaultName || "upstream",
531
+ defaultValue: defaultName,
300
532
  onSubmit: async (name) => {
301
533
  const remoteName = name.trim();
302
534
  if (!remoteName) {
@@ -333,9 +565,9 @@ function RemoteSelect({ onSelect }) {
333
565
  value: "__add_custom__"
334
566
  }
335
567
  ];
336
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
337
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "[1/5] \u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
338
- /* @__PURE__ */ jsx2(
568
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
569
+ /* @__PURE__ */ jsx4(SectionHeader, { title: "\u9009\u62E9\u8FDC\u7A0B\u4ED3\u5E93" }),
570
+ /* @__PURE__ */ jsx4(
339
571
  Select,
340
572
  {
341
573
  options,
@@ -352,73 +584,70 @@ function RemoteSelect({ onSelect }) {
352
584
  }
353
585
 
354
586
  // src/components/branch-select.tsx
355
- import { useState as useState3, useMemo } from "react";
356
- import { Box as Box3, Text as Text3 } from "ink";
357
- import { Select as Select2, Spinner as Spinner2, TextInput as TextInput2 } from "@inkjs/ui";
358
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
359
- function BranchSelect({ remote, onSelect }) {
360
- const { data: branches, loading, error } = useBranches(remote);
361
- const [filter, setFilter] = useState3("");
587
+ import { useState as useState4, useMemo } from "react";
588
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
589
+ import { Select as Select2, Spinner as Spinner3, TextInput as TextInput2 } from "@inkjs/ui";
590
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
591
+ function BranchSelect({ remote: remote2, onSelect, onBack }) {
592
+ const { data: branches, loading, error: error2 } = useBranches(remote2);
593
+ const [filter, setFilter] = useState4("");
594
+ useInput4((_input, key) => {
595
+ if (key.escape) onBack?.();
596
+ });
362
597
  const filteredOptions = useMemo(() => {
363
598
  if (!branches) return [];
364
599
  const filtered = filter ? branches.filter((b) => b.toLowerCase().includes(filter.toLowerCase())) : branches;
365
600
  return filtered.map((b) => ({ label: b, value: b }));
366
601
  }, [branches, filter]);
367
602
  if (loading) {
368
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Spinner2, { label: `\u6B63\u5728\u83B7\u53D6 ${remote} \u7684\u5206\u652F\u5217\u8868...` }) });
603
+ return /* @__PURE__ */ jsx5(Spinner3, { label: `\u83B7\u53D6 ${remote2} \u7684\u5206\u652F\u5217\u8868...` });
369
604
  }
370
- if (error) {
371
- return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
372
- "\u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
373
- error
605
+ if (error2) {
606
+ return /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
607
+ "\u2716 \u83B7\u53D6\u5206\u652F\u5217\u8868\u5931\u8D25: ",
608
+ error2
374
609
  ] });
375
610
  }
376
611
  if (!branches || branches.length === 0) {
377
- return /* @__PURE__ */ jsx3(Text3, { color: "red", children: "\u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
612
+ return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2716 \u672A\u627E\u5230\u8FDC\u7A0B\u5206\u652F" });
378
613
  }
379
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
380
- /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
381
- "[2/5] \u9009\u62E9\u5206\u652F (",
382
- remote,
383
- ")"
384
- ] }),
385
- /* @__PURE__ */ jsxs3(Box3, { children: [
386
- /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u641C\u7D22: " }),
387
- /* @__PURE__ */ jsx3(
614
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
615
+ /* @__PURE__ */ jsx5(SectionHeader, { title: `\u9009\u62E9\u5206\u652F`, subtitle: `${remote2} \xB7 ${branches.length} \u4E2A\u5206\u652F` }),
616
+ /* @__PURE__ */ jsxs5(Box5, { children: [
617
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "/ " }),
618
+ /* @__PURE__ */ jsx5(
388
619
  TextInput2,
389
620
  {
390
- placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4\u5206\u652F...",
621
+ placeholder: "\u8F93\u5165\u5173\u952E\u5B57\u8FC7\u6EE4...",
391
622
  onChange: setFilter
392
623
  }
393
- )
394
- ] }),
395
- /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
396
- "\u5171 ",
397
- branches.length,
398
- " \u4E2A\u5206\u652F",
399
- filter ? `\uFF0C\u5339\u914D ${filteredOptions.length} \u4E2A` : ""
624
+ ),
625
+ filter && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
626
+ " \xB7 \u5339\u914D ",
627
+ filteredOptions.length
628
+ ] })
400
629
  ] }),
401
- filteredOptions.length > 0 ? /* @__PURE__ */ jsx3(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u65E0\u5339\u914D\u5206\u652F" })
630
+ filteredOptions.length > 0 ? /* @__PURE__ */ jsx5(Select2, { options: filteredOptions, onChange: onSelect }) : /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B2 \u65E0\u5339\u914D\u5206\u652F" })
402
631
  ] });
403
632
  }
404
633
 
405
634
  // src/components/commit-list.tsx
406
- import { useState as useState4, useMemo as useMemo2, useRef } from "react";
407
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
408
- import { Spinner as Spinner3 } from "@inkjs/ui";
409
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
410
- function CommitList({ remote, branch, onSelect }) {
411
- const { data: commits, loading, error } = useCommits(remote, branch, 30);
412
- const [selectedIndex, setSelectedIndex] = useState4(0);
413
- const [selectedHashes, setSelectedHashes] = useState4(/* @__PURE__ */ new Set());
414
- const [shiftMode, setShiftMode] = useState4(false);
635
+ import { useState as useState5, useMemo as useMemo2, useRef } from "react";
636
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
637
+ import { Spinner as Spinner4 } from "@inkjs/ui";
638
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
639
+ function CommitList({ remote: remote2, branch: branch2, onSelect, onBack }) {
640
+ const { data: commits2, loading, error: error2 } = useCommits(remote2, branch2, 30);
641
+ const [selectedIndex, setSelectedIndex] = useState5(0);
642
+ const [selectedHashes, setSelectedHashes] = useState5(/* @__PURE__ */ new Set());
643
+ const [shiftMode, setShiftMode] = useState5(false);
415
644
  const anchorIndexRef = useRef(null);
416
645
  const selectedKey = useMemo2(() => Array.from(selectedHashes).sort().join(","), [selectedHashes]);
417
646
  const selectedArray = useMemo2(() => Array.from(selectedHashes), [selectedKey]);
418
647
  const { stat, loading: statLoading } = useCommitStat(selectedArray);
419
648
  const toggleCurrent = () => {
420
- if (!commits || commits.length === 0) return;
421
- const hash = commits[selectedIndex].hash;
649
+ if (!commits2 || commits2.length === 0) return;
650
+ const hash = commits2[selectedIndex].hash;
422
651
  setSelectedHashes((prev) => {
423
652
  const next = new Set(prev);
424
653
  if (next.has(hash)) {
@@ -432,51 +661,49 @@ function CommitList({ remote, branch, onSelect }) {
432
661
  });
433
662
  };
434
663
  const selectRange = (anchor, current) => {
435
- if (!commits) return;
664
+ if (!commits2) return;
436
665
  const start = Math.min(anchor, current);
437
666
  const end = Math.max(anchor, current);
438
667
  setSelectedHashes((prev) => {
439
668
  const next = new Set(prev);
440
669
  for (let i = start; i <= end; i++) {
441
- next.add(commits[i].hash);
670
+ next.add(commits2[i].hash);
442
671
  }
443
672
  return next;
444
673
  });
445
674
  };
446
675
  const toggleAll = () => {
447
- if (!commits || commits.length === 0) return;
676
+ if (!commits2 || commits2.length === 0) return;
448
677
  setSelectedHashes((prev) => {
449
- if (prev.size === commits.length) {
678
+ if (prev.size === commits2.length) {
450
679
  anchorIndexRef.current = null;
451
680
  return /* @__PURE__ */ new Set();
452
681
  }
453
- return new Set(commits.map((c) => c.hash));
682
+ return new Set(commits2.map((c) => c.hash));
454
683
  });
455
684
  };
456
685
  const invertSelection = () => {
457
- if (!commits || commits.length === 0) return;
686
+ if (!commits2 || commits2.length === 0) return;
458
687
  setSelectedHashes((prev) => {
459
688
  const next = /* @__PURE__ */ new Set();
460
- for (const c of commits) {
461
- if (!prev.has(c.hash)) {
462
- next.add(c.hash);
463
- }
689
+ for (const c of commits2) {
690
+ if (!prev.has(c.hash)) next.add(c.hash);
464
691
  }
465
692
  return next;
466
693
  });
467
694
  };
468
695
  const selectToCurrent = () => {
469
- if (!commits || commits.length === 0) return;
696
+ if (!commits2 || commits2.length === 0) return;
470
697
  setSelectedHashes((prev) => {
471
698
  const next = new Set(prev);
472
699
  for (let i = 0; i <= selectedIndex; i++) {
473
- next.add(commits[i].hash);
700
+ next.add(commits2[i].hash);
474
701
  }
475
702
  return next;
476
703
  });
477
704
  };
478
- useInput2((input, key) => {
479
- if (!commits || commits.length === 0) return;
705
+ useInput5((input, key) => {
706
+ if (!commits2 || commits2.length === 0) return;
480
707
  if (key.shift) {
481
708
  if (!shiftMode) {
482
709
  setShiftMode(true);
@@ -489,7 +716,7 @@ function CommitList({ remote, branch, onSelect }) {
489
716
  setSelectedIndex(newIndex);
490
717
  selectRange(anchorIndexRef.current, newIndex);
491
718
  } else if (key.downArrow) {
492
- const newIndex = Math.min(commits.length - 1, selectedIndex + 1);
719
+ const newIndex = Math.min(commits2.length - 1, selectedIndex + 1);
493
720
  setSelectedIndex(newIndex);
494
721
  selectRange(anchorIndexRef.current, newIndex);
495
722
  } else if (input === " ") {
@@ -501,13 +728,11 @@ function CommitList({ remote, branch, onSelect }) {
501
728
  }
502
729
  return;
503
730
  }
504
- if (shiftMode) {
505
- setShiftMode(false);
506
- }
731
+ if (shiftMode) setShiftMode(false);
507
732
  if (key.upArrow) {
508
733
  setSelectedIndex((prev) => Math.max(0, prev - 1));
509
734
  } else if (key.downArrow) {
510
- setSelectedIndex((prev) => Math.min(commits.length - 1, prev + 1));
735
+ setSelectedIndex((prev) => Math.min(commits2.length - 1, prev + 1));
511
736
  } else if (input === " ") {
512
737
  toggleCurrent();
513
738
  } else if (input === "a" || input === "A") {
@@ -516,127 +741,105 @@ function CommitList({ remote, branch, onSelect }) {
516
741
  invertSelection();
517
742
  } else if (input === "r" || input === "R") {
518
743
  selectToCurrent();
744
+ } else if (key.escape) {
745
+ onBack?.();
519
746
  } else if (key.return) {
520
747
  if (selectedHashes.size > 0) {
521
- onSelect(Array.from(selectedHashes), commits);
748
+ onSelect(Array.from(selectedHashes), commits2);
522
749
  }
523
750
  }
524
751
  });
525
752
  if (loading) {
526
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Spinner3, { label: `\u6B63\u5728\u83B7\u53D6 ${remote}/${branch} \u7684 commit \u5217\u8868...` }) });
753
+ return /* @__PURE__ */ jsx6(Spinner4, { label: `\u83B7\u53D6 ${remote2}/${branch2} \u7684 commit \u5217\u8868...` });
527
754
  }
528
- if (error) {
529
- return /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
530
- "\u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
531
- error
755
+ if (error2) {
756
+ return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
757
+ "\u2716 \u83B7\u53D6 commit \u5217\u8868\u5931\u8D25: ",
758
+ error2
532
759
  ] });
533
760
  }
534
- if (!commits || commits.length === 0) {
535
- return /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u8BE5\u5206\u652F\u6CA1\u6709 commit" });
761
+ if (!commits2 || commits2.length === 0) {
762
+ return /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u25B2 \u8BE5\u5206\u652F\u6CA1\u6709 commit" });
536
763
  }
537
764
  const visibleCount = 10;
538
- const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits.length - visibleCount));
539
- const visibleCommits = commits.slice(startIdx, startIdx + visibleCount);
540
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
541
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "[3/5] \u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
542
- /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
543
- remote,
544
- "/",
545
- branch,
546
- " \u6700\u8FD1 ",
547
- commits.length,
548
- " \u4E2A commit | \u5DF2\u9009 ",
549
- selectedHashes.size,
550
- " \u4E2A",
551
- shiftMode && /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: " | Shift \u6A21\u5F0F" })
765
+ const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), commits2.length - visibleCount));
766
+ const visibleCommits = commits2.slice(startIdx, startIdx + visibleCount);
767
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
768
+ /* @__PURE__ */ jsx6(SectionHeader, { title: "\u9009\u62E9\u8981\u540C\u6B65\u7684 commit" }),
769
+ /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
770
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
771
+ remote2,
772
+ "/",
773
+ branch2
774
+ ] }),
775
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
776
+ commits2.length,
777
+ " commits"
778
+ ] }),
779
+ /* @__PURE__ */ jsxs6(Text6, { color: selectedHashes.size > 0 ? "cyan" : "gray", bold: selectedHashes.size > 0, children: [
780
+ "\u5DF2\u9009 ",
781
+ selectedHashes.size
782
+ ] }),
783
+ shiftMode && /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "SHIFT" })
552
784
  ] }),
553
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
554
- startIdx > 0 && /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
785
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
786
+ startIdx > 0 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
555
787
  " \u2191 ",
556
788
  startIdx,
557
- " more..."
789
+ " more"
558
790
  ] }),
559
791
  visibleCommits.map((c, i) => {
560
792
  const actualIdx = startIdx + i;
561
793
  const isSelected = selectedHashes.has(c.hash);
562
794
  const isCursor = actualIdx === selectedIndex;
563
795
  const isAnchor = actualIdx === anchorIndexRef.current;
564
- return /* @__PURE__ */ jsxs4(Text4, { children: [
565
- /* @__PURE__ */ jsxs4(Text4, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
566
- isCursor ? "\u25B6 " : " ",
567
- isAnchor ? "\u2693 " : isSelected ? "\u25CF " : "\u25CB ",
568
- c.shortHash,
796
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
797
+ /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
798
+ isCursor ? "\u25B8 " : " ",
799
+ isAnchor ? "\u2693" : isSelected ? "\u25CF" : "\u25CB",
800
+ " "
801
+ ] }),
802
+ /* @__PURE__ */ jsx6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: "yellow", children: c.shortHash }),
803
+ /* @__PURE__ */ jsxs6(Text6, { backgroundColor: isCursor ? "blue" : void 0, color: isSelected ? "green" : "white", children: [
569
804
  " ",
570
805
  c.message
571
806
  ] }),
572
- /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
573
- " (",
807
+ /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
808
+ " ",
574
809
  c.author,
575
- ")"
810
+ " \xB7 ",
811
+ c.date
576
812
  ] })
577
813
  ] }, c.hash);
578
814
  }),
579
- startIdx + visibleCount < commits.length && /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
815
+ startIdx + visibleCount < commits2.length && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
580
816
  " \u2193 ",
581
- commits.length - startIdx - visibleCount,
582
- " more..."
817
+ commits2.length - startIdx - visibleCount,
818
+ " more"
583
819
  ] })
584
820
  ] }),
585
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 0, children: [
586
- /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
587
- /* @__PURE__ */ jsxs4(Text4, { children: [
588
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u2191/\u2193" }),
589
- " \u5BFC\u822A"
590
- ] }),
591
- /* @__PURE__ */ jsxs4(Text4, { children: [
592
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "Space" }),
593
- " \u9009\u62E9"
594
- ] }),
595
- /* @__PURE__ */ jsxs4(Text4, { children: [
596
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "a" }),
597
- " \u5168\u9009"
598
- ] }),
599
- /* @__PURE__ */ jsxs4(Text4, { children: [
600
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "i" }),
601
- " \u53CD\u9009"
602
- ] }),
603
- /* @__PURE__ */ jsxs4(Text4, { children: [
604
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "Enter" }),
605
- " \u786E\u8BA4"
606
- ] })
607
- ] }),
608
- /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
609
- /* @__PURE__ */ jsxs4(Text4, { children: [
610
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Shift+\u2191/\u2193" }),
611
- " \u8FDE\u7EED\u9009\u62E9"
612
- ] }),
613
- /* @__PURE__ */ jsxs4(Text4, { children: [
614
- /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Shift+Space" }),
615
- " \u8303\u56F4\u9009\u62E9"
616
- ] }),
617
- /* @__PURE__ */ jsxs4(Text4, { children: [
618
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "r" }),
619
- " \u9009\u81F3\u5F00\u5934"
620
- ] })
621
- ] })
821
+ /* @__PURE__ */ jsx6(KeyHints, { hints: [
822
+ { key: "\u2191\u2193", label: "\u5BFC\u822A" },
823
+ { key: "Space", label: "\u9009\u62E9" },
824
+ { key: "a", label: "\u5168\u9009" },
825
+ { key: "i", label: "\u53CD\u9009" },
826
+ { key: "r", label: "\u9009\u81F3\u5F00\u5934" },
827
+ { key: "Shift+\u2191\u2193", label: "\u8FDE\u9009" },
828
+ { key: "Enter", label: "\u786E\u8BA4" },
829
+ { key: "Esc", label: "\u8FD4\u56DE" }
622
830
  ] }),
623
- selectedHashes.size > 0 && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
624
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "yellow", children: [
625
- "\u5DF2\u9009 ",
626
- selectedHashes.size,
627
- " \u4E2A commit \u2014 diff --stat \u9884\u89C8:"
628
- ] }),
629
- statLoading ? /* @__PURE__ */ jsx4(Spinner3, { label: "\u52A0\u8F7D\u4E2D..." }) : /* @__PURE__ */ jsx4(Text4, { color: "gray", children: stat || "(\u65E0\u53D8\u66F4)" })
630
- ] })
831
+ 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)" }) })
631
832
  ] });
632
833
  }
633
834
 
634
835
  // src/components/confirm-panel.tsx
635
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
636
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
637
- function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
638
- useInput3((input) => {
639
- if (input === "y" || input === "Y") {
836
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
837
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
838
+ function ConfirmPanel({ commits: commits2, selectedHashes, hasMerge, useMainline, onToggleMainline, onConfirm, onCancel }) {
839
+ useInput6((input, key) => {
840
+ if (key.escape) {
841
+ onCancel();
842
+ } else if (input === "y" || input === "Y") {
640
843
  onConfirm();
641
844
  } else if (input === "n" || input === "N" || input === "q") {
642
845
  onCancel();
@@ -644,69 +847,58 @@ function ConfirmPanel({ commits, selectedHashes, hasMerge, useMainline, onToggle
644
847
  onToggleMainline();
645
848
  }
646
849
  });
647
- const selectedCommits = selectedHashes.map((hash) => commits.find((c) => c.hash === hash)).filter(Boolean);
648
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
649
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "[4/5] \u786E\u8BA4\u6267\u884C" }),
650
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
651
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
652
- "\u5C06 cherry-pick --no-commit \u4EE5\u4E0B ",
653
- selectedCommits.length,
654
- " \u4E2A commit:"
850
+ const selectedCommits = selectedHashes.map((hash) => commits2.find((c) => c.hash === hash)).filter(Boolean);
851
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
852
+ /* @__PURE__ */ jsx7(SectionHeader, { title: "\u786E\u8BA4\u6267\u884C" }),
853
+ /* @__PURE__ */ jsx7(StatusPanel, { type: "info", title: `cherry-pick --no-commit \xB7 ${selectedCommits.length} \u4E2A commit`, children: selectedCommits.map((c) => /* @__PURE__ */ jsxs7(Box7, { children: [
854
+ /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
855
+ " ",
856
+ c.shortHash
655
857
  ] }),
656
- selectedCommits.map((c) => /* @__PURE__ */ jsxs5(Text5, { children: [
657
- /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
658
- " ",
659
- c.shortHash
660
- ] }),
661
- /* @__PURE__ */ jsxs5(Text5, { children: [
662
- " ",
663
- c.message
664
- ] }),
665
- /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
666
- " (",
667
- c.author,
668
- ")"
669
- ] })
670
- ] }, c.hash))
671
- ] }),
672
- hasMerge && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [
673
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "\u68C0\u6D4B\u5230 Merge Commit" }),
674
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Cherry-pick \u5408\u5E76\u63D0\u4EA4\u9700\u8981\u6307\u5B9A\u7236\u8282\u70B9 (-m 1)" }),
675
- /* @__PURE__ */ jsxs5(Text5, { children: [
676
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "[m]" }),
677
- /* @__PURE__ */ jsx5(Text5, { children: " \u5207\u6362 -m 1: " }),
678
- useMainline ? /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u5DF2\u542F\u7528" }) : /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "\u672A\u542F\u7528" })
858
+ /* @__PURE__ */ jsxs7(Text7, { children: [
859
+ " ",
860
+ c.message
861
+ ] }),
862
+ /* @__PURE__ */ jsxs7(Text7, { color: "gray", dimColor: true, children: [
863
+ " ",
864
+ c.author
865
+ ] })
866
+ ] }, c.hash)) }),
867
+ hasMerge && /* @__PURE__ */ jsxs7(StatusPanel, { type: "warn", title: "\u68C0\u6D4B\u5230 Merge Commit", children: [
868
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Cherry-pick \u5408\u5E76\u63D0\u4EA4\u9700\u8981\u6307\u5B9A\u7236\u8282\u70B9 (-m 1)" }),
869
+ /* @__PURE__ */ jsxs7(Box7, { children: [
870
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
871
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "[m]" }),
872
+ /* @__PURE__ */ jsx7(Text7, { children: " \u5207\u6362 -m 1: " }),
873
+ useMainline ? /* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: "\u5DF2\u542F\u7528" }) : /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "\u672A\u542F\u7528" })
679
874
  ] })
680
875
  ] }),
681
- /* @__PURE__ */ jsxs5(Box5, { children: [
682
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u26A0 " }),
683
- /* @__PURE__ */ jsx5(Text5, { children: "\u4F7F\u7528 --no-commit \u6A21\u5F0F\uFF0C\u6539\u52A8\u5C06\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit" })
876
+ /* @__PURE__ */ jsxs7(Box7, { children: [
877
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u25B2 " }),
878
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "--no-commit \u6A21\u5F0F\uFF0C\u6539\u52A8\u5C06\u6682\u5B58\u5230\u5DE5\u4F5C\u533A\uFF0C\u9700\u624B\u52A8 commit" })
684
879
  ] }),
685
- /* @__PURE__ */ jsxs5(Box5, { children: [
686
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
687
- /* @__PURE__ */ jsx5(Text5, { color: "green", children: "[y]" }),
688
- /* @__PURE__ */ jsx5(Text5, { children: " \u786E\u8BA4 / " }),
689
- /* @__PURE__ */ jsx5(Text5, { color: "red", children: "[n]" }),
690
- /* @__PURE__ */ jsx5(Text5, { children: " \u53D6\u6D88" }),
691
- hasMerge && /* @__PURE__ */ jsxs5(Text5, { children: [
692
- " / ",
693
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "[m]" }),
694
- " \u5207\u6362 -m 1"
880
+ /* @__PURE__ */ jsxs7(Box7, { children: [
881
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "\u786E\u8BA4\u6267\u884C? " }),
882
+ /* @__PURE__ */ jsx7(InlineKeys, { hints: [
883
+ { key: "y", label: "\u786E\u8BA4" },
884
+ { key: "n", label: "\u53D6\u6D88" },
885
+ ...hasMerge ? [{ key: "m", label: "\u5207\u6362 -m 1" }] : [],
886
+ { key: "Esc", label: "\u8FD4\u56DE" }
695
887
  ] })
696
888
  ] })
697
889
  ] });
698
890
  }
699
891
 
700
892
  // src/components/result-panel.tsx
701
- import { useState as useState5, useEffect as useEffect2 } from "react";
702
- import { Box as Box6, Text as Text6 } from "ink";
703
- import { Spinner as Spinner4 } from "@inkjs/ui";
704
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
893
+ import { useState as useState6, useEffect as useEffect3 } from "react";
894
+ import { Box as Box8, Text as Text8 } from "ink";
895
+ import { Spinner as Spinner5 } from "@inkjs/ui";
896
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
705
897
  function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, onDone }) {
706
- const [phase, setPhase] = useState5("executing");
707
- const [result, setResult] = useState5(null);
708
- const [stagedStat, setStagedStat] = useState5("");
709
- const [stashRestored, setStashRestored] = useState5(null);
898
+ const [phase, setPhase] = useState6("executing");
899
+ const [result, setResult] = useState6(null);
900
+ const [stagedStat, setStagedStat] = useState6("");
901
+ const [stashRestored, setStashRestored] = useState6(null);
710
902
  const tryRestoreStash = async () => {
711
903
  if (!stashed) return true;
712
904
  setPhase("restoring");
@@ -715,7 +907,7 @@ function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, on
715
907
  if (ok) onStashRestored();
716
908
  return ok;
717
909
  };
718
- useEffect2(() => {
910
+ useEffect3(() => {
719
911
  async function run() {
720
912
  const res = await cherryPick(selectedHashes, useMainline);
721
913
  setResult(res);
@@ -732,62 +924,94 @@ function ResultPanel({ selectedHashes, useMainline, stashed, onStashRestored, on
732
924
  run();
733
925
  }, []);
734
926
  if (phase === "executing") {
735
- return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Spinner4, { label: `\u6B63\u5728\u6267\u884C cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` }) });
927
+ return /* @__PURE__ */ jsx8(Spinner5, { label: `cherry-pick --no-commit (${selectedHashes.length} \u4E2A commit)...` });
736
928
  }
737
929
  if (phase === "restoring") {
738
- return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Spinner4, { label: "\u6B63\u5728\u6062\u590D\u5DE5\u4F5C\u533A (git stash pop)..." }) });
930
+ return /* @__PURE__ */ jsx8(Spinner5, { label: "\u6062\u590D\u5DE5\u4F5C\u533A (git stash pop)..." });
739
931
  }
740
932
  if (phase === "error" && result) {
741
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
742
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "[5/5] Cherry-pick \u9047\u5230\u51B2\u7A81" }),
743
- result.conflictFiles && result.conflictFiles.length > 0 && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [
744
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "\u51B2\u7A81\u6587\u4EF6:" }),
745
- result.conflictFiles.map((f) => /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
746
- " ",
747
- f
748
- ] }, f))
933
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
934
+ /* @__PURE__ */ jsx8(SectionHeader, { title: "Cherry-pick \u9047\u5230\u51B2\u7A81" }),
935
+ result.conflictFiles && result.conflictFiles.length > 0 && /* @__PURE__ */ jsx8(StatusPanel, { type: "error", title: "\u51B2\u7A81\u6587\u4EF6", children: result.conflictFiles.map((f) => /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
936
+ " ",
937
+ f
938
+ ] }, f)) }),
939
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
940
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B8 \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
941
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "\u25B8 \u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" })
749
942
  ] }),
750
- /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u8BF7\u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add \u548C git commit" }),
751
- /* @__PURE__ */ jsx6(Text6, { color: "gray", dimColor: true, children: "\u6216\u6267\u884C git cherry-pick --abort \u653E\u5F03\u64CD\u4F5C" }),
752
- stashed && stashRestored === false && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u6CE8\u610F: stash \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" }),
753
- stashed && stashRestored === true && /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" })
943
+ stashed && stashRestored === false && /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 stash \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" }),
944
+ stashed && stashRestored === true && /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" })
754
945
  ] });
755
946
  }
756
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
757
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "green", children: "[5/5] \u540C\u6B65\u5B8C\u6210!" }),
758
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [
759
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "\u6682\u5B58\u533A\u53D8\u66F4\u6982\u89C8 (git diff --cached --stat):" }),
760
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" })
761
- ] }),
762
- stashed && (stashRestored ? /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" }) : /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" })),
763
- /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
764
- /* @__PURE__ */ jsx6(Text6, { children: "\u8BF7\u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
765
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: " git diff --cached # \u67E5\u770B\u8BE6\u7EC6 diff" }),
766
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: ' git commit -m "\u540C\u6B65 commit" # \u63D0\u4EA4' }),
767
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: " git reset HEAD # \u6216\u653E\u5F03\u6240\u6709\u6539\u52A8" })
947
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
948
+ /* @__PURE__ */ jsx8(SectionHeader, { title: "\u540C\u6B65\u5B8C\u6210" }),
949
+ /* @__PURE__ */ jsx8(StatusPanel, { type: "success", title: "\u6682\u5B58\u533A\u53D8\u66F4 (git diff --cached --stat)", children: /* @__PURE__ */ jsx8(Text8, { color: "gray", children: stagedStat || "(\u65E0\u53D8\u66F4)" }) }),
950
+ stashed && (stashRestored ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714 \u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)" }) : /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 stash pop \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8 git stash pop" })),
951
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
952
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u25B2 \u6539\u52A8\u5DF2\u6682\u5B58\u5230\u5DE5\u4F5C\u533A (--no-commit \u6A21\u5F0F)" }),
953
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: " \u5BA1\u67E5\u540E\u624B\u52A8\u6267\u884C:" }),
954
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
955
+ " git diff --cached ",
956
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u67E5\u770B\u8BE6\u7EC6 diff" })
957
+ ] }),
958
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
959
+ ' git commit -m "sync: ..." ',
960
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u63D0\u4EA4" })
961
+ ] }),
962
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
963
+ " git reset HEAD ",
964
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
965
+ ] })
966
+ ] })
768
967
  ] });
769
968
  }
770
969
 
771
970
  // src/app.tsx
772
971
  import { execSync } from "child_process";
773
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
774
- function App() {
972
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
973
+ var STEP_NUMBER = {
974
+ checking: 0,
975
+ "stash-recovery": 0,
976
+ "stash-prompt": 0,
977
+ remote: 1,
978
+ branch: 2,
979
+ commits: 3,
980
+ confirm: 4,
981
+ result: 5
982
+ };
983
+ var STEP_DEBOUNCE = 100;
984
+ function App({ initialRemote, initialBranch }) {
775
985
  const { exit } = useApp();
776
- const [step, setStep] = useState6("checking");
777
- const [remote, setRemote] = useState6("");
778
- const [branch, setBranch] = useState6("");
779
- const [selectedHashes, setSelectedHashes] = useState6([]);
780
- const [commits, setCommits] = useState6([]);
781
- const [hasMerge, setHasMerge] = useState6(false);
782
- const [useMainline, setUseMainline] = useState6(false);
783
- const [stashed, setStashed] = useState6(false);
986
+ const entryStep = initialRemote && initialBranch ? "commits" : initialRemote ? "branch" : "remote";
987
+ const [step, setStepRaw] = useState7("checking");
988
+ const [inputReady, setInputReady] = useState7(true);
989
+ const [remote2, setRemote] = useState7(initialRemote || "");
990
+ const [branch2, setBranch] = useState7(initialBranch || "");
991
+ const [selectedHashes, setSelectedHashes] = useState7([]);
992
+ const [commits2, setCommits] = useState7([]);
993
+ const [hasMerge, setHasMerge] = useState7(false);
994
+ const [useMainline, setUseMainline] = useState7(false);
995
+ const [stashed, setStashed] = useState7(false);
996
+ const [guardTimestamp, setGuardTimestamp] = useState7();
784
997
  const stashedRef = useRef2(false);
785
998
  const stashRestoredRef = useRef2(false);
999
+ const mountedRef = useRef2(true);
1000
+ const debounceTimer = useRef2(null);
1001
+ const setStep = useCallback2((newStep) => {
1002
+ setInputReady(false);
1003
+ setStepRaw(newStep);
1004
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1005
+ debounceTimer.current = setTimeout(() => {
1006
+ if (mountedRef.current) setInputReady(true);
1007
+ }, STEP_DEBOUNCE);
1008
+ }, []);
786
1009
  const restoreStashSync = useCallback2(() => {
787
1010
  if (stashedRef.current && !stashRestoredRef.current) {
788
1011
  try {
789
1012
  execSync("git stash pop", { stdio: "ignore" });
790
1013
  stashRestoredRef.current = true;
1014
+ removeStashGuardSync();
791
1015
  } catch {
792
1016
  try {
793
1017
  process.stderr.write("\n\u26A0 stash \u81EA\u52A8\u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: git stash pop\n");
@@ -798,97 +1022,144 @@ function App() {
798
1022
  }, []);
799
1023
  const markStashRestored = useCallback2(() => {
800
1024
  stashRestoredRef.current = true;
1025
+ removeStashGuard();
801
1026
  }, []);
802
- useEffect3(() => {
803
- isWorkingDirClean().then((clean) => {
804
- if (clean) {
805
- setStep("remote");
806
- } else {
807
- setStep("stash-prompt");
1027
+ useEffect4(() => {
1028
+ mountedRef.current = true;
1029
+ async function check() {
1030
+ const guard = await checkStashGuard();
1031
+ if (!mountedRef.current) return;
1032
+ if (guard.exists) {
1033
+ setGuardTimestamp(guard.timestamp);
1034
+ setStep("stash-recovery");
1035
+ return;
808
1036
  }
809
- });
1037
+ const clean = await isWorkingDirClean();
1038
+ if (!mountedRef.current) return;
1039
+ setStep(clean ? entryStep : "stash-prompt");
1040
+ }
1041
+ check();
810
1042
  const onSignal = () => {
811
1043
  restoreStashSync();
812
1044
  process.exit(0);
813
1045
  };
814
1046
  process.on("SIGINT", onSignal);
815
1047
  process.on("SIGTERM", onSignal);
816
- process.on("beforeExit", restoreStashSync);
1048
+ process.on("SIGHUP", onSignal);
817
1049
  return () => {
1050
+ mountedRef.current = false;
1051
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
818
1052
  process.off("SIGINT", onSignal);
819
1053
  process.off("SIGTERM", onSignal);
820
- process.off("beforeExit", restoreStashSync);
1054
+ process.off("SIGHUP", onSignal);
821
1055
  };
822
- }, [restoreStashSync]);
1056
+ }, [restoreStashSync, setStep]);
823
1057
  const doStash = async () => {
824
1058
  const ok = await stash();
825
1059
  if (ok) {
826
1060
  setStashed(true);
827
1061
  stashedRef.current = true;
1062
+ await writeStashGuard();
828
1063
  }
829
- setStep("remote");
1064
+ if (mountedRef.current) setStep(entryStep);
830
1065
  };
831
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
832
- /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
833
- /* @__PURE__ */ jsx7(Text7, { bold: true, inverse: true, color: "white", children: " git-sync-tui " }),
834
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
835
- /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "\u4EA4\u4E92\u5F0F commit \u540C\u6B65\u5DE5\u5177 (cherry-pick --no-commit)" }),
836
- stashed && /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: " (\u5DF2\u81EA\u52A8 stash)" })
837
- ] }),
838
- step === "checking" && /* @__PURE__ */ jsx7(Spinner5, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
839
- step === "stash-prompt" && /* @__PURE__ */ jsx7(
1066
+ const doStashRecover = async () => {
1067
+ const entry = await findStashEntry();
1068
+ if (entry) {
1069
+ await stashPop();
1070
+ }
1071
+ await removeStashGuard();
1072
+ if (!mountedRef.current) return;
1073
+ const clean = await isWorkingDirClean();
1074
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1075
+ };
1076
+ const skipStashRecover = async () => {
1077
+ await removeStashGuard();
1078
+ if (!mountedRef.current) return;
1079
+ const clean = await isWorkingDirClean();
1080
+ if (mountedRef.current) setStep(clean ? entryStep : "stash-prompt");
1081
+ };
1082
+ const goBack = useCallback2((fromStep) => {
1083
+ const backMap = {
1084
+ branch: "remote",
1085
+ commits: "branch",
1086
+ confirm: "commits"
1087
+ };
1088
+ const prev = backMap[fromStep];
1089
+ if (prev) {
1090
+ setStep(prev);
1091
+ } else {
1092
+ restoreStashSync();
1093
+ exit();
1094
+ }
1095
+ }, [setStep, restoreStashSync, exit]);
1096
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1097
+ /* @__PURE__ */ jsx9(AppHeader, { step: STEP_NUMBER[step], stashed }),
1098
+ step === "checking" && /* @__PURE__ */ jsx9(Spinner6, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1099
+ step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx9(
1100
+ StashRecovery,
1101
+ {
1102
+ timestamp: guardTimestamp,
1103
+ onRecover: doStashRecover,
1104
+ onSkip: skipStashRecover
1105
+ }
1106
+ ),
1107
+ step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx9(
840
1108
  StashPrompt,
841
1109
  {
842
1110
  onConfirm: doStash,
843
- onSkip: () => setStep("remote")
1111
+ onSkip: () => setStep(entryStep)
844
1112
  }
845
1113
  ),
846
- step === "remote" && /* @__PURE__ */ jsx7(
1114
+ step === "remote" && inputReady && /* @__PURE__ */ jsx9(
847
1115
  RemoteSelect,
848
1116
  {
849
1117
  onSelect: (r) => {
850
1118
  setRemote(r);
851
1119
  setStep("branch");
852
- }
1120
+ },
1121
+ onBack: () => goBack("remote")
853
1122
  }
854
1123
  ),
855
- step === "branch" && /* @__PURE__ */ jsx7(
1124
+ step === "branch" && inputReady && /* @__PURE__ */ jsx9(
856
1125
  BranchSelect,
857
1126
  {
858
- remote,
1127
+ remote: remote2,
859
1128
  onSelect: (b) => {
860
1129
  setBranch(b);
861
1130
  setStep("commits");
862
- }
1131
+ },
1132
+ onBack: () => goBack("branch")
863
1133
  }
864
1134
  ),
865
- step === "commits" && /* @__PURE__ */ jsx7(
1135
+ step === "commits" && inputReady && /* @__PURE__ */ jsx9(
866
1136
  CommitList,
867
1137
  {
868
- remote,
869
- branch,
1138
+ remote: remote2,
1139
+ branch: branch2,
870
1140
  onSelect: async (hashes, loadedCommits) => {
871
1141
  setSelectedHashes(hashes);
872
1142
  setCommits(loadedCommits);
873
1143
  const merge = await hasMergeCommits(hashes);
874
1144
  setHasMerge(merge);
875
1145
  setStep("confirm");
876
- }
1146
+ },
1147
+ onBack: () => goBack("commits")
877
1148
  }
878
1149
  ),
879
- step === "confirm" && /* @__PURE__ */ jsx7(
1150
+ step === "confirm" && inputReady && /* @__PURE__ */ jsx9(
880
1151
  ConfirmPanel,
881
1152
  {
882
- commits,
1153
+ commits: commits2,
883
1154
  selectedHashes,
884
1155
  hasMerge,
885
1156
  useMainline,
886
1157
  onToggleMainline: () => setUseMainline((v) => !v),
887
1158
  onConfirm: () => setStep("result"),
888
- onCancel: () => setStep("commits")
1159
+ onCancel: () => goBack("confirm")
889
1160
  }
890
1161
  ),
891
- step === "result" && /* @__PURE__ */ jsx7(
1162
+ step === "result" && /* @__PURE__ */ jsx9(
892
1163
  ResultPanel,
893
1164
  {
894
1165
  selectedHashes,
@@ -904,32 +1175,244 @@ function App() {
904
1175
  ] });
905
1176
  }
906
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
+
907
1342
  // src/cli.tsx
908
- import { jsx as jsx8 } from "react/jsx-runtime";
1343
+ import { jsx as jsx10 } from "react/jsx-runtime";
909
1344
  var cli = meow(
910
1345
  `
911
1346
  \u7528\u6CD5
912
- $ git-sync-tui
1347
+ $ git-sync-tui [options]
913
1348
 
914
1349
  \u9009\u9879
915
- --help \u663E\u793A\u5E2E\u52A9
916
- --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
917
1358
 
918
- \u8BF4\u660E
919
- \u4EA4\u4E92\u5F0F TUI \u5DE5\u5177\uFF0C\u4ECE\u8FDC\u7A0B\u5206\u652F\u6311\u9009 commit \u540C\u6B65\u5230\u5F53\u524D\u5206\u652F\u3002
920
- \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
921
1365
 
922
- \u5FEB\u6377\u952E
1366
+ TUI \u5FEB\u6377\u952E
923
1367
  Space \u9009\u62E9/\u53D6\u6D88 commit
924
1368
  Shift+\u2191/\u2193 \u8FDE\u7EED\u9009\u62E9
925
1369
  a \u5168\u9009/\u53D6\u6D88\u5168\u9009
926
1370
  i \u53CD\u9009
927
1371
  r \u9009\u81F3\u5F00\u5934
928
1372
  Enter \u786E\u8BA4\u9009\u62E9
1373
+ Esc \u8FD4\u56DE\u4E0A\u4E00\u6B65
929
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
930
1382
  `,
931
1383
  {
932
- 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
+ }
933
1395
  }
934
1396
  );
935
- render(/* @__PURE__ */ jsx8(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
+ }