git-sync-tui 0.1.6 → 0.1.7

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.
package/README.md CHANGED
@@ -15,14 +15,15 @@
15
15
 
16
16
  <p align="center">
17
17
  Cherry-pick commits from remote branches with an intuitive terminal UI.<br>
18
- Select specific commits, preview changes, and sync with <code>--no-commit</code> mode for safe review.
18
+ Multi-select commits, preview diff stats, handle conflicts interactively, and sync safely with backup &amp; stash protection.
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
22
  <a href="#-features">Features</a> ·
23
23
  <a href="#-quick-start">Quick Start</a> ·
24
24
  <a href="#-installation">Installation</a> ·
25
- <a href="#-workflow">Workflow</a>
25
+ <a href="#-workflow">Workflow</a> ·
26
+ <a href="#%EF%B8%8F-cli-options">CLI Options</a>
26
27
  </p>
27
28
 
28
29
  <p align="center">
@@ -35,11 +36,18 @@
35
36
 
36
37
  ## ✨ Features
37
38
 
38
- - 🎯 **Multi-select commits** — Cherry-pick non-consecutive commits with Space / Enter
39
+ - 🎯 **Multi-select commits** — Select non-consecutive commits with Space, range-select with Shift+↑↓, toggle all with `a`, invert with `i`
39
40
  - 🔍 **Branch search** — Fuzzy filter branches by keyword
40
- - 👀 **Diff preview** — See `--stat` summary of selected commits before executing
41
- - ⚡ **Safe mode** — `--no-commit` stages changes for review, never auto-commits
42
- - ⚠️ **Conflict handling** — Clear display of conflicted files when cherry-pick fails
41
+ - 👀 **Diff preview** — Scrollable `--stat` summary panel with `j`/`k` navigation
42
+ - ⚡ **Dual mode** — `--no-commit` stages changes for review, or commit individually preserving original messages
43
+ - 🔀 **One-by-one cherry-pick** — Executes commits sequentially, pausing on conflicts for interactive resolution
44
+ - ⚠️ **Conflict handling** — Shows conflicted files, resolve in another terminal, then continue/abort/quit
45
+ - 🛡️ **Safe backup** — Creates a backup branch before execution; full rollback on abort
46
+ - 📦 **Auto stash** — Detects uncommitted changes, offers to stash, auto-restores after sync
47
+ - 🔄 **Stash recovery** — Detects interrupted sessions and offers to recover stashed changes
48
+ - 🌿 **Branch check** — Auto-creates target branch from main/master if not on it
49
+ - ✅ **Synced markers** — Marks already-synced commits as `[synced]` in the commit list
50
+ - 🖥️ **CLI mode** — Non-interactive mode with `-r -b -c` flags for scripting
43
51
  - 🌐 **Universal** — Works in any git repository, any language
44
52
 
45
53
  ## 🚀 Quick Start
@@ -64,35 +72,95 @@ npm install -g git-sync-tui
64
72
  ## 🔄 Workflow
65
73
 
66
74
  ```
67
- Select Remote → Select Branch → Multi-select Commits → Preview Changes
68
-
69
- Review & Commit manually ← Cherry-pick --no-commit (staged, not committed)
75
+ Check workspace → Select Remote → Select Branch → Branch Check → Multi-select Commits
76
+
77
+ Auto stash Preview diff stats
78
+ (if needed) ↓
79
+ Confirm & choose mode
80
+
81
+ Cherry-pick one-by-one (with backup)
82
+
83
+ Handle conflicts / Done
84
+
85
+ Restore stash & exit
70
86
  ```
71
87
 
72
88
  ## ⌨️ Keyboard Shortcuts
73
89
 
90
+ ### Commit Selection
91
+
74
92
  | Key | Action |
75
93
  |-----|--------|
76
- | `↑` `↓` | Navigate items |
94
+ | `↑` `↓` | Navigate commits |
77
95
  | `Space` | Toggle commit selection |
96
+ | `Shift`+`↑`/`↓` | Range select |
97
+ | `a` | Select all / Deselect all |
98
+ | `i` | Invert selection |
99
+ | `r` | Select from top to cursor |
100
+ | `j` / `k` | Scroll diff stat preview |
78
101
  | `Enter` | Confirm selection |
79
- | `y` / `n` | Confirm / cancel execution |
80
- | `/` | Search (in branch list) |
102
+ | `Esc` | Go back |
81
103
 
82
- ## 📋 After Sync
104
+ ### Confirm Panel
105
+
106
+ | Key | Action |
107
+ |-----|--------|
108
+ | `y` | Confirm execution |
109
+ | `n` | Cancel |
110
+ | `c` | Toggle commit mode (--no-commit / individual) |
111
+ | `m` | Toggle `-m 1` for merge commits |
112
+ | `Esc` | Go back |
83
113
 
84
- Changes are staged in your working tree (not committed). You can:
114
+ ### Conflict Handling
85
115
 
86
- ```bash
87
- # Review staged changes
88
- git diff --cached
116
+ | Key | Action |
117
+ |-----|--------|
118
+ | `c` | Continue (after resolving conflicts) |
119
+ | `a` | Abort (rollback all changes) |
120
+ | `q` | Quit (keep current state) |
89
121
 
90
- # Commit when ready
91
- git commit -m "sync: cherry-picked commits from feature-branch"
122
+ ## ⚙️ CLI Options
92
123
 
93
- # Or discard all changes
94
- git reset HEAD
95
124
  ```
125
+ Usage
126
+ $ git-sync-tui [options]
127
+
128
+ Options
129
+ -r, --remote <name> Remote name
130
+ -b, --branch <name> Remote branch name
131
+ -c, --commits <hashes> Commit hashes (comma-separated)
132
+ -n, --count <number> Number of commits to show (default: 100)
133
+ -m, --mainline Use -m 1 for merge commits
134
+ -y, --yes Skip confirmation
135
+ --no-stash Skip stash prompt
136
+ --list List remote branch commits and exit
137
+
138
+ Modes
139
+ No arguments Interactive TUI mode
140
+ -r -b --list List commits (plain text)
141
+ -r -b -c CLI mode, confirm before execution
142
+ -r -b -c --yes CLI mode, execute directly
143
+ -r or -r -b only TUI mode, skip completed steps
144
+
145
+ Examples
146
+ $ git-sync-tui # TUI mode
147
+ $ git-sync-tui -r upstream -b main --list # List commits
148
+ $ git-sync-tui -r upstream -b main -c abc1234 --yes # Execute directly
149
+ $ git-sync-tui -r upstream -b main -c abc1234,def5678 # Confirm then execute
150
+ $ git-sync-tui -r upstream # TUI mode, skip remote select
151
+ ```
152
+
153
+ ## 📋 After Sync
154
+
155
+ **--no-commit mode** — Changes are staged in your working tree (not committed):
156
+
157
+ ```bash
158
+ git diff --cached # Review staged changes
159
+ git commit -m "sync: cherry-picked commits from feature-branch" # Commit
160
+ git reset HEAD # Or discard all changes
161
+ ```
162
+
163
+ **Individual commit mode** — Original commit messages are preserved. Check with `git log`.
96
164
 
97
165
  ## 💡 Use Cases
98
166
 
@@ -107,8 +175,8 @@ git reset HEAD
107
175
  ```bash
108
176
  git clone https://github.com/KiWi233333/git-sync-tui.git
109
177
  cd git-sync-tui
110
- npm install
111
- npm start
178
+ pnpm install
179
+ pnpm start
112
180
  ```
113
181
 
114
182
  ## 🏗️ Tech Stack
@@ -116,6 +184,7 @@ npm start
116
184
  - [Ink](https://github.com/vadimdemedes/ink) — React for interactive CLI apps
117
185
  - [@inkjs/ui](https://github.com/inkjs/ui) — UI components for Ink
118
186
  - [simple-git](https://github.com/steveukx/git-js) — Git commands interface
187
+ - [meow](https://github.com/sindresorhus/meow) — CLI argument parsing
119
188
 
120
189
  ## 🤝 Contributing
121
190
 
package/README.zh-CN.md CHANGED
@@ -15,14 +15,15 @@
15
15
 
16
16
  <p align="center">
17
17
  通过直观的终端界面从远程分支 cherry-pick 提交。<br>
18
- 选择特定提交、预览变更,并使用 <code>--no-commit</code> 模式在提交前安全审查。
18
+ 多选提交、预览 diff 统计、交互式处理冲突,支持备份分支和 stash 保护机制。
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
22
  <a href="#-功能特性">功能</a> ·
23
23
  <a href="#-快速开始">快速开始</a> ·
24
24
  <a href="#-安装">安装</a> ·
25
- <a href="#-工作流程">工作流程</a>
25
+ <a href="#-工作流程">工作流程</a> ·
26
+ <a href="#%EF%B8%8F-命令行选项">命令行选项</a>
26
27
  </p>
27
28
 
28
29
  <p align="center">
@@ -35,11 +36,18 @@
35
36
 
36
37
  ## ✨ 功能特性
37
38
 
38
- - 🎯 **多选提交** — 使用 Space / Enter 选择不连续的提交进行 cherry-pick
39
+ - 🎯 **多选提交** — 使用 Space 选择不连续的提交,Shift+↑↓ 连选,`a` 全选,`i` 反选
39
40
  - 🔍 **分支搜索** — 按关键词模糊过滤分支
40
- - 👀 **差异预览** — 执行前查看所选提交的 `--stat` 摘要
41
- - ⚡ **安全模式** — `--no-commit` 仅暂存变更供审查,不会自动提交
42
- - ⚠️ **冲突处理** cherry-pick 失败时清晰显示冲突文件
41
+ - 👀 **差异预览** — 可滚动的 `--stat` 摘要面板,支持 `j`/`k` 上下滚动
42
+ - ⚡ **双模式** — `--no-commit` 仅暂存变更供审查,或逐个提交保留原始 commit 信息
43
+ - 🔀 **逐个 cherry-pick** — 按顺序执行提交,遇到冲突时暂停等待交互处理
44
+ - ⚠️ **冲突处理** — 显示冲突文件列表,在另一终端解决后继续/放弃/退出
45
+ - 🛡️ **安全备份** — 执行前自动创建备份分支,放弃时完整回滚
46
+ - 📦 **自动 stash** — 检测未提交变更,提示 stash 保存,同步后自动恢复
47
+ - 🔄 **Stash 恢复** — 检测上次中断的会话,提供恢复 stash 的选项
48
+ - 🌿 **分支检查** — 若当前不在目标分支,自动从 main/master 创建并切换
49
+ - ✅ **已同步标记** — 在 commit 列表中标记已同步的提交为 `[已同步]`
50
+ - 🖥️ **CLI 模式** — 支持 `-r -b -c` 参数的非交互模式,适用于脚本
43
51
  - 🌐 **通用性** — 适用于任何 Git 仓库,不限语言
44
52
 
45
53
  ## 🚀 快速开始
@@ -64,35 +72,95 @@ npm install -g git-sync-tui
64
72
  ## 🔄 工作流程
65
73
 
66
74
  ```
67
- 选择远程仓库 → 选择分支 → 多选提交预览变更
68
-
69
- 手动审查并提交 ← Cherry-pick --no-commit(已暂存,未提交)
75
+ 检查工作区 → 选择远程仓库 → 选择分支 → 分支检查多选提交
76
+
77
+ 自动 stash 预览 diff 统计
78
+ (如需要) ↓
79
+ 确认并选择模式
80
+
81
+ 逐个 cherry-pick(带备份)
82
+
83
+ 处理冲突 / 完成
84
+
85
+ 恢复 stash 并退出
70
86
  ```
71
87
 
72
88
  ## ⌨️ 快捷键
73
89
 
90
+ ### 提交选择
91
+
74
92
  | 按键 | 操作 |
75
93
  |-----|------|
76
94
  | `↑` `↓` | 上下导航 |
77
95
  | `Space` | 切换提交选择 |
96
+ | `Shift`+`↑`/`↓` | 连续选择 |
97
+ | `a` | 全选 / 取消全选 |
98
+ | `i` | 反选 |
99
+ | `r` | 从开头选至光标 |
100
+ | `j` / `k` | 滚动 diff stat 预览 |
78
101
  | `Enter` | 确认选择 |
79
- | `y` / `n` | 确认 / 取消执行 |
80
- | `/` | 搜索(在分支列表中) |
102
+ | `Esc` | 返回上一步 |
81
103
 
82
- ## 📋 同步后操作
104
+ ### 确认面板
105
+
106
+ | 按键 | 操作 |
107
+ |-----|------|
108
+ | `y` | 确认执行 |
109
+ | `n` | 取消 |
110
+ | `c` | 切换提交模式(--no-commit / 逐个提交) |
111
+ | `m` | 切换 `-m 1`(merge commit 时) |
112
+ | `Esc` | 返回 |
83
113
 
84
- 变更已暂存在工作区(未提交)。你可以:
114
+ ### 冲突处理
85
115
 
86
- ```bash
87
- # 查看暂存的变更
88
- git diff --cached
116
+ | 按键 | 操作 |
117
+ |-----|------|
118
+ | `c` | 继续(冲突已解决) |
119
+ | `a` | 放弃(回滚全部变更) |
120
+ | `q` | 退出(保留当前状态) |
89
121
 
90
- # 准备好后提交
91
- git commit -m "sync: 从 feature-branch cherry-pick 提交"
122
+ ## ⚙️ 命令行选项
92
123
 
93
- # 或放弃所有变更
94
- git reset HEAD
95
124
  ```
125
+ 用法
126
+ $ git-sync-tui [options]
127
+
128
+ 选项
129
+ -r, --remote <name> 指定远程仓库名称
130
+ -b, --branch <name> 指定远程分支名称
131
+ -c, --commits <hashes> 指定 commit hash(逗号分隔)
132
+ -n, --count <number> 显示 commit 数量(默认 100)
133
+ -m, --mainline 对 merge commit 使用 -m 1
134
+ -y, --yes 跳过确认直接执行
135
+ --no-stash 跳过 stash 提示
136
+ --list 列出远程分支的 commit 后退出
137
+
138
+ 模式
139
+ 无参数 交互式 TUI 模式
140
+ -r -b --list 列出 commit(纯文本)
141
+ -r -b -c CLI 模式,确认后执行
142
+ -r -b -c --yes CLI 模式,直接执行
143
+ 仅 -r 或 -r -b TUI 模式,跳过已指定步骤
144
+
145
+ 示例
146
+ $ git-sync-tui # TUI 模式
147
+ $ git-sync-tui -r upstream -b main --list # 列出 commits
148
+ $ git-sync-tui -r upstream -b main -c abc1234 --yes # 直接执行
149
+ $ git-sync-tui -r upstream -b main -c abc1234,def5678 # 确认后执行
150
+ $ git-sync-tui -r upstream # TUI 模式,跳过选择仓库
151
+ ```
152
+
153
+ ## 📋 同步后操作
154
+
155
+ **--no-commit 模式** — 变更已暂存在工作区(未提交):
156
+
157
+ ```bash
158
+ git diff --cached # 查看暂存的变更
159
+ git commit -m "sync: 从 feature-branch cherry-pick 提交" # 提交
160
+ git reset HEAD # 或放弃所有变更
161
+ ```
162
+
163
+ **逐个提交模式** — 保留原始 commit 信息,可通过 `git log` 查看。
96
164
 
97
165
  ## 💡 使用场景
98
166
 
@@ -107,8 +175,8 @@ git reset HEAD
107
175
  ```bash
108
176
  git clone https://github.com/KiWi233333/git-sync-tui.git
109
177
  cd git-sync-tui
110
- npm install
111
- npm start
178
+ pnpm install
179
+ pnpm start
112
180
  ```
113
181
 
114
182
  ## 🏗️ 技术栈
@@ -116,6 +184,7 @@ npm start
116
184
  - [Ink](https://github.com/vadimdemedes/ink) — 用于构建交互式 CLI 应用的 React 框架
117
185
  - [@inkjs/ui](https://github.com/inkjs/ui) — Ink 的 UI 组件库
118
186
  - [simple-git](https://github.com/steveukx/git-js) — Git 命令接口
187
+ - [meow](https://github.com/sindresorhus/meow) — CLI 参数解析
119
188
 
120
189
  ## 🤝 贡献
121
190
 
package/dist/cli.js CHANGED
@@ -11,8 +11,8 @@ import { render } from "ink";
11
11
  import meow from "meow";
12
12
 
13
13
  // src/app.tsx
14
- import { useState as useState8, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback3 } from "react";
15
- import { Box as Box10, useApp } from "ink";
14
+ import { useState as useState9, useEffect as useEffect7, useRef as useRef5, useCallback as useCallback3 } from "react";
15
+ import { Box as Box11, useApp } from "ink";
16
16
  import { Spinner as Spinner7 } from "@inkjs/ui";
17
17
 
18
18
  // src/components/ui.tsx
@@ -1463,6 +1463,10 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
1463
1463
  setPhase("aborted");
1464
1464
  }, [tryRestoreStash]);
1465
1465
  useInput8((input, key) => {
1466
+ if (phase === "done" || phase === "aborted") {
1467
+ onDone();
1468
+ return;
1469
+ }
1466
1470
  if (phase === "conflict") {
1467
1471
  if (input === "c" || input === "C") {
1468
1472
  handleContinue();
@@ -1571,7 +1575,8 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
1571
1575
  stashed && stashRestored === true && /* @__PURE__ */ jsxs9(Text9, { color: "green", children: [
1572
1576
  "\u2714 ",
1573
1577
  "\u5DF2\u6062\u590D\u5DE5\u4F5C\u533A\u53D8\u66F4 (stash pop)"
1574
- ] })
1578
+ ] }),
1579
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
1575
1580
  ] });
1576
1581
  }
1577
1582
  const total = orderedHashes.current.length;
@@ -1608,13 +1613,139 @@ function ResultPanel({ selectedHashes, useMainline, noCommit, stashed, onStashRe
1608
1613
  " git reset HEAD ",
1609
1614
  /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "# \u6216\u653E\u5F03" })
1610
1615
  ] })
1611
- ] }) : /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5DF2\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F\uFF0C\u53EF\u901A\u8FC7 git log \u67E5\u770B" }) })
1616
+ ] }) : /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: " \u5DF2\u4FDD\u7559\u539F\u59CB commit \u4FE1\u606F\uFF0C\u53EF\u901A\u8FC7 git log \u67E5\u770B" }) }),
1617
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "\u6309\u4EFB\u610F\u952E\u9000\u51FA" })
1612
1618
  ] });
1613
1619
  }
1614
1620
 
1621
+ // src/components/update-banner.tsx
1622
+ import { useState as useState8, useEffect as useEffect6 } from "react";
1623
+ import { Box as Box10, Text as Text10 } from "ink";
1624
+
1625
+ // src/utils/update-check.ts
1626
+ import https from "https";
1627
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
1628
+ import { join as join2 } from "path";
1629
+ import { homedir } from "os";
1630
+ var PKG_NAME = "git-sync-tui";
1631
+ var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
1632
+ var REQUEST_TIMEOUT = 3e3;
1633
+ function getCachePath() {
1634
+ const dir = join2(homedir(), ".config", "git-sync-tui");
1635
+ return join2(dir, "update-check.json");
1636
+ }
1637
+ function readCache() {
1638
+ try {
1639
+ const raw = readFileSync2(getCachePath(), "utf-8");
1640
+ return JSON.parse(raw);
1641
+ } catch {
1642
+ return null;
1643
+ }
1644
+ }
1645
+ function writeCache(data) {
1646
+ try {
1647
+ const dir = join2(homedir(), ".config", "git-sync-tui");
1648
+ mkdirSync(dir, { recursive: true });
1649
+ writeFileSync2(getCachePath(), JSON.stringify(data));
1650
+ } catch {
1651
+ }
1652
+ }
1653
+ function fetchLatestVersion() {
1654
+ return new Promise((resolve) => {
1655
+ const req = https.get(
1656
+ `https://registry.npmjs.org/${PKG_NAME}/latest`,
1657
+ { timeout: REQUEST_TIMEOUT, headers: { Accept: "application/json" } },
1658
+ (res) => {
1659
+ if (res.statusCode !== 200) {
1660
+ resolve(null);
1661
+ return;
1662
+ }
1663
+ let data = "";
1664
+ res.on("data", (chunk) => {
1665
+ data += chunk.toString();
1666
+ });
1667
+ res.on("end", () => {
1668
+ try {
1669
+ const json = JSON.parse(data);
1670
+ resolve(json.version || null);
1671
+ } catch {
1672
+ resolve(null);
1673
+ }
1674
+ });
1675
+ }
1676
+ );
1677
+ req.on("error", () => resolve(null));
1678
+ req.on("timeout", () => {
1679
+ req.destroy();
1680
+ resolve(null);
1681
+ });
1682
+ });
1683
+ }
1684
+ function compareVersions(current, latest) {
1685
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
1686
+ const c = parse(current);
1687
+ const l = parse(latest);
1688
+ for (let i = 0; i < 3; i++) {
1689
+ if ((l[i] || 0) > (c[i] || 0)) return true;
1690
+ if ((l[i] || 0) < (c[i] || 0)) return false;
1691
+ }
1692
+ return false;
1693
+ }
1694
+ async function checkForUpdate(currentVersion) {
1695
+ const noUpdate = { hasUpdate: false, current: currentVersion, latest: currentVersion };
1696
+ const cache = readCache();
1697
+ if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL) {
1698
+ return {
1699
+ hasUpdate: compareVersions(currentVersion, cache.latest),
1700
+ current: currentVersion,
1701
+ latest: cache.latest
1702
+ };
1703
+ }
1704
+ const latest = await fetchLatestVersion();
1705
+ if (!latest) return noUpdate;
1706
+ writeCache({ latest, checkedAt: Date.now() });
1707
+ return {
1708
+ hasUpdate: compareVersions(currentVersion, latest),
1709
+ current: currentVersion,
1710
+ latest
1711
+ };
1712
+ }
1713
+
1714
+ // src/components/update-banner.tsx
1715
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1716
+ function UpdateBanner({ currentVersion }) {
1717
+ const [info, setInfo] = useState8(null);
1718
+ useEffect6(() => {
1719
+ let cancelled = false;
1720
+ checkForUpdate(currentVersion).then((result) => {
1721
+ if (!cancelled && result.hasUpdate) {
1722
+ setInfo(result);
1723
+ }
1724
+ });
1725
+ return () => {
1726
+ cancelled = true;
1727
+ };
1728
+ }, [currentVersion]);
1729
+ if (!info) return null;
1730
+ return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
1731
+ "\u{1F4A1} ",
1732
+ "\u65B0\u7248\u672C\u53EF\u7528 ",
1733
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "green", children: info.latest }),
1734
+ /* @__PURE__ */ jsxs10(Text10, { color: "gray", children: [
1735
+ " (\u5F53\u524D ",
1736
+ info.current,
1737
+ ")"
1738
+ ] }),
1739
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: " \u2192 npm i -g git-sync-tui" })
1740
+ ] }) });
1741
+ }
1742
+
1615
1743
  // src/app.tsx
1616
1744
  import { execSync } from "child_process";
1617
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1745
+ import { createRequire } from "module";
1746
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1747
+ var require2 = createRequire(import.meta.url);
1748
+ var { version: APP_VERSION } = require2("../package.json");
1618
1749
  var STEP_NUMBER = {
1619
1750
  checking: 0,
1620
1751
  "stash-recovery": 0,
@@ -1630,17 +1761,17 @@ var STEP_DEBOUNCE = 100;
1630
1761
  function App({ initialRemote, initialBranch }) {
1631
1762
  const { exit } = useApp();
1632
1763
  const entryStep = initialRemote && initialBranch ? "branch-check" : initialRemote ? "branch" : "remote";
1633
- const [step, setStepRaw] = useState8("checking");
1634
- const [inputReady, setInputReady] = useState8(true);
1635
- const [remote2, setRemote] = useState8(initialRemote || "");
1636
- const [branch2, setBranch] = useState8(initialBranch || "");
1637
- const [selectedHashes, setSelectedHashes] = useState8([]);
1638
- const [commits2, setCommits] = useState8([]);
1639
- const [hasMerge, setHasMerge] = useState8(false);
1640
- const [useMainline, setUseMainline] = useState8(false);
1641
- const [noCommit, setNoCommit] = useState8(false);
1642
- const [stashed, setStashed] = useState8(false);
1643
- const [guardTimestamp, setGuardTimestamp] = useState8();
1764
+ const [step, setStepRaw] = useState9("checking");
1765
+ const [inputReady, setInputReady] = useState9(true);
1766
+ const [remote2, setRemote] = useState9(initialRemote || "");
1767
+ const [branch2, setBranch] = useState9(initialBranch || "");
1768
+ const [selectedHashes, setSelectedHashes] = useState9([]);
1769
+ const [commits2, setCommits] = useState9([]);
1770
+ const [hasMerge, setHasMerge] = useState9(false);
1771
+ const [useMainline, setUseMainline] = useState9(false);
1772
+ const [noCommit, setNoCommit] = useState9(false);
1773
+ const [stashed, setStashed] = useState9(false);
1774
+ const [guardTimestamp, setGuardTimestamp] = useState9();
1644
1775
  const stashedRef = useRef5(false);
1645
1776
  const stashRestoredRef = useRef5(false);
1646
1777
  const mountedRef = useRef5(true);
@@ -1671,7 +1802,7 @@ function App({ initialRemote, initialBranch }) {
1671
1802
  stashRestoredRef.current = true;
1672
1803
  removeStashGuard();
1673
1804
  }, []);
1674
- useEffect6(() => {
1805
+ useEffect7(() => {
1675
1806
  mountedRef.current = true;
1676
1807
  async function check() {
1677
1808
  const guard = await checkStashGuard();
@@ -1741,10 +1872,10 @@ function App({ initialRemote, initialBranch }) {
1741
1872
  exit();
1742
1873
  }
1743
1874
  }, [setStep, restoreStashSync, exit]);
1744
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
1745
- /* @__PURE__ */ jsx10(AppHeader, { step: STEP_NUMBER[step], stashed, noCommit }),
1746
- step === "checking" && /* @__PURE__ */ jsx10(Spinner7, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1747
- step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx10(
1875
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
1876
+ /* @__PURE__ */ jsx11(AppHeader, { step: STEP_NUMBER[step], stashed, noCommit }),
1877
+ step === "checking" && /* @__PURE__ */ jsx11(Spinner7, { label: "\u68C0\u67E5\u5DE5\u4F5C\u533A\u72B6\u6001..." }),
1878
+ step === "stash-recovery" && inputReady && /* @__PURE__ */ jsx11(
1748
1879
  StashRecovery,
1749
1880
  {
1750
1881
  timestamp: guardTimestamp,
@@ -1752,14 +1883,14 @@ function App({ initialRemote, initialBranch }) {
1752
1883
  onSkip: skipStashRecover
1753
1884
  }
1754
1885
  ),
1755
- step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx10(
1886
+ step === "stash-prompt" && inputReady && /* @__PURE__ */ jsx11(
1756
1887
  StashPrompt,
1757
1888
  {
1758
1889
  onConfirm: doStash,
1759
1890
  onSkip: () => setStep(entryStep)
1760
1891
  }
1761
1892
  ),
1762
- step === "remote" && inputReady && /* @__PURE__ */ jsx10(
1893
+ step === "remote" && inputReady && /* @__PURE__ */ jsx11(
1763
1894
  RemoteSelect,
1764
1895
  {
1765
1896
  onSelect: (r) => {
@@ -1769,7 +1900,7 @@ function App({ initialRemote, initialBranch }) {
1769
1900
  onBack: () => goBack("remote")
1770
1901
  }
1771
1902
  ),
1772
- step === "branch" && inputReady && /* @__PURE__ */ jsx10(
1903
+ step === "branch" && inputReady && /* @__PURE__ */ jsx11(
1773
1904
  BranchSelect,
1774
1905
  {
1775
1906
  remote: remote2,
@@ -1780,7 +1911,7 @@ function App({ initialRemote, initialBranch }) {
1780
1911
  onBack: () => goBack("branch")
1781
1912
  }
1782
1913
  ),
1783
- step === "branch-check" && inputReady && /* @__PURE__ */ jsx10(
1914
+ step === "branch-check" && inputReady && /* @__PURE__ */ jsx11(
1784
1915
  BranchCheck,
1785
1916
  {
1786
1917
  targetBranch: branch2,
@@ -1788,7 +1919,7 @@ function App({ initialRemote, initialBranch }) {
1788
1919
  onBack: () => goBack("branch-check")
1789
1920
  }
1790
1921
  ),
1791
- step === "commits" && inputReady && /* @__PURE__ */ jsx10(
1922
+ step === "commits" && inputReady && /* @__PURE__ */ jsx11(
1792
1923
  CommitList,
1793
1924
  {
1794
1925
  remote: remote2,
@@ -1801,7 +1932,7 @@ function App({ initialRemote, initialBranch }) {
1801
1932
  onBack: () => goBack("commits")
1802
1933
  }
1803
1934
  ),
1804
- step === "confirm" && inputReady && /* @__PURE__ */ jsx10(
1935
+ step === "confirm" && inputReady && /* @__PURE__ */ jsx11(
1805
1936
  ConfirmPanel,
1806
1937
  {
1807
1938
  commits: commits2,
@@ -1815,7 +1946,7 @@ function App({ initialRemote, initialBranch }) {
1815
1946
  onCancel: () => goBack("confirm")
1816
1947
  }
1817
1948
  ),
1818
- step === "result" && /* @__PURE__ */ jsx10(
1949
+ step === "result" && /* @__PURE__ */ jsx11(
1819
1950
  ResultPanel,
1820
1951
  {
1821
1952
  selectedHashes,
@@ -1828,12 +1959,16 @@ function App({ initialRemote, initialBranch }) {
1828
1959
  exit();
1829
1960
  }
1830
1961
  }
1831
- )
1962
+ ),
1963
+ /* @__PURE__ */ jsx11(UpdateBanner, { currentVersion: APP_VERSION })
1832
1964
  ] });
1833
1965
  }
1834
1966
 
1835
1967
  // src/cli-runner.ts
1836
1968
  import { createInterface } from "readline";
1969
+ import { createRequire as createRequire2 } from "module";
1970
+ var require3 = createRequire2(import.meta.url);
1971
+ var { version: APP_VERSION2 } = require3("../package.json");
1837
1972
  function log(msg) {
1838
1973
  process.stdout.write(msg + "\n");
1839
1974
  }
@@ -1988,16 +2123,24 @@ ${stat}`);
1988
2123
  process.exit(1);
1989
2124
  }
1990
2125
  }
2126
+ async function printUpdateNotice() {
2127
+ const info = await checkForUpdate(APP_VERSION2);
2128
+ if (info.hasUpdate) {
2129
+ log(`
2130
+ \u{1F4A1} \u65B0\u7248\u672C\u53EF\u7528 ${info.latest} (\u5F53\u524D ${info.current}) \u2192 npm i -g git-sync-tui`);
2131
+ }
2132
+ }
1991
2133
  async function runCli(opts) {
1992
2134
  if (opts.list) {
1993
2135
  await runList(opts);
1994
2136
  } else {
1995
2137
  await runExec(opts);
1996
2138
  }
2139
+ await printUpdateNotice();
1997
2140
  }
1998
2141
 
1999
2142
  // src/cli.tsx
2000
- import { jsx as jsx11 } from "react/jsx-runtime";
2143
+ import { jsx as jsx12 } from "react/jsx-runtime";
2001
2144
  var cli = meow(
2002
2145
  `
2003
2146
  \u7528\u6CD5
@@ -2071,5 +2214,5 @@ if (isCliMode) {
2071
2214
  process.exit(1);
2072
2215
  });
2073
2216
  } else {
2074
- render(/* @__PURE__ */ jsx11(App, { initialRemote: remote, initialBranch: branch }));
2217
+ render(/* @__PURE__ */ jsx12(App, { initialRemote: remote, initialBranch: branch }));
2075
2218
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-sync-tui",
3
3
  "type": "module",
4
- "version": "0.1.6",
4
+ "version": "0.1.7",
5
5
  "packageManager": "pnpm@10.32.1",
6
6
  "description": "Interactive TUI tool for cross-repo git commit synchronization (cherry-pick --no-commit)",
7
7
  "author": "KiWi233333",