@zjex/git-workflow 0.2.19 → 0.2.21

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/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,11 +212,22 @@ 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
- await checkForUpdates(version, "@zjex/git-workflow");
230
+ await checkForUpdates(version, "@zjex/git-workflow", true);
182
231
  return mainMenu();
183
232
  });
184
233
 
@@ -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");
@@ -290,6 +339,14 @@ cli
290
339
  return update(version);
291
340
  });
292
341
 
342
+ cli.command("clean", "清理缓存文件").action(async () => {
343
+ const { clearUpdateCache } = await import("./update-notifier.js");
344
+ clearUpdateCache();
345
+ console.log("");
346
+ console.log(colors.green("✔ 缓存已清理"));
347
+ console.log("");
348
+ });
349
+
293
350
  cli.help((sections) => {
294
351
  sections.push({
295
352
  body: showHelp(),
@@ -1,57 +1,72 @@
1
1
  import { execSync } from "child_process";
2
- import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
  import boxen from "boxen";
6
6
  import { select } from "@inquirer/prompts";
7
7
  import ora from "ora";
8
+ import semver from "semver";
8
9
  import { colors } from "./utils.js";
9
10
 
11
+ const CHECK_INTERVAL = 1000 * 60 * 60 * 4; // 4 小时检查一次
10
12
  const DISMISS_INTERVAL = 1000 * 60 * 60 * 24; // 24 小时后再次提示
11
13
  const CACHE_FILE = ".gw-update-check";
12
14
 
13
15
  interface UpdateCache {
16
+ lastCheck?: number; // 上次检查更新的时间
14
17
  lastDismiss?: number; // 用户上次关闭提示的时间
15
- latestVersion?: string;
18
+ latestVersion?: string; // 最新版本号
19
+ checkedVersion?: string; // 检查时的当前版本
16
20
  }
17
21
 
18
22
  /**
19
- * 检查是否有新版本
23
+ * 检查是否有新版本(异步静默检查)
24
+ * @param currentVersion 当前版本
25
+ * @param packageName 包名
26
+ * @param interactive 是否交互式(true: 显示完整提示并可选择更新,false: 只显示简单提示)
20
27
  */
21
28
  export async function checkForUpdates(
22
29
  currentVersion: string,
23
- packageName: string = "@zjex/git-workflow"
30
+ packageName: string = "@zjex/git-workflow",
31
+ interactive: boolean = false
24
32
  ): Promise<void> {
25
33
  try {
26
- // 读取缓存
27
34
  const cache = readCache();
28
35
  const now = Date.now();
29
36
 
30
- // 如果用户在 24 小时内关闭过提示,跳过
31
- if (cache?.lastDismiss && now - cache.lastDismiss < DISMISS_INTERVAL) {
32
- return;
33
- }
37
+ // 1. 先检查缓存中是否有新版本需要提示
38
+ if (cache?.latestVersion && cache.checkedVersion === currentVersion) {
39
+ // 如果用户在 24 小时内关闭过提示,跳过
40
+ if (cache.lastDismiss && now - cache.lastDismiss < DISMISS_INTERVAL) {
41
+ // 继续后台检查(不阻塞)
42
+ backgroundCheck(currentVersion, packageName);
43
+ return;
44
+ }
45
+
46
+ // 使用 semver 比较版本
47
+ if (semver.gt(cache.latestVersion, currentVersion)) {
48
+ if (interactive) {
49
+ // 交互式模式:显示完整提示,可选择更新
50
+ const action = await showUpdateMessage(
51
+ currentVersion,
52
+ cache.latestVersion,
53
+ packageName
54
+ );
34
55
 
35
- // 获取最新版本
36
- const latestVersion = await getLatestVersion(packageName);
37
-
38
- // 如果有新版本,显示提示
39
- if (latestVersion && latestVersion !== currentVersion) {
40
- const action = await showUpdateMessage(
41
- currentVersion,
42
- latestVersion,
43
- packageName
44
- );
45
-
46
- if (action === "update") {
47
- // 用户选择立即更新
48
- await performUpdate(packageName);
49
- } else if (action === "dismiss") {
50
- // 用户选择跳过,记录时间
51
- writeCache({ lastDismiss: now, latestVersion });
56
+ if (action === "update") {
57
+ await performUpdate(packageName);
58
+ } else if (action === "dismiss") {
59
+ writeCache({ ...cache, lastDismiss: now });
60
+ }
61
+ } else {
62
+ // 非交互式模式:只显示简单提示
63
+ showSimpleNotification(currentVersion, cache.latestVersion);
64
+ }
52
65
  }
53
- // action === "continue" 时直接继续,不记录
54
66
  }
67
+
68
+ // 2. 后台异步检查更新(不阻塞当前命令)
69
+ backgroundCheck(currentVersion, packageName);
55
70
  } catch (error) {
56
71
  // 如果是用户按 Ctrl+C,重新抛出让全局处理
57
72
  if (error?.constructor?.name === "ExitPromptError") {
@@ -61,6 +76,37 @@ export async function checkForUpdates(
61
76
  }
62
77
  }
63
78
 
79
+ /**
80
+ * 后台异步检查更新(不阻塞)
81
+ */
82
+ function backgroundCheck(currentVersion: string, packageName: string): void {
83
+ const cache = readCache();
84
+ const now = Date.now();
85
+
86
+ // 如果距离上次检查不到 4 小时,跳过
87
+ if (cache?.lastCheck && now - cache.lastCheck < CHECK_INTERVAL) {
88
+ return;
89
+ }
90
+
91
+ // 使用 Promise 异步执行,不阻塞当前命令
92
+ Promise.resolve().then(async () => {
93
+ try {
94
+ const latestVersion = await getLatestVersion(packageName);
95
+
96
+ if (latestVersion) {
97
+ writeCache({
98
+ ...cache,
99
+ lastCheck: now,
100
+ latestVersion,
101
+ checkedVersion: currentVersion,
102
+ });
103
+ }
104
+ } catch {
105
+ // 静默失败
106
+ }
107
+ });
108
+ }
109
+
64
110
  /**
65
111
  * 获取 npm 上的最新版本
66
112
  */
@@ -78,7 +124,29 @@ async function getLatestVersion(packageName: string): Promise<string | null> {
78
124
  }
79
125
 
80
126
  /**
81
- * 显示更新提示消息并让用户选择
127
+ * 显示简单的更新通知(非交互式,不阻塞)
128
+ */
129
+ function showSimpleNotification(current: string, latest: string): void {
130
+ const message = `${colors.yellow("🎉 发现新版本")} ${colors.dim(
131
+ current
132
+ )} → ${colors.green(latest)} ${colors.dim("运行")} ${colors.cyan(
133
+ "gw update"
134
+ )} ${colors.dim("更新")}`;
135
+
136
+ console.log("");
137
+ console.log(
138
+ boxen(message, {
139
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
140
+ margin: { top: 0, bottom: 1, left: 0, right: 0 },
141
+ borderStyle: "round",
142
+ borderColor: "yellow",
143
+ align: "center",
144
+ })
145
+ );
146
+ }
147
+
148
+ /**
149
+ * 显示更新提示消息并让用户选择(交互式)
82
150
  * @returns "update" | "continue" | "dismiss"
83
151
  */
84
152
  async function showUpdateMessage(
@@ -87,7 +155,7 @@ async function showUpdateMessage(
87
155
  packageName: string
88
156
  ): Promise<"update" | "continue" | "dismiss"> {
89
157
  const message = [
90
- colors.bold(" 发现新版新本可用!"),
158
+ colors.yellow(colors.bold("🎉 发现新版本!")),
91
159
  "",
92
160
  `${colors.dim(current)} → ${colors.green(colors.bold(latest))}`,
93
161
  ].join("\n");
@@ -95,11 +163,12 @@ async function showUpdateMessage(
95
163
  console.log("");
96
164
  console.log(
97
165
  boxen(message, {
98
- padding: 1,
166
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
99
167
  margin: 1,
100
168
  borderStyle: "round",
101
169
  borderColor: "yellow",
102
- align: "left",
170
+ align: "center",
171
+ width: 40,
103
172
  })
104
173
  );
105
174
 
@@ -152,20 +221,25 @@ async function performUpdate(packageName: string): Promise<void> {
152
221
  });
153
222
 
154
223
  spinner.succeed(colors.green("更新成功!"));
224
+
225
+ // 清理缓存文件
226
+ clearUpdateCache();
227
+
155
228
  console.log("");
156
229
  console.log(
157
230
  boxen(
158
231
  [
159
- colors.bold("✨ 更新完成!"),
232
+ colors.green(colors.bold("✨ 更新完成!")),
160
233
  "",
161
234
  colors.dim("请重新打开终端使用新版本"),
162
235
  ].join("\n"),
163
236
  {
164
- padding: 1,
237
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
165
238
  margin: { top: 0, bottom: 1, left: 2, right: 2 },
166
239
  borderStyle: "round",
167
240
  borderColor: "green",
168
- align: "left",
241
+ align: "center",
242
+ width: 40,
169
243
  }
170
244
  )
171
245
  );
@@ -181,6 +255,20 @@ async function performUpdate(packageName: string): Promise<void> {
181
255
  }
182
256
  }
183
257
 
258
+ /**
259
+ * 清理更新缓存文件
260
+ */
261
+ export function clearUpdateCache(): void {
262
+ try {
263
+ const cacheFile = join(homedir(), CACHE_FILE);
264
+ if (existsSync(cacheFile)) {
265
+ unlinkSync(cacheFile);
266
+ }
267
+ } catch {
268
+ // 静默失败
269
+ }
270
+ }
271
+
184
272
  /**
185
273
  * 读取缓存
186
274
  */