@zjex/git-workflow 0.4.5 → 0.4.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
@@ -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-435%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,58 @@ 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
+ const label = code === 0 ? "\u8F93\u51FA\u4FE1\u606F" : "\u9519\u8BEF\u8F93\u51FA";
88
+ console.log(colors.dim(`[DEBUG] ${label}:
89
+ ${errorOutput}`));
90
+ }
91
+ }
92
+ if (code === 0) {
93
+ resolve({ success: true });
94
+ } else {
95
+ resolve({ success: false, error: errorOutput.trim() });
96
+ }
62
97
  });
63
- process2.on("error", () => {
64
- resolve(false);
98
+ process2.on("error", (err) => {
99
+ if (debugMode) {
100
+ console.log(colors.dim(`[DEBUG] \u8FDB\u7A0B\u9519\u8BEF: ${err.message}`));
101
+ console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u5806\u6808:
102
+ ${err.stack}`));
103
+ }
104
+ resolve({ success: false, error: err.message });
65
105
  });
66
106
  });
67
107
  }
68
108
  async function execWithSpinner(command, spinner, successMessage, errorMessage) {
69
- const success = await execAsync(command, spinner);
70
- if (success) {
109
+ const result = await execAsync(command, spinner);
110
+ if (result.success) {
71
111
  if (successMessage) {
72
112
  spinner.succeed(successMessage);
73
113
  } else {
@@ -79,13 +119,27 @@ async function execWithSpinner(command, spinner, successMessage, errorMessage) {
79
119
  } else {
80
120
  spinner.fail();
81
121
  }
122
+ if (result.error) {
123
+ console.log(colors.dim(` ${result.error}`));
124
+ }
125
+ if (debugMode) {
126
+ console.log(colors.yellow("\n[DEBUG] \u6545\u969C\u6392\u67E5\u4FE1\u606F:"));
127
+ console.log(colors.dim(` \u547D\u4EE4: ${command}`));
128
+ console.log(colors.dim(` \u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}`));
129
+ console.log(colors.dim(` Shell: ${process.env.SHELL || "unknown"}`));
130
+ console.log(
131
+ 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
132
+ `)
133
+ );
134
+ }
82
135
  }
83
- return success;
136
+ return result.success;
84
137
  }
85
- var colors, TODAY, theme;
138
+ var debugMode, colors, TODAY, theme;
86
139
  var init_utils = __esm({
87
140
  "src/utils.ts"() {
88
141
  "use strict";
142
+ debugMode = false;
89
143
  colors = {
90
144
  red: (s) => `\x1B[31m${s}\x1B[0m`,
91
145
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -1006,6 +1060,25 @@ async function createTag(inputPrefix) {
1006
1060
  }
1007
1061
  async function doCreateTag(tagName) {
1008
1062
  divider();
1063
+ const hasCommits = execOutput("git rev-parse HEAD 2>/dev/null");
1064
+ if (!hasCommits) {
1065
+ console.log(colors.red("\u5F53\u524D\u4ED3\u5E93\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4"));
1066
+ console.log("");
1067
+ console.log(colors.dim(" \u63D0\u793A: \u9700\u8981\u5148\u521B\u5EFA\u81F3\u5C11\u4E00\u4E2A\u63D0\u4EA4\u624D\u80FD\u6253 tag:"));
1068
+ console.log(colors.cyan(" git add ."));
1069
+ console.log(colors.cyan(' git commit -m "Initial commit"'));
1070
+ console.log(colors.cyan(" gw tag"));
1071
+ return;
1072
+ }
1073
+ const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
1074
+ if (existingTags.includes(tagName)) {
1075
+ console.log(colors.red(`Tag ${tagName} \u5DF2\u5B58\u5728`));
1076
+ console.log("");
1077
+ console.log(colors.dim(" \u63D0\u793A: \u5982\u9700\u91CD\u65B0\u521B\u5EFA\uFF0C\u8BF7\u5148\u5220\u9664\u65E7 tag:"));
1078
+ console.log(colors.cyan(` git tag -d ${tagName}`));
1079
+ console.log(colors.cyan(` git push origin --delete ${tagName}`));
1080
+ return;
1081
+ }
1009
1082
  const spinner = ora2(`\u6B63\u5728\u521B\u5EFA tag: ${tagName}`).start();
1010
1083
  const success = await execWithSpinner(
1011
1084
  `git tag -a "${tagName}" -m "Release ${tagName}"`,
@@ -1153,9 +1226,12 @@ async function updateTag() {
1153
1226
  spinner.fail("tag \u91CD\u547D\u540D\u5931\u8D25");
1154
1227
  return;
1155
1228
  }
1156
- const deleteSuccess = await execAsync(`git tag -d "${oldTag}"`, spinner);
1157
- if (!deleteSuccess) {
1229
+ const deleteResult = await execAsync(`git tag -d "${oldTag}"`, spinner);
1230
+ if (!deleteResult.success) {
1158
1231
  spinner.fail("\u5220\u9664\u65E7 tag \u5931\u8D25");
1232
+ if (deleteResult.error) {
1233
+ console.log(colors.dim(` ${deleteResult.error}`));
1234
+ }
1159
1235
  return;
1160
1236
  }
1161
1237
  spinner.succeed(`Tag \u5DF2\u91CD\u547D\u540D: ${oldTag} \u2192 ${newTag}`);
@@ -1169,26 +1245,32 @@ async function updateTag() {
1169
1245
  });
1170
1246
  if (pushRemote) {
1171
1247
  const pushSpinner = ora2("\u6B63\u5728\u540C\u6B65\u5230\u8FDC\u7A0B...").start();
1172
- const pushNewSuccess = await execAsync(
1248
+ const pushNewResult = await execAsync(
1173
1249
  `git push origin "${newTag}"`,
1174
1250
  pushSpinner
1175
1251
  );
1176
- if (!pushNewSuccess) {
1252
+ if (!pushNewResult.success) {
1177
1253
  pushSpinner.warn(
1178
1254
  `\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C:
1179
1255
  git push origin ${newTag}
1180
1256
  git push origin --delete ${oldTag}`
1181
1257
  );
1258
+ if (pushNewResult.error) {
1259
+ console.log(colors.dim(` ${pushNewResult.error}`));
1260
+ }
1182
1261
  return;
1183
1262
  }
1184
- const deleteOldSuccess = await execAsync(
1263
+ const deleteOldResult = await execAsync(
1185
1264
  `git push origin --delete "${oldTag}"`,
1186
1265
  pushSpinner
1187
1266
  );
1188
- if (!deleteOldSuccess) {
1267
+ if (!deleteOldResult.success) {
1189
1268
  pushSpinner.warn(
1190
1269
  `\u8FDC\u7A0B\u65E7 tag \u5220\u9664\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C: git push origin --delete ${oldTag}`
1191
1270
  );
1271
+ if (deleteOldResult.error) {
1272
+ console.log(colors.dim(` ${deleteOldResult.error}`));
1273
+ }
1192
1274
  return;
1193
1275
  }
1194
1276
  pushSpinner.succeed(`\u8FDC\u7A0B tag \u5DF2\u540C\u6B65: ${oldTag} \u2192 ${newTag}`);
@@ -1241,8 +1323,8 @@ async function cleanInvalidTags() {
1241
1323
  let localSuccess = 0;
1242
1324
  let localFailed = 0;
1243
1325
  for (const tag of invalidTags) {
1244
- const success = await execAsync(`git tag -d "${tag}"`, localSpinner);
1245
- if (success) {
1326
+ const result = await execAsync(`git tag -d "${tag}"`, localSpinner);
1327
+ if (result.success) {
1246
1328
  localSuccess++;
1247
1329
  } else {
1248
1330
  localFailed++;
@@ -1268,11 +1350,11 @@ async function cleanInvalidTags() {
1268
1350
  let remoteSuccess = 0;
1269
1351
  let remoteFailed = 0;
1270
1352
  for (const tag of invalidTags) {
1271
- const success = await execAsync(
1353
+ const result = await execAsync(
1272
1354
  `git push origin --delete "${tag}"`,
1273
1355
  remoteSpinner
1274
1356
  );
1275
- if (success) {
1357
+ if (result.success) {
1276
1358
  remoteSuccess++;
1277
1359
  } else {
1278
1360
  remoteFailed++;
@@ -2753,16 +2835,28 @@ function clearUpdateCache2() {
2753
2835
  }
2754
2836
  }
2755
2837
  async function getLatestVersion2(packageName) {
2756
- try {
2757
- const result = execSync4(`npm view ${packageName} version`, {
2758
- encoding: "utf-8",
2759
- timeout: 3e3,
2760
- stdio: ["pipe", "pipe", "ignore"]
2838
+ return new Promise((resolve) => {
2839
+ const npmView = spawn3("npm", ["view", packageName, "version"], {
2840
+ stdio: ["ignore", "pipe", "ignore"],
2841
+ timeout: 5e3
2761
2842
  });
2762
- return result.trim();
2763
- } catch {
2764
- return null;
2765
- }
2843
+ let output = "";
2844
+ if (npmView.stdout) {
2845
+ npmView.stdout.on("data", (data) => {
2846
+ output += data.toString();
2847
+ });
2848
+ }
2849
+ npmView.on("close", (code) => {
2850
+ if (code === 0 && output.trim()) {
2851
+ resolve(output.trim());
2852
+ } else {
2853
+ resolve(null);
2854
+ }
2855
+ });
2856
+ npmView.on("error", () => {
2857
+ resolve(null);
2858
+ });
2859
+ });
2766
2860
  }
2767
2861
  function isUsingVolta2() {
2768
2862
  try {
@@ -3533,7 +3627,7 @@ process.on("SIGTERM", () => {
3533
3627
  console.log("");
3534
3628
  process.exit(0);
3535
3629
  });
3536
- var version = true ? "0.4.5" : "0.0.0-dev";
3630
+ var version = true ? "0.4.7" : "0.0.0-dev";
3537
3631
  async function mainMenu() {
3538
3632
  console.log(
3539
3633
  colors.green(`
@@ -3813,7 +3907,12 @@ cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").a
3813
3907
  });
3814
3908
  cli.option("-v, --version", "\u663E\u793A\u7248\u672C\u53F7");
3815
3909
  cli.option("-h, --help", "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F");
3910
+ cli.option("-d, --debug", "\u542F\u7528\u8C03\u8BD5\u6A21\u5F0F\uFF0C\u663E\u793A\u8BE6\u7EC6\u7684\u547D\u4EE4\u548C\u9519\u8BEF\u4FE1\u606F");
3816
3911
  var processArgs = process.argv.slice(2);
3912
+ if (processArgs.includes("-d") || processArgs.includes("--debug")) {
3913
+ setDebugMode(true);
3914
+ console.log(colors.yellow("\u{1F41B} Debug \u6A21\u5F0F\u5DF2\u542F\u7528\n"));
3915
+ }
3817
3916
  if (processArgs.includes("-v") || processArgs.includes("--version")) {
3818
3917
  console.log(colors.yellow(`v${version}`));
3819
3918
  process.exit(0);
@@ -54,6 +54,8 @@ export default defineConfig({
54
54
  items: [
55
55
  { text: "开发指南", link: "/guide/development" },
56
56
  { text: "测试指南", link: "/guide/testing" },
57
+ { text: "Debug 模式", link: "/guide/debug-mode" },
58
+ { text: "命令引号处理", link: "/guide/command-quotes-handling" },
57
59
  { text: "API 文档", link: "/guide/api" },
58
60
  { text: "贡献指南", link: "/guide/contributing" },
59
61
  ],
@@ -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 函数