@zjex/git-workflow 0.2.20 → 0.2.22

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.
@@ -0,0 +1,169 @@
1
+ # 代码文档说明
2
+
3
+ ## 已添加详细注释的文件
4
+
5
+ ### ✅ src/commands/commit.ts
6
+
7
+ **功能**: 交互式提交命令,支持 AI 自动生成和手动编写
8
+
9
+ **注释内容**:
10
+
11
+ - 模块级注释:说明 Conventional Commits 和 Gitmoji 规范
12
+ - 函数注释:每个函数都有 JSDoc 注释
13
+ - 步骤标记:用 `==========` 分隔不同的处理步骤
14
+ - 行内注释:关键逻辑都有说明
15
+
16
+ **关键函数**:
17
+
18
+ - `commit()`: 主函数,处理整个提交流程
19
+ - `buildManualCommitMessage()`: 手动构建 commit message
20
+ - `parseGitStatus()`: 解析 git 状态
21
+ - `formatFileStatus()`: 格式化文件状态显示
22
+
23
+ ### ✅ src/index.ts
24
+
25
+ **功能**: 主入口文件,CLI 应用初始化和命令注册
26
+
27
+ **注释内容**:
28
+
29
+ - 模块级注释:说明文件职责
30
+ - 全局错误处理:Ctrl+C、未捕获异常、Promise 拒绝
31
+ - 版本信息:构建时注入
32
+ - 交互式主菜单:ASCII Logo 和命令选择
33
+ - CLI 命令注册:所有命令的注册和配置
34
+
35
+ ## 其他文件状态
36
+
37
+ ### ✅ 无错误的文件(已通过 TypeScript 检查)
38
+
39
+ - src/ai-service.ts
40
+ - src/config.ts
41
+ - src/utils.ts
42
+ - src/update-notifier.ts
43
+ - src/commands/branch.ts
44
+ - src/commands/help.ts
45
+ - src/commands/init.ts
46
+ - src/commands/release.ts
47
+ - src/commands/stash.ts
48
+ - src/commands/tag.ts
49
+ - src/commands/update.ts
50
+
51
+ ## 代码质量改进
52
+
53
+ ### 1. 类型安全
54
+
55
+ - 所有函数都有明确的返回类型
56
+ - 使用 TypeScript 的类型推导
57
+ - 接口定义清晰(如 `FileStatus`)
58
+
59
+ ### 2. 错误处理
60
+
61
+ - 全局错误捕获
62
+ - 优雅的 Ctrl+C 处理
63
+ - 详细的错误提示信息
64
+ - 提供手动执行命令
65
+
66
+ ### 3. 用户体验
67
+
68
+ - 提交前检查暂存文件
69
+ - 清晰的步骤分隔
70
+ - 彩色输出和 emoji
71
+ - 交互式选择界面
72
+
73
+ ### 4. 代码组织
74
+
75
+ - 模块化设计
76
+ - 单一职责原则
77
+ - 清晰的函数命名
78
+ - 详细的注释说明
79
+
80
+ ## 关键改进点
81
+
82
+ ### commit.ts
83
+
84
+ 1. **修复了 `message` 变量未初始化问题**
85
+
86
+ - 初始化为空字符串
87
+ - 确保所有代码路径都会赋值
88
+
89
+ 2. **添加提交前检查**
90
+
91
+ - 再次验证是否有暂存文件
92
+ - 避免空提交
93
+
94
+ 3. **改进错误提示**
95
+
96
+ - 显示完整错误信息
97
+ - 提供手动执行命令
98
+
99
+ 4. **修复 refactor 对齐问题**
100
+ - 针对 ♻️ emoji 特殊处理
101
+ - 动态调整间距
102
+
103
+ ### index.ts
104
+
105
+ 1. **完善错误处理**
106
+
107
+ - 捕获所有类型的错误
108
+ - 优雅退出
109
+
110
+ 2. **添加模块说明**
111
+ - 清晰的文件职责
112
+ - 详细的功能说明
113
+
114
+ ## 建议
115
+
116
+ 如果需要给其他文件添加详细注释,可以按照以下优先级:
117
+
118
+ 1. **高优先级**(核心业务逻辑):
119
+
120
+ - src/ai-service.ts - AI commit 生成
121
+ - src/config.ts - 配置管理
122
+ - src/commands/tag.ts - Tag 管理
123
+
124
+ 2. **中优先级**(常用功能):
125
+
126
+ - src/commands/branch.ts - 分支管理
127
+ - src/commands/stash.ts - Stash 管理
128
+ - src/update-notifier.ts - 更新检查
129
+
130
+ 3. **低优先级**(辅助功能):
131
+ - src/utils.ts - 工具函数
132
+ - src/commands/help.ts - 帮助信息
133
+ - src/commands/init.ts - 初始化配置
134
+
135
+ ## 注释规范
136
+
137
+ ### JSDoc 注释格式
138
+
139
+ ```typescript
140
+ /**
141
+ * 函数简短描述
142
+ *
143
+ * 详细说明(可选)
144
+ *
145
+ * @param paramName 参数说明
146
+ * @returns 返回值说明
147
+ */
148
+ ```
149
+
150
+ ### 行内注释
151
+
152
+ ```typescript
153
+ // 简短说明当前代码的作用
154
+ const result = doSomething();
155
+ ```
156
+
157
+ ### 步骤标记
158
+
159
+ ```typescript
160
+ // ========== 步骤 1: 处理输入 ==========
161
+ // ========== 步骤 2: 验证数据 ==========
162
+ ```
163
+
164
+ ## 总结
165
+
166
+ ✅ 核心文件已添加详细注释
167
+ ✅ 所有文件通过 TypeScript 检查
168
+ ✅ 代码质量和可维护性显著提升
169
+ ✅ 用户体验得到改善
package/dist/index.js CHANGED
@@ -145,6 +145,14 @@ function backgroundCheck(currentVersion, packageName) {
145
145
  }
146
146
  });
147
147
  }
148
+ function isUsingVolta() {
149
+ try {
150
+ const whichGw = execSync6("which gw", { encoding: "utf-8" }).trim();
151
+ return whichGw.includes(".volta");
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
148
156
  async function getLatestVersion(packageName) {
149
157
  try {
150
158
  const result = execSync6(`npm view ${packageName} version`, {
@@ -192,6 +200,8 @@ async function showUpdateMessage(current, latest, packageName) {
192
200
  width: 40
193
201
  })
194
202
  );
203
+ const usingVolta = isUsingVolta();
204
+ const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
195
205
  try {
196
206
  const action = await select7({
197
207
  message: "\u4F60\u60F3\u505A\u4EC0\u4E48\uFF1F",
@@ -199,7 +209,7 @@ async function showUpdateMessage(current, latest, packageName) {
199
209
  {
200
210
  name: "\u{1F680} \u7ACB\u5373\u66F4\u65B0",
201
211
  value: "update",
202
- description: `\u8FD0\u884C npm install -g ${packageName}`
212
+ description: `\u8FD0\u884C ${updateCommand}`
203
213
  },
204
214
  {
205
215
  name: "\u23ED\uFE0F \u7A0D\u540E\u66F4\u65B0\uFF0C\u7EE7\u7EED\u4F7F\u7528",
@@ -221,12 +231,14 @@ async function showUpdateMessage(current, latest, packageName) {
221
231
  }
222
232
  async function performUpdate(packageName) {
223
233
  console.log("");
234
+ const usingVolta = isUsingVolta();
235
+ const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
224
236
  const spinner = ora5({
225
237
  text: "\u6B63\u5728\u66F4\u65B0...",
226
238
  spinner: "dots"
227
239
  }).start();
228
240
  try {
229
- execSync6(`npm install -g ${packageName}@latest`, {
241
+ execSync6(updateCommand, {
230
242
  encoding: "utf-8",
231
243
  stdio: ["pipe", "pipe", "pipe"]
232
244
  });
@@ -238,15 +250,18 @@ async function performUpdate(packageName) {
238
250
  [
239
251
  colors.green(colors.bold("\u2728 \u66F4\u65B0\u5B8C\u6210\uFF01")),
240
252
  "",
241
- colors.dim("\u8BF7\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u4F7F\u7528\u65B0\u7248\u672C")
253
+ colors.dim("\u8BF7\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u9A8C\u8BC1:"),
254
+ colors.cyan(" hash -r && gw --version"),
255
+ "",
256
+ colors.dim("\u6216\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF")
242
257
  ].join("\n"),
243
258
  {
244
- padding: { top: 1, bottom: 1, left: 3, right: 3 },
259
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
245
260
  margin: { top: 0, bottom: 1, left: 2, right: 2 },
246
261
  borderStyle: "round",
247
262
  borderColor: "green",
248
- align: "center",
249
- width: 40
263
+ align: "left",
264
+ width: 50
250
265
  }
251
266
  )
252
267
  );
@@ -255,7 +270,7 @@ async function performUpdate(packageName) {
255
270
  spinner.fail(colors.red("\u66F4\u65B0\u5931\u8D25"));
256
271
  console.log("");
257
272
  console.log(colors.dim(" \u4F60\u53EF\u4EE5\u624B\u52A8\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0:"));
258
- console.log(colors.cyan(` npm install -g ${packageName}@latest`));
273
+ console.log(colors.cyan(` ${updateCommand}`));
259
274
  console.log("");
260
275
  }
261
276
  }
@@ -978,38 +993,55 @@ async function updateTag() {
978
993
  divider();
979
994
  const choices = tags.map((tag) => ({ name: tag, value: tag }));
980
995
  choices.push({ name: "\u53D6\u6D88", value: "__cancel__" });
981
- const tagToUpdate = await select2({
982
- message: "\u9009\u62E9\u8981\u4FEE\u6539\u7684 tag:",
996
+ const oldTag = await select2({
997
+ message: "\u9009\u62E9\u8981\u91CD\u547D\u540D\u7684 tag:",
983
998
  choices,
984
999
  theme
985
1000
  });
986
- if (tagToUpdate === "__cancel__") {
1001
+ if (oldTag === "__cancel__") {
987
1002
  console.log(colors.yellow("\u5DF2\u53D6\u6D88"));
988
1003
  return;
989
1004
  }
990
- const newMessage = await input2({
991
- message: "\u8F93\u5165\u65B0\u7684 tag \u6D88\u606F:",
992
- default: `Release ${tagToUpdate}`,
1005
+ console.log("");
1006
+ console.log(colors.dim(`\u5F53\u524D tag: ${oldTag}`));
1007
+ console.log("");
1008
+ const newTag = await input2({
1009
+ message: "\u8F93\u5165\u65B0\u7684 tag \u540D\u79F0:",
1010
+ default: oldTag,
993
1011
  theme
994
1012
  });
995
- if (!newMessage) {
1013
+ if (!newTag || newTag === oldTag) {
996
1014
  console.log(colors.yellow("\u5DF2\u53D6\u6D88"));
997
1015
  return;
998
1016
  }
1017
+ const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
1018
+ if (existingTags.includes(newTag)) {
1019
+ console.log(colors.red(`Tag ${newTag} \u5DF2\u5B58\u5728\uFF0C\u65E0\u6CD5\u91CD\u547D\u540D`));
1020
+ return;
1021
+ }
999
1022
  divider();
1000
- const spinner = ora2(`\u6B63\u5728\u66F4\u65B0 tag: ${tagToUpdate}`).start();
1023
+ const spinner = ora2(`\u6B63\u5728\u91CD\u547D\u540D tag: ${oldTag} \u2192 ${newTag}`).start();
1001
1024
  try {
1002
- execSync3(`git tag -d "${tagToUpdate}"`, { stdio: "pipe" });
1003
- execSync3(`git tag -a "${tagToUpdate}" -m "${newMessage}"`, {
1004
- stdio: "pipe"
1005
- });
1006
- spinner.succeed(`Tag \u5DF2\u66F4\u65B0: ${tagToUpdate}`);
1007
- } catch {
1008
- spinner.fail("tag \u66F4\u65B0\u5931\u8D25");
1025
+ const commit2 = execOutput(`git rev-list -n 1 "${oldTag}"`).trim();
1026
+ const message = execOutput(
1027
+ `git tag -l --format='%(contents)' "${oldTag}"`
1028
+ ).trim();
1029
+ if (message) {
1030
+ execSync3(`git tag -a "${newTag}" "${commit2}" -m "${message}"`, {
1031
+ stdio: "pipe"
1032
+ });
1033
+ } else {
1034
+ execSync3(`git tag "${newTag}" "${commit2}"`, { stdio: "pipe" });
1035
+ }
1036
+ execSync3(`git tag -d "${oldTag}"`, { stdio: "pipe" });
1037
+ spinner.succeed(`Tag \u5DF2\u91CD\u547D\u540D: ${oldTag} \u2192 ${newTag}`);
1038
+ } catch (error) {
1039
+ spinner.fail("tag \u91CD\u547D\u540D\u5931\u8D25");
1040
+ console.log(colors.red(String(error)));
1009
1041
  return;
1010
1042
  }
1011
1043
  const pushRemote = await select2({
1012
- message: "\u662F\u5426\u63A8\u9001\u5230\u8FDC\u7A0B\uFF08\u4F1A\u5F3A\u5236\u8986\u76D6\uFF09?",
1044
+ message: "\u662F\u5426\u540C\u6B65\u5230\u8FDC\u7A0B?",
1013
1045
  choices: [
1014
1046
  { name: "\u662F", value: true },
1015
1047
  { name: "\u5426", value: false }
@@ -1017,13 +1049,16 @@ async function updateTag() {
1017
1049
  theme
1018
1050
  });
1019
1051
  if (pushRemote) {
1020
- const pushSpinner = ora2("\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B...").start();
1052
+ const pushSpinner = ora2("\u6B63\u5728\u540C\u6B65\u5230\u8FDC\u7A0B...").start();
1021
1053
  try {
1022
- execSync3(`git push origin "${tagToUpdate}" --force`, { stdio: "pipe" });
1023
- pushSpinner.succeed(`Tag \u5DF2\u63A8\u9001: ${tagToUpdate}`);
1054
+ execSync3(`git push origin "${newTag}"`, { stdio: "pipe" });
1055
+ execSync3(`git push origin --delete "${oldTag}"`, { stdio: "pipe" });
1056
+ pushSpinner.succeed(`\u8FDC\u7A0B tag \u5DF2\u540C\u6B65: ${oldTag} \u2192 ${newTag}`);
1024
1057
  } catch {
1025
1058
  pushSpinner.warn(
1026
- `\u8FDC\u7A0B\u63A8\u9001\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C: git push origin ${tagToUpdate} --force`
1059
+ `\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C:
1060
+ git push origin ${newTag}
1061
+ git push origin --delete ${oldTag}`
1027
1062
  );
1028
1063
  }
1029
1064
  }
@@ -1882,8 +1917,7 @@ var DEFAULT_COMMIT_TYPES = [
1882
1917
  { type: "test", emoji: "\u2705", description: "\u6D4B\u8BD5\u76F8\u5173" },
1883
1918
  { type: "build", emoji: "\u{1F4E6}", description: "\u6784\u5EFA/\u4F9D\u8D56\u76F8\u5173" },
1884
1919
  { type: "ci", emoji: "\u{1F477}", description: "CI/CD \u76F8\u5173" },
1885
- { type: "chore", emoji: "\u{1F527}", description: "\u5176\u4ED6\u6742\u9879" },
1886
- { type: "revert", emoji: "\u23EA", description: "\u56DE\u9000\u63D0\u4EA4" }
1920
+ { type: "chore", emoji: "\u{1F527}", description: "\u5176\u4ED6\u6742\u9879" }
1887
1921
  ];
1888
1922
  function getCommitTypes(config2) {
1889
1923
  const customEmojis = config2.commitEmojis || {};
@@ -1915,11 +1949,17 @@ function parseGitStatus() {
1915
1949
  function formatFileStatus(status) {
1916
1950
  const statusMap = {
1917
1951
  M: colors.yellow("M"),
1952
+ // 修改
1918
1953
  A: colors.green("A"),
1954
+ // 新增
1919
1955
  D: colors.red("D"),
1956
+ // 删除
1920
1957
  R: colors.yellow("R"),
1958
+ // 重命名
1921
1959
  C: colors.yellow("C"),
1960
+ // 复制
1922
1961
  "?": colors.green("?")
1962
+ // 未跟踪
1923
1963
  };
1924
1964
  return statusMap[status] || status;
1925
1965
  }
@@ -1994,7 +2034,7 @@ async function commit() {
1994
2034
  theme
1995
2035
  });
1996
2036
  }
1997
- let message;
2037
+ let message = "";
1998
2038
  if (commitMode === "ai") {
1999
2039
  const spinner2 = ora4("AI \u6B63\u5728\u5206\u6790\u4EE3\u7801\u53D8\u66F4...").start();
2000
2040
  try {
@@ -2049,6 +2089,17 @@ async function commit() {
2049
2089
  }
2050
2090
  const spinner = ora4("\u6B63\u5728\u63D0\u4EA4...").start();
2051
2091
  try {
2092
+ const finalStatus = parseGitStatus();
2093
+ if (finalStatus.staged.length === 0) {
2094
+ spinner.fail("\u6CA1\u6709\u6682\u5B58\u7684\u6587\u4EF6\u53EF\u4EE5\u63D0\u4EA4");
2095
+ console.log("");
2096
+ console.log(colors.yellow("\u8BF7\u5148\u6682\u5B58\u6587\u4EF6:"));
2097
+ console.log(colors.cyan(" git add <file>"));
2098
+ console.log(colors.dim(" \u6216"));
2099
+ console.log(colors.cyan(" git add -A"));
2100
+ console.log("");
2101
+ return;
2102
+ }
2052
2103
  const escapedMessage = message.replace(/"/g, '\\"');
2053
2104
  execSync5(`git commit -m "${escapedMessage}"`, { stdio: "pipe" });
2054
2105
  spinner.succeed("\u63D0\u4EA4\u6210\u529F");
@@ -2056,19 +2107,31 @@ async function commit() {
2056
2107
  console.log(colors.dim(`commit: ${commitHash}`));
2057
2108
  } catch (error) {
2058
2109
  spinner.fail("\u63D0\u4EA4\u5931\u8D25");
2110
+ console.log("");
2059
2111
  if (error instanceof Error) {
2060
- console.log(colors.red(error.message));
2112
+ console.log(colors.red("\u9519\u8BEF\u4FE1\u606F:"));
2113
+ console.log(colors.dim(` ${error.message}`));
2061
2114
  }
2115
+ console.log("");
2116
+ console.log(colors.yellow("\u4F60\u53EF\u4EE5\u624B\u52A8\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4:"));
2117
+ console.log(colors.cyan(` git commit -m "${message}"`));
2118
+ console.log("");
2062
2119
  }
2063
2120
  }
2064
2121
  async function buildManualCommitMessage(config2) {
2065
2122
  const commitTypes = getCommitTypes(config2);
2066
2123
  const typeChoice = await select6({
2067
2124
  message: "\u9009\u62E9\u63D0\u4EA4\u7C7B\u578B:",
2068
- choices: commitTypes.map((t) => ({
2069
- name: `${t.emoji} ${t.type.padEnd(10)} ${colors.dim(t.description)}`,
2070
- value: t
2071
- })),
2125
+ choices: commitTypes.map((t) => {
2126
+ const typeText = t.type.padEnd(10);
2127
+ const spacing = t.type === "refactor" ? " " : " ";
2128
+ return {
2129
+ name: `${t.emoji}${spacing}${typeText} ${colors.dim(t.description)}`,
2130
+ value: t
2131
+ };
2132
+ }),
2133
+ pageSize: commitTypes.length,
2134
+ // 显示所有选项,不滚动
2072
2135
  theme
2073
2136
  });
2074
2137
  const scope = await input5({
@@ -2161,7 +2224,7 @@ Tag \u547D\u4EE4:
2161
2224
  gw tag:delete \u5220\u9664 tag
2162
2225
  gw td \u540C\u4E0A (\u522B\u540D)
2163
2226
 
2164
- gw tag:update \u4FEE\u6539 tag \u6D88\u606F
2227
+ gw tag:update \u91CD\u547D\u540D tag
2165
2228
  gw tu \u540C\u4E0A (\u522B\u540D)
2166
2229
 
2167
2230
  \u53D1\u5E03\u547D\u4EE4:
@@ -2245,8 +2308,17 @@ async function getLatestVersion2(packageName) {
2245
2308
  return null;
2246
2309
  }
2247
2310
  }
2311
+ function isUsingVolta2() {
2312
+ try {
2313
+ const whichGw = execSync7("which gw", { encoding: "utf-8" }).trim();
2314
+ return whichGw.includes(".volta");
2315
+ } catch {
2316
+ return false;
2317
+ }
2318
+ }
2248
2319
  async function update(currentVersion) {
2249
2320
  const packageName = "@zjex/git-workflow";
2321
+ const usingVolta = isUsingVolta2();
2250
2322
  console.log("");
2251
2323
  console.log(colors.bold("\u{1F50D} \u68C0\u67E5\u66F4\u65B0..."));
2252
2324
  console.log("");
@@ -2279,12 +2351,6 @@ async function update(currentVersion) {
2279
2351
  return;
2280
2352
  }
2281
2353
  const versionText = `${currentVersion} \u2192 ${latestVersion}`;
2282
- const maxWidth = Math.max(
2283
- "\u{1F389} \u53D1\u73B0\u65B0\u7248\u672C\uFF01".length,
2284
- versionText.length,
2285
- "\u2728 \u66F4\u65B0\u5B8C\u6210\uFF01".length,
2286
- "\u8BF7\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u4F7F\u7528\u65B0\u7248\u672C".length
2287
- );
2288
2354
  console.log(
2289
2355
  boxen2(
2290
2356
  [
@@ -2305,7 +2371,8 @@ async function update(currentVersion) {
2305
2371
  )
2306
2372
  );
2307
2373
  const updateSpinner = ora6("\u6B63\u5728\u66F4\u65B0...").start();
2308
- execSync7(`npm install -g ${packageName}@latest`, {
2374
+ const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
2375
+ execSync7(updateCommand, {
2309
2376
  encoding: "utf-8",
2310
2377
  stdio: ["pipe", "pipe", "pipe"]
2311
2378
  });
@@ -2317,15 +2384,20 @@ async function update(currentVersion) {
2317
2384
  [
2318
2385
  colors.green(colors.bold("\u2728 \u66F4\u65B0\u5B8C\u6210\uFF01")),
2319
2386
  "",
2320
- colors.dim("\u8BF7\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u4F7F\u7528\u65B0\u7248\u672C")
2387
+ `\u65B0\u7248\u672C: ${colors.green(colors.bold(latestVersion))}`,
2388
+ "",
2389
+ colors.dim("\u8BF7\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u9A8C\u8BC1:"),
2390
+ colors.cyan(" hash -r && gw --version"),
2391
+ "",
2392
+ colors.dim("\u6216\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF")
2321
2393
  ].join("\n"),
2322
2394
  {
2323
- padding: { top: 1, bottom: 1, left: 3, right: 3 },
2395
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
2324
2396
  margin: { top: 0, bottom: 1, left: 2, right: 2 },
2325
2397
  borderStyle: "round",
2326
2398
  borderColor: "green",
2327
- align: "center",
2328
- width: 40
2399
+ align: "left",
2400
+ width: 50
2329
2401
  }
2330
2402
  )
2331
2403
  );
@@ -2334,7 +2406,8 @@ async function update(currentVersion) {
2334
2406
  spinner.fail(colors.red("\u66F4\u65B0\u5931\u8D25"));
2335
2407
  console.log("");
2336
2408
  console.log(colors.dim(" \u4F60\u53EF\u4EE5\u624B\u52A8\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0:"));
2337
- console.log(colors.cyan(` npm install -g ${packageName}@latest`));
2409
+ const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
2410
+ console.log(colors.cyan(` ${updateCommand}`));
2338
2411
  console.log("");
2339
2412
  process.exit(1);
2340
2413
  }
@@ -2365,7 +2438,7 @@ process.on("SIGTERM", () => {
2365
2438
  console.log("");
2366
2439
  process.exit(0);
2367
2440
  });
2368
- var version = true ? "0.2.20" : "0.0.0-dev";
2441
+ var version = true ? "0.2.22" : "0.0.0-dev";
2369
2442
  async function mainMenu() {
2370
2443
  console.log(
2371
2444
  colors.green(`
@@ -2407,7 +2480,7 @@ async function mainMenu() {
2407
2480
  value: "tag-delete"
2408
2481
  },
2409
2482
  {
2410
- name: `[7] \u270F\uFE0F \u4FEE\u6539 tag ${colors.dim("gw tu")}`,
2483
+ name: `[7] \u270F\uFE0F \u91CD\u547D\u540D tag ${colors.dim("gw tu")}`,
2411
2484
  value: "tag-update"
2412
2485
  },
2413
2486
  {
@@ -2517,7 +2590,7 @@ cli.command("tag:delete", "\u5220\u9664 tag").alias("td").action(async () => {
2517
2590
  checkGitRepo();
2518
2591
  return deleteTag();
2519
2592
  });
2520
- cli.command("tag:update", "\u4FEE\u6539 tag \u6D88\u606F").alias("tu").action(async () => {
2593
+ cli.command("tag:update", "\u91CD\u547D\u540D tag").alias("tu").action(async () => {
2521
2594
  await checkForUpdates(version, "@zjex/git-workflow");
2522
2595
  checkGitRepo();
2523
2596
  return updateTag();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjex/git-workflow",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,7 +5,11 @@ import { colors, theme, execOutput, divider } from "../utils.js";
5
5
  import { getConfig } from "../config.js";
6
6
  import { generateAICommitMessage, isAICommitAvailable } from "../ai-service.js";
7
7
 
8
- // Conventional Commits 类型 + Gitmoji
8
+ /**
9
+ * Conventional Commits 类型定义 + Gitmoji
10
+ * 遵循 https://www.conventionalcommits.org/ 规范
11
+ * 使用 https://gitmoji.dev/ emoji
12
+ */
9
13
  const DEFAULT_COMMIT_TYPES = [
10
14
  { type: "feat", emoji: "✨", description: "新功能" },
11
15
  { type: "fix", emoji: "🐛", description: "修复 Bug" },
@@ -17,11 +21,15 @@ const DEFAULT_COMMIT_TYPES = [
17
21
  { type: "build", emoji: "📦", description: "构建/依赖相关" },
18
22
  { type: "ci", emoji: "👷", description: "CI/CD 相关" },
19
23
  { type: "chore", emoji: "🔧", description: "其他杂项" },
20
- { type: "revert", emoji: "⏪", description: "回退提交" },
21
24
  ] as const;
22
25
 
23
26
  type CommitType = (typeof DEFAULT_COMMIT_TYPES)[number]["type"];
24
27
 
28
+ /**
29
+ * 获取提交类型列表(支持自定义 emoji)
30
+ * @param config 用户配置
31
+ * @returns 提交类型列表
32
+ */
25
33
  function getCommitTypes(config: ReturnType<typeof getConfig>) {
26
34
  const customEmojis = config.commitEmojis || {};
27
35
  return DEFAULT_COMMIT_TYPES.map((item) => ({
@@ -30,11 +38,18 @@ function getCommitTypes(config: ReturnType<typeof getConfig>) {
30
38
  }));
31
39
  }
32
40
 
41
+ /**
42
+ * 文件状态接口
43
+ */
33
44
  interface FileStatus {
34
- status: string;
35
- file: string;
45
+ status: string; // M=修改, A=新增, D=删除, ?=未跟踪
46
+ file: string; // 文件路径
36
47
  }
37
48
 
49
+ /**
50
+ * 解析 git status 输出
51
+ * @returns 已暂存和未暂存的文件列表
52
+ */
38
53
  function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
39
54
  const output = execOutput("git status --porcelain");
40
55
  if (!output) return { staged: [], unstaged: [] };
@@ -44,9 +59,9 @@ function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
44
59
 
45
60
  for (const line of output.split("\n")) {
46
61
  if (!line) continue;
47
- const indexStatus = line[0];
48
- const workTreeStatus = line[1];
49
- const file = line.slice(3);
62
+ const indexStatus = line[0]; // 暂存区状态
63
+ const workTreeStatus = line[1]; // 工作区状态
64
+ const file = line.slice(3); // 文件路径
50
65
 
51
66
  // 已暂存的更改 (index 有状态)
52
67
  if (indexStatus !== " " && indexStatus !== "?") {
@@ -63,23 +78,33 @@ function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
63
78
  return { staged, unstaged };
64
79
  }
65
80
 
81
+ /**
82
+ * 格式化文件状态显示(带颜色)
83
+ * @param status 文件状态
84
+ * @returns 带颜色的状态字符串
85
+ */
66
86
  function formatFileStatus(status: string): string {
67
87
  const statusMap: Record<string, string> = {
68
- M: colors.yellow("M"),
69
- A: colors.green("A"),
70
- D: colors.red("D"),
71
- R: colors.yellow("R"),
72
- C: colors.yellow("C"),
73
- "?": colors.green("?"),
88
+ M: colors.yellow("M"), // 修改
89
+ A: colors.green("A"), // 新增
90
+ D: colors.red("D"), // 删除
91
+ R: colors.yellow("R"), // 重命名
92
+ C: colors.yellow("C"), // 复制
93
+ "?": colors.green("?"), // 未跟踪
74
94
  };
75
95
  return statusMap[status] || status;
76
96
  }
77
97
 
98
+ /**
99
+ * 交互式提交命令
100
+ * 支持 AI 自动生成和手动编写两种模式
101
+ * 遵循 Conventional Commits 规范
102
+ */
78
103
  export async function commit(): Promise<void> {
79
104
  const config = getConfig();
80
105
  let { staged, unstaged } = parseGitStatus();
81
106
 
82
- // 如果有未暂存的更改,根据配置决定是否自动暂存
107
+ // ========== 步骤 1: 处理未暂存的文件 ==========
83
108
  if (unstaged.length > 0) {
84
109
  const autoStage = config.autoStage ?? true;
85
110
 
@@ -131,7 +156,7 @@ export async function commit(): Promise<void> {
131
156
  }
132
157
  }
133
158
 
134
- // 没有暂存的更改
159
+ // ========== 步骤 2: 检查是否有文件可提交 ==========
135
160
  if (staged.length === 0) {
136
161
  console.log(colors.yellow("工作区干净,没有需要提交的更改"));
137
162
  return;
@@ -144,7 +169,7 @@ export async function commit(): Promise<void> {
144
169
  }
145
170
  divider();
146
171
 
147
- // 询问用户选择手动还是 AI 生成
172
+ // ========== 步骤 3: 选择提交方式(AI 或手动)==========
148
173
  const aiAvailable = isAICommitAvailable(config);
149
174
  let commitMode: "ai" | "manual" = "manual";
150
175
 
@@ -167,10 +192,12 @@ export async function commit(): Promise<void> {
167
192
  });
168
193
  }
169
194
 
170
- let message: string;
195
+ // 初始化 commit message 变量
196
+ let message: string = "";
171
197
 
198
+ // ========== 步骤 4: 生成 commit message ==========
199
+ // AI 生成模式
172
200
  if (commitMode === "ai") {
173
- // AI 生成模式
174
201
  const spinner = ora("AI 正在分析代码变更...").start();
175
202
 
176
203
  try {
@@ -208,11 +235,12 @@ export async function commit(): Promise<void> {
208
235
  }
209
236
  }
210
237
 
238
+ // 手动输入模式
211
239
  if (commitMode === "manual") {
212
- // 手动输入模式(原有逻辑)
213
240
  message = await buildManualCommitMessage(config);
214
241
  }
215
242
 
243
+ // ========== 步骤 5: 预览并确认提交 ==========
216
244
  divider();
217
245
  console.log("提交信息预览:");
218
246
  console.log(colors.green(message));
@@ -232,9 +260,23 @@ export async function commit(): Promise<void> {
232
260
  return;
233
261
  }
234
262
 
263
+ // ========== 步骤 6: 执行提交 ==========
235
264
  const spinner = ora("正在提交...").start();
236
265
 
237
266
  try {
267
+ // 提交前再次检查是否有暂存的文件
268
+ const finalStatus = parseGitStatus();
269
+ if (finalStatus.staged.length === 0) {
270
+ spinner.fail("没有暂存的文件可以提交");
271
+ console.log("");
272
+ console.log(colors.yellow("请先暂存文件:"));
273
+ console.log(colors.cyan(" git add <file>"));
274
+ console.log(colors.dim(" 或"));
275
+ console.log(colors.cyan(" git add -A"));
276
+ console.log("");
277
+ return;
278
+ }
279
+
238
280
  // 使用 -m 参数,需要转义引号
239
281
  const escapedMessage = message.replace(/"/g, '\\"');
240
282
  execSync(`git commit -m "${escapedMessage}"`, { stdio: "pipe" });
@@ -245,14 +287,26 @@ export async function commit(): Promise<void> {
245
287
  console.log(colors.dim(`commit: ${commitHash}`));
246
288
  } catch (error) {
247
289
  spinner.fail("提交失败");
290
+ console.log("");
291
+
292
+ // 显示详细错误信息
248
293
  if (error instanceof Error) {
249
- console.log(colors.red(error.message));
294
+ console.log(colors.red("错误信息:"));
295
+ console.log(colors.dim(` ${error.message}`));
250
296
  }
297
+
298
+ console.log("");
299
+ console.log(colors.yellow("你可以手动执行以下命令:"));
300
+ console.log(colors.cyan(` git commit -m "${message}"`));
301
+ console.log("");
251
302
  }
252
303
  }
253
304
 
254
305
  /**
255
306
  * 手动构建 commit message
307
+ * 通过交互式问答收集信息,构建符合 Conventional Commits 规范的提交信息
308
+ * @param config 用户配置
309
+ * @returns 完整的 commit message
256
310
  */
257
311
  async function buildManualCommitMessage(
258
312
  config: ReturnType<typeof getConfig>
@@ -260,23 +314,30 @@ async function buildManualCommitMessage(
260
314
  // 获取提交类型(支持自定义 emoji)
261
315
  const commitTypes = getCommitTypes(config);
262
316
 
263
- // 选择提交类型
317
+ // ========== 1. 选择提交类型 ==========
264
318
  const typeChoice = await select({
265
319
  message: "选择提交类型:",
266
- choices: commitTypes.map((t) => ({
267
- name: `${t.emoji} ${t.type.padEnd(10)} ${colors.dim(t.description)}`,
268
- value: t,
269
- })),
320
+ choices: commitTypes.map((t) => {
321
+ // 使用固定宽度格式化,不依赖 emoji 宽度
322
+ const typeText = t.type.padEnd(10);
323
+ // 针对 refactor 特殊处理,因为 ♻️ emoji 在不同终端宽度不一致
324
+ const spacing = t.type === "refactor" ? " " : " ";
325
+ return {
326
+ name: `${t.emoji}${spacing}${typeText} ${colors.dim(t.description)}`,
327
+ value: t,
328
+ };
329
+ }),
330
+ pageSize: commitTypes.length, // 显示所有选项,不滚动
270
331
  theme,
271
332
  });
272
333
 
273
- // 输入 scope (可选)
334
+ // ========== 2. 输入 scope (可选) ==========
274
335
  const scope = await input({
275
336
  message: "输入影响范围 scope (可跳过):",
276
337
  theme,
277
338
  });
278
339
 
279
- // 输入简短描述
340
+ // ========== 3. 输入简短描述 (必填) ==========
280
341
  const subject = await input({
281
342
  message: "输入简短描述:",
282
343
  validate: (value) => {
@@ -287,13 +348,13 @@ async function buildManualCommitMessage(
287
348
  theme,
288
349
  });
289
350
 
290
- // 输入详细描述 (可选)
351
+ // ========== 4. 输入详细描述 (可选) ==========
291
352
  const body = await input({
292
353
  message: "输入详细描述 (可跳过):",
293
354
  theme,
294
355
  });
295
356
 
296
- // 是否有破坏性变更
357
+ // ========== 5. 是否有破坏性变更 ==========
297
358
  const hasBreaking = await select({
298
359
  message: "是否包含破坏性变更 (BREAKING CHANGE)?",
299
360
  choices: [
@@ -312,13 +373,13 @@ async function buildManualCommitMessage(
312
373
  });
313
374
  }
314
375
 
315
- // 关联 Issue (可选)
376
+ // ========== 6. 关联 Issue (可选) ==========
316
377
  const issues = await input({
317
378
  message: "关联 Issue (如 #123, 可跳过):",
318
379
  theme,
319
380
  });
320
381
 
321
- // 构建 commit message
382
+ // ========== 7. 构建 commit message ==========
322
383
  const { type, emoji } = typeChoice;
323
384
  const scopePart = scope ? `(${scope})` : "";
324
385
  const breakingMark = hasBreaking ? "!" : "";
@@ -330,7 +391,7 @@ async function buildManualCommitMessage(
330
391
  // Header: [emoji] type(scope)!: subject
331
392
  let message = `${emojiPrefix}${type}${scopePart}${breakingMark}: ${subject}`;
332
393
 
333
- // Body
394
+ // Body (可选)
334
395
  if (body || hasBreaking || issues) {
335
396
  message += "\n";
336
397
 
@@ -25,7 +25,7 @@ Tag 命令:
25
25
  gw tag:delete 删除 tag
26
26
  gw td 同上 (别名)
27
27
 
28
- gw tag:update 修改 tag 消息
28
+ gw tag:update 重命名 tag
29
29
  gw tu 同上 (别名)
30
30
 
31
31
  发布命令:
@@ -417,7 +417,7 @@ export async function deleteTag(): Promise<void> {
417
417
  }
418
418
 
419
419
  /**
420
- * 修改 tag(重新打标签)
420
+ * 修改 tag 名称(重命名 tag)
421
421
  */
422
422
  export async function updateTag(): Promise<void> {
423
423
  const fetchSpinner = ora("正在获取 tags...").start();
@@ -438,47 +438,71 @@ export async function updateTag(): Promise<void> {
438
438
  const choices = tags.map((tag) => ({ name: tag, value: tag }));
439
439
  choices.push({ name: "取消", value: "__cancel__" });
440
440
 
441
- const tagToUpdate = await select({
442
- message: "选择要修改的 tag:",
441
+ const oldTag = await select({
442
+ message: "选择要重命名的 tag:",
443
443
  choices,
444
444
  theme,
445
445
  });
446
446
 
447
- if (tagToUpdate === "__cancel__") {
447
+ if (oldTag === "__cancel__") {
448
448
  console.log(colors.yellow("已取消"));
449
449
  return;
450
450
  }
451
451
 
452
- const newMessage = await input({
453
- message: "输入新的 tag 消息:",
454
- default: `Release ${tagToUpdate}`,
452
+ console.log("");
453
+ console.log(colors.dim(`当前 tag: ${oldTag}`));
454
+ console.log("");
455
+
456
+ const newTag = await input({
457
+ message: "输入新的 tag 名称:",
458
+ default: oldTag,
455
459
  theme,
456
460
  });
457
461
 
458
- if (!newMessage) {
462
+ if (!newTag || newTag === oldTag) {
459
463
  console.log(colors.yellow("已取消"));
460
464
  return;
461
465
  }
462
466
 
467
+ // 检查新 tag 是否已存在
468
+ const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
469
+ if (existingTags.includes(newTag)) {
470
+ console.log(colors.red(`Tag ${newTag} 已存在,无法重命名`));
471
+ return;
472
+ }
473
+
463
474
  divider();
464
475
 
465
- const spinner = ora(`正在更新 tag: ${tagToUpdate}`).start();
476
+ const spinner = ora(`正在重命名 tag: ${oldTag} → ${newTag}`).start();
466
477
 
467
478
  try {
479
+ // 获取旧 tag 的 commit 和消息
480
+ const commit = execOutput(`git rev-list -n 1 "${oldTag}"`).trim();
481
+ const message = execOutput(
482
+ `git tag -l --format='%(contents)' "${oldTag}"`
483
+ ).trim();
484
+
485
+ // 创建新 tag(指向同一个 commit)
486
+ if (message) {
487
+ execSync(`git tag -a "${newTag}" "${commit}" -m "${message}"`, {
488
+ stdio: "pipe",
489
+ });
490
+ } else {
491
+ execSync(`git tag "${newTag}" "${commit}"`, { stdio: "pipe" });
492
+ }
493
+
468
494
  // 删除旧 tag
469
- execSync(`git tag -d "${tagToUpdate}"`, { stdio: "pipe" });
470
- // 创建新 tag(在同一个 commit 上)
471
- execSync(`git tag -a "${tagToUpdate}" -m "${newMessage}"`, {
472
- stdio: "pipe",
473
- });
474
- spinner.succeed(`Tag 已更新: ${tagToUpdate}`);
475
- } catch {
476
- spinner.fail("tag 更新失败");
495
+ execSync(`git tag -d "${oldTag}"`, { stdio: "pipe" });
496
+
497
+ spinner.succeed(`Tag 已重命名: ${oldTag} ${newTag}`);
498
+ } catch (error) {
499
+ spinner.fail("tag 重命名失败");
500
+ console.log(colors.red(String(error)));
477
501
  return;
478
502
  }
479
503
 
480
504
  const pushRemote = await select({
481
- message: "是否推送到远程(会强制覆盖)?",
505
+ message: "是否同步到远程?",
482
506
  choices: [
483
507
  { name: "是", value: true },
484
508
  { name: "否", value: false },
@@ -487,13 +511,16 @@ export async function updateTag(): Promise<void> {
487
511
  });
488
512
 
489
513
  if (pushRemote) {
490
- const pushSpinner = ora("正在推送到远程...").start();
514
+ const pushSpinner = ora("正在同步到远程...").start();
491
515
  try {
492
- execSync(`git push origin "${tagToUpdate}" --force`, { stdio: "pipe" });
493
- pushSpinner.succeed(`Tag 已推送: ${tagToUpdate}`);
516
+ // 推送新 tag
517
+ execSync(`git push origin "${newTag}"`, { stdio: "pipe" });
518
+ // 删除远程旧 tag
519
+ execSync(`git push origin --delete "${oldTag}"`, { stdio: "pipe" });
520
+ pushSpinner.succeed(`远程 tag 已同步: ${oldTag} → ${newTag}`);
494
521
  } catch {
495
522
  pushSpinner.warn(
496
- `远程推送失败,可稍后手动执行: git push origin ${tagToUpdate} --force`
523
+ `远程同步失败,可稍后手动执行:\n git push origin ${newTag}\n git push origin --delete ${oldTag}`
497
524
  );
498
525
  }
499
526
  }
@@ -39,11 +39,24 @@ async function getLatestVersion(packageName: string): Promise<string | null> {
39
39
  }
40
40
  }
41
41
 
42
+ /**
43
+ * 检测是否使用 Volta
44
+ */
45
+ function isUsingVolta(): boolean {
46
+ try {
47
+ const whichGw = execSync("which gw", { encoding: "utf-8" }).trim();
48
+ return whichGw.includes(".volta");
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
42
54
  /**
43
55
  * 手动更新命令
44
56
  */
45
57
  export async function update(currentVersion: string): Promise<void> {
46
58
  const packageName = "@zjex/git-workflow";
59
+ const usingVolta = isUsingVolta();
47
60
 
48
61
  console.log("");
49
62
  console.log(colors.bold("🔍 检查更新..."));
@@ -85,12 +98,6 @@ export async function update(currentVersion: string): Promise<void> {
85
98
 
86
99
  // 有新版本
87
100
  const versionText = `${currentVersion} → ${latestVersion}`;
88
- const maxWidth = Math.max(
89
- "🎉 发现新版本!".length,
90
- versionText.length,
91
- "✨ 更新完成!".length,
92
- "请重新打开终端使用新版本".length
93
- );
94
101
 
95
102
  console.log(
96
103
  boxen(
@@ -115,7 +122,12 @@ export async function update(currentVersion: string): Promise<void> {
115
122
  // 开始更新
116
123
  const updateSpinner = ora("正在更新...").start();
117
124
 
118
- execSync(`npm install -g ${packageName}@latest`, {
125
+ // 根据包管理器选择更新命令
126
+ const updateCommand = usingVolta
127
+ ? `volta install ${packageName}@latest`
128
+ : `npm install -g ${packageName}@latest`;
129
+
130
+ execSync(updateCommand, {
119
131
  encoding: "utf-8",
120
132
  stdio: ["pipe", "pipe", "pipe"],
121
133
  });
@@ -131,15 +143,20 @@ export async function update(currentVersion: string): Promise<void> {
131
143
  [
132
144
  colors.green(colors.bold("✨ 更新完成!")),
133
145
  "",
134
- colors.dim("请重新打开终端使用新版本"),
146
+ `新版本: ${colors.green(colors.bold(latestVersion))}`,
147
+ "",
148
+ colors.dim("请执行以下命令验证:"),
149
+ colors.cyan(" hash -r && gw --version"),
150
+ "",
151
+ colors.dim("或重新打开终端"),
135
152
  ].join("\n"),
136
153
  {
137
- padding: { top: 1, bottom: 1, left: 3, right: 3 },
154
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
138
155
  margin: { top: 0, bottom: 1, left: 2, right: 2 },
139
156
  borderStyle: "round",
140
157
  borderColor: "green",
141
- align: "center",
142
- width: 40,
158
+ align: "left",
159
+ width: 50,
143
160
  }
144
161
  )
145
162
  );
@@ -150,7 +167,10 @@ export async function update(currentVersion: string): Promise<void> {
150
167
  spinner.fail(colors.red("更新失败"));
151
168
  console.log("");
152
169
  console.log(colors.dim(" 你可以手动运行以下命令更新:"));
153
- console.log(colors.cyan(` npm install -g ${packageName}@latest`));
170
+ const updateCommand = usingVolta
171
+ ? `volta install ${packageName}@latest`
172
+ : `npm install -g ${packageName}@latest`;
173
+ console.log(colors.cyan(` ${updateCommand}`));
154
174
  console.log("");
155
175
  process.exit(1);
156
176
  }
package/src/index.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * @zjex/git-workflow - Git 工作流 CLI 工具
3
+ *
4
+ * 主入口文件,负责:
5
+ * 1. 初始化 CLI 应用
6
+ * 2. 注册所有命令
7
+ * 3. 处理全局错误和信号
8
+ * 4. 显示交互式主菜单
9
+ */
10
+
1
11
  // @ts-nocheck shebang handled by tsup banner
2
12
 
3
13
  import { cac } from "cac";
@@ -14,7 +24,12 @@ import { showHelp } from "./commands/help.js";
14
24
  import { checkForUpdates } from "./update-notifier.js";
15
25
  import { update } from "./commands/update.js";
16
26
 
17
- // 捕获 Ctrl+C 退出,静默处理
27
+ // ========== 全局错误处理 ==========
28
+
29
+ /**
30
+ * 捕获未捕获的异常
31
+ * 主要用于优雅处理用户按 Ctrl+C 退出的情况
32
+ */
18
33
  process.on("uncaughtException", (err) => {
19
34
  if (err instanceof ExitPromptError) {
20
35
  console.log(""); // 输出空行,让界面更整洁
@@ -24,7 +39,9 @@ process.on("uncaughtException", (err) => {
24
39
  process.exit(1);
25
40
  });
26
41
 
27
- // 捕获未处理的 Promise 拒绝
42
+ /**
43
+ * 捕获未处理的 Promise 拒绝
44
+ */
28
45
  process.on("unhandledRejection", (reason) => {
29
46
  if (reason instanceof ExitPromptError) {
30
47
  console.log("");
@@ -34,29 +51,50 @@ process.on("unhandledRejection", (reason) => {
34
51
  process.exit(1);
35
52
  });
36
53
 
37
- // 捕获 SIGINT 信号 (Ctrl+C)
54
+ /**
55
+ * 捕获 SIGINT 信号 (Ctrl+C)
56
+ * 确保用户按 Ctrl+C 时能优雅退出
57
+ */
38
58
  process.on("SIGINT", () => {
39
59
  console.log("");
40
60
  process.exit(0);
41
61
  });
42
62
 
43
- // 捕获 SIGTERM 信号
63
+ /**
64
+ * 捕获 SIGTERM 信号
65
+ * 处理进程终止信号
66
+ */
44
67
  process.on("SIGTERM", () => {
45
68
  console.log("");
46
69
  process.exit(0);
47
70
  });
48
71
 
72
+ // ========== 版本信息 ==========
73
+
74
+ /**
75
+ * 版本号由构建工具注入
76
+ * 开发环境下使用 0.0.0-dev
77
+ */
49
78
  declare const __VERSION__: string | undefined;
50
79
 
51
- // 开发环境下从 package.json 读取版本号
80
+ /**
81
+ * 当前版本号
82
+ * 生产环境:从构建时注入的 __VERSION__ 获取
83
+ * 开发环境:使用 0.0.0-dev
84
+ */
52
85
  const version: string =
53
86
  typeof __VERSION__ !== "undefined" && __VERSION__ !== ""
54
87
  ? __VERSION__
55
88
  : "0.0.0-dev";
56
89
 
57
- // 交互式主菜单
90
+ // ========== 交互式主菜单 ==========
91
+
92
+ /**
93
+ * 显示交互式主菜单
94
+ * 提供所有可用命令的可视化选择界面
95
+ */
58
96
  async function mainMenu(): Promise<void> {
59
- // ASCII Art Logo
97
+ // 显示 ASCII Art Logo
60
98
  console.log(
61
99
  colors.green(`
62
100
  ███████╗ ██╗███████╗██╗ ██╗
@@ -97,7 +135,7 @@ async function mainMenu(): Promise<void> {
97
135
  value: "tag-delete",
98
136
  },
99
137
  {
100
- name: `[7] ✏️ 修改 tag ${colors.dim("gw tu")}`,
138
+ name: `[7] ✏️ 重命名 tag ${colors.dim("gw tu")}`,
101
139
  value: "tag-update",
102
140
  },
103
141
  {
@@ -174,9 +212,20 @@ async function mainMenu(): Promise<void> {
174
212
  }
175
213
  }
176
214
 
215
+ // ========== CLI 应用初始化 ==========
216
+
217
+ /**
218
+ * 创建 CLI 应用实例
219
+ * 使用 cac (Command And Conquer) 库
220
+ */
177
221
  const cli = cac("gw");
178
222
 
179
- // 默认命令 - 显示交互式菜单
223
+ // ========== 命令注册 ==========
224
+
225
+ /**
226
+ * 默认命令 - 显示交互式菜单
227
+ * 运行 `gw` 时触发,会检查更新(交互式模式)
228
+ */
180
229
  cli.command("", "显示交互式菜单").action(async () => {
181
230
  await checkForUpdates(version, "@zjex/git-workflow", true);
182
231
  return mainMenu();
@@ -242,7 +291,7 @@ cli
242
291
  });
243
292
 
244
293
  cli
245
- .command("tag:update", "修改 tag 消息")
294
+ .command("tag:update", "重命名 tag")
246
295
  .alias("tu")
247
296
  .action(async () => {
248
297
  await checkForUpdates(version, "@zjex/git-workflow");
@@ -107,6 +107,18 @@ function backgroundCheck(currentVersion: string, packageName: string): void {
107
107
  });
108
108
  }
109
109
 
110
+ /**
111
+ * 检测是否使用 Volta
112
+ */
113
+ function isUsingVolta(): boolean {
114
+ try {
115
+ const whichGw = execSync("which gw", { encoding: "utf-8" }).trim();
116
+ return whichGw.includes(".volta");
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
110
122
  /**
111
123
  * 获取 npm 上的最新版本
112
124
  */
@@ -172,6 +184,11 @@ async function showUpdateMessage(
172
184
  })
173
185
  );
174
186
 
187
+ const usingVolta = isUsingVolta();
188
+ const updateCommand = usingVolta
189
+ ? `volta install ${packageName}@latest`
190
+ : `npm install -g ${packageName}@latest`;
191
+
175
192
  try {
176
193
  const action = await select({
177
194
  message: "你想做什么?",
@@ -179,7 +196,7 @@ async function showUpdateMessage(
179
196
  {
180
197
  name: "🚀 立即更新",
181
198
  value: "update",
182
- description: `运行 npm install -g ${packageName}`,
199
+ description: `运行 ${updateCommand}`,
183
200
  },
184
201
  {
185
202
  name: "⏭️ 稍后更新,继续使用",
@@ -208,14 +225,19 @@ async function showUpdateMessage(
208
225
  async function performUpdate(packageName: string): Promise<void> {
209
226
  console.log("");
210
227
 
228
+ const usingVolta = isUsingVolta();
229
+ const updateCommand = usingVolta
230
+ ? `volta install ${packageName}@latest`
231
+ : `npm install -g ${packageName}@latest`;
232
+
211
233
  const spinner = ora({
212
234
  text: "正在更新...",
213
235
  spinner: "dots",
214
236
  }).start();
215
237
 
216
238
  try {
217
- // 直接安装最新版本(npm 会自动覆盖旧版本)
218
- execSync(`npm install -g ${packageName}@latest`, {
239
+ // 根据包管理器选择更新命令
240
+ execSync(updateCommand, {
219
241
  encoding: "utf-8",
220
242
  stdio: ["pipe", "pipe", "pipe"],
221
243
  });
@@ -231,15 +253,18 @@ async function performUpdate(packageName: string): Promise<void> {
231
253
  [
232
254
  colors.green(colors.bold("✨ 更新完成!")),
233
255
  "",
234
- colors.dim("请重新打开终端使用新版本"),
256
+ colors.dim("请执行以下命令验证:"),
257
+ colors.cyan(" hash -r && gw --version"),
258
+ "",
259
+ colors.dim("或重新打开终端"),
235
260
  ].join("\n"),
236
261
  {
237
- padding: { top: 1, bottom: 1, left: 3, right: 3 },
262
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
238
263
  margin: { top: 0, bottom: 1, left: 2, right: 2 },
239
264
  borderStyle: "round",
240
265
  borderColor: "green",
241
- align: "center",
242
- width: 40,
266
+ align: "left",
267
+ width: 50,
243
268
  }
244
269
  )
245
270
  );
@@ -250,7 +275,7 @@ async function performUpdate(packageName: string): Promise<void> {
250
275
  spinner.fail(colors.red("更新失败"));
251
276
  console.log("");
252
277
  console.log(colors.dim(" 你可以手动运行以下命令更新:"));
253
- console.log(colors.cyan(` npm install -g ${packageName}@latest`));
278
+ console.log(colors.cyan(` ${updateCommand}`));
254
279
  console.log("");
255
280
  }
256
281
  }