@zjex/git-workflow 0.4.4 → 0.4.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.0.0](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.5...v1.0.0) (2026-01-19)
4
+
5
+
6
+ ## [v0.4.5](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.4...v0.4.5) (2026-01-19)
7
+
8
+ ### ✨ Features
9
+
10
+ - 更新日期格式说明,支持完整时间输入 ([25f2b39](https://github.com/iamzjt-front-end/git-workflow/commit/25f2b39))
11
+
12
+ ### 📖 Documentation
13
+
14
+ - 📝 docs: 自动更新测试数量徽章 [skip ci] ([c49a4bd](https://github.com/iamzjt-front-end/git-workflow/commit/c49a4bd))
15
+
16
+ ### 🔧 Chore
17
+
18
+ - 🔖 chore(release): 发布 v0.4.5 ([8abd7b4](https://github.com/iamzjt-front-end/git-workflow/commit/8abd7b4))
19
+
20
+
21
+ ## [v0.4.4](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.3...v0.4.4) (2026-01-19)
22
+
23
+ ### ✨ Features
24
+
25
+ - 优化分支创建和删除功能的用户体验 ([88dec43](https://github.com/iamzjt-front-end/git-workflow/commit/88dec43))
26
+
27
+ ### 🔧 Chore
28
+
29
+ - 🔖 chore(release): 发布 v0.4.4 ([d673bfb](https://github.com/iamzjt-front-end/git-workflow/commit/d673bfb))
30
+
31
+
3
32
  ## [v0.4.3](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.2...v0.4.3) (2026-01-19)
4
33
 
5
34
  ### ✨ Features
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  <a href="https://github.com/iamzjt-front-end/git-workflow"><img src="https://img.shields.io/github/stars/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=F59E0B" alt="github stars"></a>
13
13
  <a href="https://github.com/iamzjt-front-end/git-workflow/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zjex/git-workflow?style=flat&colorA=18181B&colorB=10B981" alt="license"></a>
14
14
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat&logo=node.js&logoColor=white&colorA=18181B" alt="node version"></a>
15
- <a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-431%20passed-success?style=flat&colorA=18181B" alt="tests"></a>
15
+ <a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-474%20passed-success?style=flat&colorA=18181B" alt="tests"></a>
16
16
  <a href="https://github.com/iamzjt-front-end/git-workflow/issues"><img src="https://img.shields.io/github/issues/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=EC4899" alt="issues"></a>
17
17
  </p>
18
18
 
package/dist/index.js CHANGED
@@ -11,6 +11,9 @@ var __export = (target, all) => {
11
11
 
12
12
  // src/utils.ts
13
13
  import { execSync, spawn } from "child_process";
14
+ function setDebugMode(enabled) {
15
+ debugMode = enabled;
16
+ }
14
17
  function exec(cmd, silent = false) {
15
18
  try {
16
19
  const options = {
@@ -53,21 +56,57 @@ function divider() {
53
56
  }
54
57
  function execAsync(command, spinner) {
55
58
  return new Promise((resolve) => {
56
- const [cmd, ...args] = command.split(" ");
57
- const process2 = spawn(cmd, args, {
58
- stdio: spinner ? "pipe" : "inherit"
59
+ if (debugMode) {
60
+ console.log(colors.dim(`
61
+ [DEBUG] \u6267\u884C\u547D\u4EE4: ${colors.cyan(command)}`));
62
+ }
63
+ const process2 = spawn(command, {
64
+ stdio: spinner ? "pipe" : "inherit",
65
+ shell: true
59
66
  });
67
+ let errorOutput = "";
68
+ let stdoutOutput = "";
69
+ if (debugMode && process2.stdout) {
70
+ process2.stdout.on("data", (data) => {
71
+ stdoutOutput += data.toString();
72
+ });
73
+ }
74
+ if (process2.stderr) {
75
+ process2.stderr.on("data", (data) => {
76
+ errorOutput += data.toString();
77
+ });
78
+ }
60
79
  process2.on("close", (code) => {
61
- resolve(code === 0);
80
+ if (debugMode) {
81
+ console.log(colors.dim(`[DEBUG] \u9000\u51FA\u7801: ${code}`));
82
+ if (stdoutOutput) {
83
+ console.log(colors.dim(`[DEBUG] \u6807\u51C6\u8F93\u51FA:
84
+ ${stdoutOutput}`));
85
+ }
86
+ if (errorOutput) {
87
+ console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u8F93\u51FA:
88
+ ${errorOutput}`));
89
+ }
90
+ }
91
+ if (code === 0) {
92
+ resolve({ success: true });
93
+ } else {
94
+ resolve({ success: false, error: errorOutput.trim() });
95
+ }
62
96
  });
63
- process2.on("error", () => {
64
- resolve(false);
97
+ process2.on("error", (err) => {
98
+ if (debugMode) {
99
+ console.log(colors.dim(`[DEBUG] \u8FDB\u7A0B\u9519\u8BEF: ${err.message}`));
100
+ console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u5806\u6808:
101
+ ${err.stack}`));
102
+ }
103
+ resolve({ success: false, error: err.message });
65
104
  });
66
105
  });
67
106
  }
68
107
  async function execWithSpinner(command, spinner, successMessage, errorMessage) {
69
- const success = await execAsync(command, spinner);
70
- if (success) {
108
+ const result = await execAsync(command, spinner);
109
+ if (result.success) {
71
110
  if (successMessage) {
72
111
  spinner.succeed(successMessage);
73
112
  } else {
@@ -79,13 +118,27 @@ async function execWithSpinner(command, spinner, successMessage, errorMessage) {
79
118
  } else {
80
119
  spinner.fail();
81
120
  }
121
+ if (result.error) {
122
+ console.log(colors.dim(` ${result.error}`));
123
+ }
124
+ if (debugMode) {
125
+ console.log(colors.yellow("\n[DEBUG] \u6545\u969C\u6392\u67E5\u4FE1\u606F:"));
126
+ console.log(colors.dim(` \u547D\u4EE4: ${command}`));
127
+ console.log(colors.dim(` \u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}`));
128
+ console.log(colors.dim(` Shell: ${process.env.SHELL || "unknown"}`));
129
+ console.log(
130
+ colors.dim(` \u5EFA\u8BAE: \u5C1D\u8BD5\u5728\u7EC8\u7AEF\u4E2D\u76F4\u63A5\u8FD0\u884C\u4E0A\u8FF0\u547D\u4EE4\u4EE5\u83B7\u53D6\u66F4\u591A\u4FE1\u606F
131
+ `)
132
+ );
133
+ }
82
134
  }
83
- return success;
135
+ return result.success;
84
136
  }
85
- var colors, TODAY, theme;
137
+ var debugMode, colors, TODAY, theme;
86
138
  var init_utils = __esm({
87
139
  "src/utils.ts"() {
88
140
  "use strict";
141
+ debugMode = false;
89
142
  colors = {
90
143
  red: (s) => `\x1B[31m${s}\x1B[0m`,
91
144
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -3223,6 +3276,20 @@ function formatGitDate(date) {
3223
3276
  }
3224
3277
  function parseDate(input8) {
3225
3278
  const trimmed = input8.trim();
3279
+ const fullMatch = trimmed.match(
3280
+ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/
3281
+ );
3282
+ if (fullMatch) {
3283
+ const [, year, month, day, hours, minutes, seconds] = fullMatch;
3284
+ return new Date(
3285
+ parseInt(year),
3286
+ parseInt(month) - 1,
3287
+ parseInt(day),
3288
+ parseInt(hours),
3289
+ parseInt(minutes),
3290
+ parseInt(seconds)
3291
+ );
3292
+ }
3226
3293
  const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
3227
3294
  if (dateMatch) {
3228
3295
  const [, year, month, day] = dateMatch;
@@ -3279,7 +3346,6 @@ async function amendDate(commitHash) {
3279
3346
  value: c,
3280
3347
  description: c.date
3281
3348
  })),
3282
- pageSize: 15,
3283
3349
  theme
3284
3350
  });
3285
3351
  }
@@ -3289,14 +3355,16 @@ async function amendDate(commitHash) {
3289
3355
  console.log(` Message: ${selectedCommit.message}`);
3290
3356
  console.log(` Date: ${colors.cyan(selectedCommit.date)}`);
3291
3357
  divider();
3292
- console.log(colors.dim("\u8F93\u5165\u65E5\u671F\u683C\u5F0F: YYYY-MM-DD (\u5982: 2026-01-19)"));
3358
+ console.log(colors.dim("\u652F\u6301\u683C\u5F0F:"));
3359
+ console.log(colors.dim(" YYYY-MM-DD (\u5982: 2026-01-19\uFF0C\u9ED8\u8BA4 00:00:00)"));
3360
+ console.log(colors.dim(" YYYY-MM-DD HH:mm:ss (\u5982: 2026-01-19 14:30:00)"));
3293
3361
  console.log("");
3294
3362
  const dateInput = await input6({
3295
3363
  message: "\u8F93\u5165\u65B0\u7684\u65E5\u671F:",
3296
3364
  validate: (value) => {
3297
3365
  const parsed = parseDate(value);
3298
3366
  if (!parsed) {
3299
- return "\u65E5\u671F\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD \u683C\u5F0F";
3367
+ return "\u65E5\u671F\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD \u6216 YYYY-MM-DD HH:mm:ss \u683C\u5F0F";
3300
3368
  }
3301
3369
  return true;
3302
3370
  },
@@ -3409,7 +3477,6 @@ async function amend(commitHash) {
3409
3477
  value: c,
3410
3478
  description: c.message
3411
3479
  })),
3412
- pageSize: 15,
3413
3480
  theme
3414
3481
  });
3415
3482
  }
@@ -3519,7 +3586,7 @@ process.on("SIGTERM", () => {
3519
3586
  console.log("");
3520
3587
  process.exit(0);
3521
3588
  });
3522
- var version = true ? "0.4.4" : "0.0.0-dev";
3589
+ var version = true ? "0.4.6" : "0.0.0-dev";
3523
3590
  async function mainMenu() {
3524
3591
  console.log(
3525
3592
  colors.green(`
@@ -3799,7 +3866,12 @@ cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").a
3799
3866
  });
3800
3867
  cli.option("-v, --version", "\u663E\u793A\u7248\u672C\u53F7");
3801
3868
  cli.option("-h, --help", "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F");
3869
+ cli.option("-d, --debug", "\u542F\u7528\u8C03\u8BD5\u6A21\u5F0F\uFF0C\u663E\u793A\u8BE6\u7EC6\u7684\u547D\u4EE4\u548C\u9519\u8BEF\u4FE1\u606F");
3802
3870
  var processArgs = process.argv.slice(2);
3871
+ if (processArgs.includes("-d") || processArgs.includes("--debug")) {
3872
+ setDebugMode(true);
3873
+ console.log(colors.yellow("\u{1F41B} Debug \u6A21\u5F0F\u5DF2\u542F\u7528\n"));
3874
+ }
3803
3875
  if (processArgs.includes("-v") || processArgs.includes("--version")) {
3804
3876
  console.log(colors.yellow(`v${version}`));
3805
3877
  process.exit(0);
@@ -66,12 +66,17 @@ gw ad HEAD~2 # 上上个 commit
66
66
  ### 步骤 3: 输入新日期
67
67
 
68
68
  ```
69
- 输入日期格式: YYYY-MM-DD (如: 2026-01-19)
69
+ 支持格式:
70
+ YYYY-MM-DD (如: 2026-01-19,默认 00:00:00)
71
+ YYYY-MM-DD HH:mm:ss (如: 2026-01-19 14:30:00)
70
72
 
71
- ? 输入新的日期: 2026-01-15
73
+ ? 输入新的日期: 2026-01-15 14:30:00
72
74
  ```
73
75
 
74
- **日期格式**: `YYYY-MM-DD`,时间默认为 `00:00:00`
76
+ **支持的日期格式**:
77
+
78
+ - `YYYY-MM-DD` - 只指定日期,时间默认为 `00:00:00`
79
+ - `YYYY-MM-DD HH:mm:ss` - 指定完整的日期和时间
75
80
 
76
81
  ### 步骤 4: 确认修改
77
82
 
@@ -79,7 +84,7 @@ gw ad HEAD~2 # 上上个 commit
79
84
  修改预览:
80
85
  Commit: a1b2c3d
81
86
  旧时间: 2026-01-19 14:30:00 +0800
82
- 新时间: 2026-01-15 00:00:00
87
+ 新时间: 2026-01-15 14:30:00
83
88
  修改类型: Author + Committer (两者都修改)
84
89
  ────────────────────────────────────────
85
90
 
@@ -164,9 +169,37 @@ git push --force
164
169
 
165
170
  ### 📅 时间格式
166
171
 
167
- - **输入格式**: `YYYY-MM-DD`(如 `2026-01-19`)
168
- - **存储格式**: `YYYY-MM-DD 00:00:00`(时间默认为 00:00:00)
169
- - **时区**: 使用系统当前时区
172
+ 支持两种输入格式:
173
+
174
+ 1. **仅日期**: `YYYY-MM-DD`(如 `2026-01-19`)
175
+ - 时间默认为 `00:00:00`
176
+ - 适合只关心日期的场景
177
+
178
+ 2. **完整时间**: `YYYY-MM-DD HH:mm:ss`(如 `2026-01-19 14:30:00`)
179
+ - 可以精确指定时分秒
180
+ - 适合需要精确时间的场景
181
+
182
+ **示例**:
183
+
184
+ ```bash
185
+ # 只指定日期
186
+ 输入: 2026-01-19
187
+ 结果: 2026-01-19 00:00:00
188
+
189
+ # 指定完整时间
190
+ 输入: 2026-01-19 14:30:00
191
+ 结果: 2026-01-19 14:30:00
192
+
193
+ # 指定午夜
194
+ 输入: 2026-01-19 00:00:00
195
+ 结果: 2026-01-19 00:00:00
196
+
197
+ # 指定一天结束
198
+ 输入: 2026-01-19 23:59:59
199
+ 结果: 2026-01-19 23:59:59
200
+ ```
201
+
202
+ **时区**: 使用系统当前时区
170
203
 
171
204
  ### 🔄 修改范围
172
205
 
@@ -187,11 +220,21 @@ $ gw ad HEAD
187
220
  ✔ 修改成功
188
221
  ```
189
222
 
190
- ### 示例 2: 修改指定 hash 的 commit
223
+ ### 示例 2: 修改为指定的完整时间
224
+
225
+ ```bash
226
+ $ gw ad HEAD
227
+ 输入新的日期: 2026-01-15 14:30:00
228
+ 确认修改? y
229
+
230
+ ✔ 修改成功
231
+ ```
232
+
233
+ ### 示例 3: 修改指定 hash 的 commit
191
234
 
192
235
  ```bash
193
236
  $ gw ad a1b2c3d
194
- 输入新的日期: 2026-01-10
237
+ 输入新的日期: 2026-01-10 09:00:00
195
238
  确认修改? y
196
239
 
197
240
  ⚠️ 警告: 修改非最新 commit 需要使用 rebase,可能会改变 commit hash
@@ -199,7 +242,7 @@ $ gw ad a1b2c3d
199
242
  ✔ 修改成功
200
243
  ```
201
244
 
202
- ### 示例 3: 交互式选择并修改
245
+ ### 示例 4: 交互式选择并修改
203
246
 
204
247
  ```bash
205
248
  $ gw ad
@@ -208,7 +251,7 @@ $ gw ad
208
251
  ❯ a1b2c3d ✨ feat: 添加登录功能 2026-01-19 14:30:00
209
252
  d4e5f6g 🐛 fix: 修复bug 2026-01-18 10:20:00
210
253
 
211
- 输入新的日期: 2026-01-15
254
+ 输入新的日期: 2026-01-15 16:45:30
212
255
  确认修改? y
213
256
 
214
257
  ✔ 修改成功
@@ -363,11 +406,30 @@ git am < patch.diff
363
406
 
364
407
  ### Q: 为什么时间是 00:00:00?
365
408
 
366
- A: 为了简化操作,只需要输入日期(YYYY-MM-DD),时间默认为 00:00:00。大多数情况下,我们只关心日期,不需要精确到秒。
409
+ A: 当你只输入日期(YYYY-MM-DD)时,时间会默认为 00:00:00。这是为了简化操作,大多数情况下我们只关心日期。
410
+
411
+ 如果需要指定精确时间,可以使用完整格式:
412
+
413
+ ```bash
414
+ # 只输入日期
415
+ 输入: 2026-01-15
416
+ 结果: 2026-01-15 00:00:00
417
+
418
+ # 输入完整时间
419
+ 输入: 2026-01-15 14:30:00
420
+ 结果: 2026-01-15 14:30:00
421
+ ```
367
422
 
368
423
  ### Q: 可以修改时间部分吗?
369
424
 
370
- A: 当前版本只支持修改日期,时间固定为 00:00:00。如需精确时间,可以手动使用 git 命令:
425
+ A: 可以!使用完整格式 `YYYY-MM-DD HH:mm:ss` 即可指定精确时间:
426
+
427
+ ```bash
428
+ $ gw ad
429
+ 输入新的日期: 2026-01-15 14:30:00
430
+ ```
431
+
432
+ 如果需要更灵活的控制,也可以手动使用 git 命令:
371
433
 
372
434
  ```bash
373
435
  # 修改最新 commit 的时间为指定的精确时间
@@ -0,0 +1,279 @@
1
+ # 命令参数引号处理
2
+
3
+ 本文档说明了 git-workflow 如何正确处理带引号和特殊字符的命令参数。
4
+
5
+ ## 问题背景
6
+
7
+ 在早期版本中,使用简单的字符串分割方式处理命令参数:
8
+
9
+ ```typescript
10
+ // ❌ 错误的方式
11
+ const [cmd, ...args] = command.split(" ");
12
+ spawn(cmd, args);
13
+ ```
14
+
15
+ 这种方式无法正确处理引号:
16
+
17
+ ```javascript
18
+ 'git tag -a "v1.5.3" -m "Release v1.5.3"'.split(" ");
19
+ // 结果: ["git", "tag", "-a", '"v1.5.3"', "-m", '"Release', 'v1.5.3"']
20
+ // 引号被当作参数的一部分!
21
+ ```
22
+
23
+ 导致的问题:
24
+
25
+ - Tag 名称包含引号:`"v1.5.3"` 而不是 `v1.5.3`
26
+ - Git 报错:`fatal: Failed to resolve '"v1.5.3"' as a valid ref.`
27
+
28
+ ## 解决方案
29
+
30
+ 使用 `shell: true` 选项让 spawn 通过 shell 执行命令:
31
+
32
+ ```typescript
33
+ // ✅ 正确的方式
34
+ spawn(command, {
35
+ stdio: spinner ? "pipe" : "inherit",
36
+ shell: true, // 使用 shell 模式
37
+ });
38
+ ```
39
+
40
+ ### 优势
41
+
42
+ 1. **正确处理引号**:Shell 会自动解析和移除引号
43
+ 2. **支持特殊字符**:emoji、中文、空格等都能正确处理
44
+ 3. **支持转义**:`\"` 等转义字符正常工作
45
+
46
+ ## 支持的场景
47
+
48
+ ### 1. Tag 命令
49
+
50
+ ```bash
51
+ # 基本版本号
52
+ gw tag # 创建 v1.5.3
53
+
54
+ # 预发布版本
55
+ gw tag # 创建 v1.0.0-beta.1
56
+
57
+ # 带特殊字符
58
+ git tag -a "v1.0.0-🎉" -m "Release 🎉"
59
+ ```
60
+
61
+ ### 2. Branch 命令
62
+
63
+ ```bash
64
+ # 带日期和描述的分支
65
+ gw b feature # 创建 feature/20240120-123-add-feature
66
+
67
+ # 删除带特殊字符的分支
68
+ git branch -D "feature/20240120-123-add-feature"
69
+ git push origin --delete "feature/20240120-123-add-feature"
70
+ ```
71
+
72
+ ### 3. Stash 命令
73
+
74
+ ```bash
75
+ # 带中文的 stash 消息
76
+ git stash push -m "临时保存:修复登录bug"
77
+
78
+ # 带引号的消息
79
+ git stash push -m "WIP: 添加\"新功能\""
80
+
81
+ # 从 stash 创建分支
82
+ git stash branch "feature/from-stash" stash@{0}
83
+ ```
84
+
85
+ ### 4. Commit 命令
86
+
87
+ ```bash
88
+ # 带特殊字符的提交消息
89
+ git commit -m "feat: add \"quotes\" support"
90
+ git commit -m "fix: 修复登录问题 🐛"
91
+ ```
92
+
93
+ ## 测试覆盖
94
+
95
+ 我们创建了全面的测试用例来确保引号处理的正确性:
96
+
97
+ ### 测试场景
98
+
99
+ 1. **基本引号处理**
100
+ - 带引号的 tag 名称
101
+ - 带空格的分支名称
102
+ - 带特殊字符的 commit message
103
+
104
+ 2. **特殊字符支持**
105
+ - Emoji:`v1.0.0-🎉`
106
+ - 中文:`临时保存:修复bug`
107
+ - 转义引号:`feat: add \"quotes\" support`
108
+
109
+ 3. **错误处理**
110
+ - 捕获 stderr 错误信息
111
+ - 显示详细的失败原因
112
+ - 提供解决建议
113
+
114
+ ### 运行测试
115
+
116
+ ```bash
117
+ # 运行引号处理测试
118
+ npm test -- tests/command-with-quotes.test.ts
119
+
120
+ # 运行所有测试
121
+ npm test
122
+ ```
123
+
124
+ ## 错误信息改进
125
+
126
+ 现在当命令失败时,会显示详细的错误信息:
127
+
128
+ ### Tag 已存在
129
+
130
+ ```
131
+ ✗ Tag v1.5.3 已存在
132
+
133
+ 提示: 如需重新创建,请先删除旧 tag:
134
+ git tag -d v1.5.3
135
+ git push origin --delete v1.5.3
136
+ ```
137
+
138
+ ### 没有提交
139
+
140
+ ```
141
+ ✗ 当前仓库没有任何提交
142
+
143
+ 提示: 需要先创建至少一个提交才能打 tag:
144
+ git add .
145
+ git commit -m "Initial commit"
146
+ gw tag
147
+ ```
148
+
149
+ ### Git 命令错误
150
+
151
+ ```
152
+ ✗ tag 创建失败
153
+ fatal: Failed to resolve 'HEAD' as a valid ref.
154
+ ```
155
+
156
+ ## 实现细节
157
+
158
+ ### execAsync 函数
159
+
160
+ ```typescript
161
+ export function execAsync(
162
+ command: string,
163
+ spinner?: Ora,
164
+ ): Promise<{ success: boolean; error?: string }> {
165
+ return new Promise((resolve) => {
166
+ const process = spawn(command, {
167
+ stdio: spinner ? "pipe" : "inherit",
168
+ shell: true, // 关键:使用 shell 模式
169
+ });
170
+
171
+ let errorOutput = "";
172
+
173
+ // 捕获错误输出
174
+ if (process.stderr) {
175
+ process.stderr.on("data", (data) => {
176
+ errorOutput += data.toString();
177
+ });
178
+ }
179
+
180
+ process.on("close", (code) => {
181
+ if (code === 0) {
182
+ resolve({ success: true });
183
+ } else {
184
+ resolve({ success: false, error: errorOutput.trim() });
185
+ }
186
+ });
187
+
188
+ process.on("error", (err) => {
189
+ resolve({ success: false, error: err.message });
190
+ });
191
+ });
192
+ }
193
+ ```
194
+
195
+ ### execWithSpinner 函数
196
+
197
+ ```typescript
198
+ export async function execWithSpinner(
199
+ command: string,
200
+ spinner: Ora,
201
+ successMessage?: string,
202
+ errorMessage?: string,
203
+ ): Promise<boolean> {
204
+ const result = await execAsync(command, spinner);
205
+
206
+ if (result.success) {
207
+ if (successMessage) {
208
+ spinner.succeed(successMessage);
209
+ } else {
210
+ spinner.succeed();
211
+ }
212
+ } else {
213
+ if (errorMessage) {
214
+ spinner.fail(errorMessage);
215
+ } else {
216
+ spinner.fail();
217
+ }
218
+
219
+ // 显示具体的错误信息
220
+ if (result.error) {
221
+ console.log(colors.dim(` ${result.error}`));
222
+ }
223
+ }
224
+
225
+ return result.success;
226
+ }
227
+ ```
228
+
229
+ ## 最佳实践
230
+
231
+ ### 1. 始终使用引号包裹可能包含特殊字符的参数
232
+
233
+ ```typescript
234
+ // ✅ 推荐
235
+ await execAsync(`git tag -a "${tagName}" -m "Release ${tagName}"`);
236
+
237
+ // ❌ 不推荐(如果 tagName 包含空格会失败)
238
+ await execAsync(`git tag -a ${tagName} -m Release ${tagName}`);
239
+ ```
240
+
241
+ ### 2. 转义用户输入中的引号
242
+
243
+ ```typescript
244
+ // ✅ 正确处理用户输入
245
+ const message = userInput.replace(/"/g, '\\"');
246
+ await execAsync(`git commit -m "${message}"`);
247
+ ```
248
+
249
+ ### 3. 使用 execWithSpinner 显示进度
250
+
251
+ ```typescript
252
+ // ✅ 推荐:显示进度和错误信息
253
+ const spinner = ora("正在创建 tag...").start();
254
+ const success = await execWithSpinner(
255
+ `git tag -a "${tagName}" -m "Release ${tagName}"`,
256
+ spinner,
257
+ "Tag 创建成功",
258
+ "Tag 创建失败",
259
+ );
260
+
261
+ if (!success) {
262
+ // 错误信息已自动显示
263
+ return;
264
+ }
265
+ ```
266
+
267
+ ## 相关文件
268
+
269
+ - `src/utils.ts` - execAsync 和 execWithSpinner 实现
270
+ - `tests/command-with-quotes.test.ts` - 引号处理测试
271
+ - `src/commands/tag.ts` - Tag 命令实现
272
+ - `src/commands/branch.ts` - Branch 命令实现
273
+ - `src/commands/stash.ts` - Stash 命令实现
274
+
275
+ ## 版本历史
276
+
277
+ - **v0.4.5** - 修复引号处理问题,添加详细错误信息
278
+ - **v0.4.4** - 修复 spinner 阻塞问题
279
+ - **v0.4.3** - 添加 execAsync 和 execWithSpinner 函数