@zjex/git-workflow 0.5.1 → 0.5.3

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.5.2](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.1...v0.5.2) (2026-02-06)
4
+
5
+ - 🔖 chore(release): 发布 v0.5.2 ([d2b659c](https://github.com/iamzjt-front-end/git-workflow/commit/d2b659c))
6
+ - refactor(update-notifier): Simplify version checking logic ([165e21f](https://github.com/iamzjt-front-end/git-workflow/commit/165e21f))
7
+
8
+ ## [v0.5.1](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.0...v0.5.1) (2026-02-06)
9
+
10
+ - 🔖 chore(release): 发布 v0.5.1 ([88c2089](https://github.com/iamzjt-front-end/git-workflow/commit/88c2089))
11
+ - feat(release): 优化发布脚本,自动同步更新 CHANGELOG 和 README 版本号 ([89af235](https://github.com/iamzjt-front-end/git-workflow/commit/89af235))
12
+
3
13
  ## [v0.5.0](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.7...v0.5.0) (2026-02-06)
4
14
 
5
15
  - 🔖 chore(release): 发布 v0.5.0 ([51c5740](https://github.com/iamzjt-front-end/git-workflow/commit/51c5740))
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-573%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-570%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
@@ -175,7 +175,7 @@ __export(update_notifier_exports, {
175
175
  checkForUpdates: () => checkForUpdates,
176
176
  clearUpdateCache: () => clearUpdateCache
177
177
  });
178
- import { execSync as execSync3 } from "child_process";
178
+ import { execSync as execSync3, spawn as spawn3 } from "child_process";
179
179
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
180
180
  import { homedir as homedir3 } from "os";
181
181
  import { join as join4 } from "path";
@@ -206,36 +206,45 @@ async function checkForUpdates(currentVersion, packageName = "@zjex/git-workflow
206
206
  }
207
207
  }
208
208
  }
209
- backgroundCheck(currentVersion, packageName);
209
+ spawnBackgroundCheck(packageName);
210
210
  } catch (error) {
211
211
  if (error?.constructor?.name === "ExitPromptError") {
212
212
  throw error;
213
213
  }
214
214
  }
215
215
  }
216
- function backgroundCheck(currentVersion, packageName) {
217
- const cache = readCache();
218
- const now = Date.now();
219
- const isUpToDate = cache?.latestVersion && !semver.gt(cache.latestVersion, currentVersion);
220
- const recentlyChecked = cache?.lastCheck && now - cache.lastCheck < CHECK_INTERVAL;
221
- if (isUpToDate && recentlyChecked) {
222
- return;
216
+ function spawnBackgroundCheck(packageName) {
217
+ try {
218
+ const cacheFile = join4(homedir3(), CACHE_FILE);
219
+ const script = `
220
+ const { execSync } = require('child_process');
221
+ const { writeFileSync, readFileSync, existsSync } = require('fs');
222
+ try {
223
+ const version = execSync('npm view ${packageName} version', {
224
+ encoding: 'utf-8',
225
+ timeout: 10000,
226
+ stdio: ['pipe', 'pipe', 'ignore']
227
+ }).trim();
228
+ if (version) {
229
+ let cache = {};
230
+ try {
231
+ if (existsSync('${cacheFile}')) {
232
+ cache = JSON.parse(readFileSync('${cacheFile}', 'utf-8'));
233
+ }
234
+ } catch {}
235
+ cache.lastCheck = Date.now();
236
+ cache.latestVersion = version;
237
+ writeFileSync('${cacheFile}', JSON.stringify(cache), 'utf-8');
238
+ }
239
+ } catch {}
240
+ `;
241
+ const child = spawn3("node", ["-e", script], {
242
+ detached: true,
243
+ stdio: "ignore"
244
+ });
245
+ child.unref();
246
+ } catch {
223
247
  }
224
- setImmediate(async () => {
225
- try {
226
- const latestVersion = await getLatestVersion(packageName);
227
- if (latestVersion) {
228
- const cache2 = readCache() || {};
229
- writeCache({
230
- ...cache2,
231
- lastCheck: Date.now(),
232
- latestVersion,
233
- checkedVersion: currentVersion
234
- });
235
- }
236
- } catch {
237
- }
238
- });
239
248
  }
240
249
  function isUsingVolta() {
241
250
  try {
@@ -245,19 +254,6 @@ function isUsingVolta() {
245
254
  return false;
246
255
  }
247
256
  }
248
- async function getLatestVersion(packageName) {
249
- try {
250
- const result = execSync3(`npm view ${packageName} version`, {
251
- encoding: "utf-8",
252
- timeout: 3e3,
253
- stdio: ["pipe", "pipe", "ignore"]
254
- // 忽略 stderr
255
- });
256
- return result.trim();
257
- } catch {
258
- return null;
259
- }
260
- }
261
257
  function showSimpleNotification(current, latest) {
262
258
  const message = `${colors.yellow("\u{1F389} \u53D1\u73B0\u65B0\u7248\u672C")} ${colors.dim(
263
259
  current
@@ -393,13 +389,12 @@ function writeCache(cache) {
393
389
  } catch {
394
390
  }
395
391
  }
396
- var DISMISS_INTERVAL, CHECK_INTERVAL, CACHE_FILE;
392
+ var DISMISS_INTERVAL, CACHE_FILE;
397
393
  var init_update_notifier = __esm({
398
394
  "src/update-notifier.ts"() {
399
395
  "use strict";
400
396
  init_utils();
401
397
  DISMISS_INTERVAL = 1e3 * 60 * 60 * 24;
402
- CHECK_INTERVAL = 1e3 * 60 * 60 * 1;
403
398
  CACHE_FILE = ".gw-update-check";
404
399
  }
405
400
  });
@@ -2817,7 +2812,7 @@ init_update_notifier();
2817
2812
 
2818
2813
  // src/commands/update.ts
2819
2814
  init_utils();
2820
- import { execSync as execSync4, spawn as spawn3 } from "child_process";
2815
+ import { execSync as execSync4, spawn as spawn4 } from "child_process";
2821
2816
  import ora6 from "ora";
2822
2817
  import boxen3 from "boxen";
2823
2818
  import semver2 from "semver";
@@ -2834,9 +2829,9 @@ function clearUpdateCache2() {
2834
2829
  } catch {
2835
2830
  }
2836
2831
  }
2837
- async function getLatestVersion2(packageName) {
2832
+ async function getLatestVersion(packageName) {
2838
2833
  return new Promise((resolve) => {
2839
- const npmView = spawn3("npm", ["view", packageName, "version"], {
2834
+ const npmView = spawn4("npm", ["view", packageName, "version"], {
2840
2835
  stdio: ["ignore", "pipe", "ignore"],
2841
2836
  timeout: 5e3
2842
2837
  });
@@ -2874,7 +2869,7 @@ async function update(currentVersion) {
2874
2869
  console.log("");
2875
2870
  const spinner = ora6("\u6B63\u5728\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F...").start();
2876
2871
  try {
2877
- const latestVersion = await getLatestVersion2(packageName);
2872
+ const latestVersion = await getLatestVersion(packageName);
2878
2873
  if (!latestVersion) {
2879
2874
  spinner.fail("\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F");
2880
2875
  console.log(colors.dim(" \u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5"));
@@ -2925,7 +2920,7 @@ async function update(currentVersion) {
2925
2920
  console.log("");
2926
2921
  const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
2927
2922
  const [command, ...args] = updateCommand.split(" ");
2928
- const updateProcess = spawn3(command, args, {
2923
+ const updateProcess = spawn4(command, args, {
2929
2924
  stdio: "inherit"
2930
2925
  // 继承父进程的 stdio,显示实时输出
2931
2926
  });
@@ -2996,7 +2991,7 @@ async function update(currentVersion) {
2996
2991
  init_utils();
2997
2992
  import { execSync as execSync5 } from "child_process";
2998
2993
  import boxen4 from "boxen";
2999
- import { spawn as spawn4 } from "child_process";
2994
+ import { spawn as spawn5 } from "child_process";
3000
2995
  function parseGitLog(output) {
3001
2996
  const commits = [];
3002
2997
  const lines = output.trim().split("\n");
@@ -3214,7 +3209,7 @@ function formatTimelineStyle(commits) {
3214
3209
  function startInteractivePager(content) {
3215
3210
  const pager = process.env.PAGER || "less";
3216
3211
  try {
3217
- const pagerProcess = spawn4(pager, ["-R", "-S", "-F", "-X", "-i"], {
3212
+ const pagerProcess = spawn5(pager, ["-R", "-S", "-F", "-X", "-i"], {
3218
3213
  stdio: ["pipe", "inherit", "inherit"],
3219
3214
  env: { ...process.env, LESS: "-R -S -F -X -i" }
3220
3215
  });
@@ -3630,16 +3625,19 @@ var AI_PROVIDERS2 = {
3630
3625
  defaultModel: "qwen2.5-coder:14b"
3631
3626
  }
3632
3627
  };
3628
+ function parseCommitLine(line) {
3629
+ const parts = line.split("|");
3630
+ if (parts.length < 5) return null;
3631
+ const [hash, shortHash, subject, author, date] = parts;
3632
+ return { hash, shortHash, subject, author, date };
3633
+ }
3633
3634
  function getRecentCommits3(limit = 20) {
3634
3635
  try {
3635
3636
  const output = execOutput(
3636
3637
  `git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`
3637
3638
  );
3638
3639
  if (!output) return [];
3639
- return output.split("\n").filter(Boolean).map((line) => {
3640
- const [hash, shortHash, subject, author, date] = line.split("|");
3641
- return { hash, shortHash, subject, author, date };
3642
- });
3640
+ return output.split("\n").filter(Boolean).map((line) => parseCommitLine(line)).filter((c) => c !== null);
3643
3641
  } catch {
3644
3642
  return [];
3645
3643
  }
@@ -4079,25 +4077,49 @@ async function review(hashes, options = {}) {
4079
4077
  let diff = "";
4080
4078
  let commits = [];
4081
4079
  if (hashes && hashes.length > 0) {
4082
- commits = hashes.map((hash) => {
4083
- const info = execOutput(
4084
- `git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
4085
- );
4086
- if (!info) {
4087
- console.log(colors.red(`\u274C \u627E\u4E0D\u5230 commit: ${hash}`));
4080
+ if (hashes.length === 1 && hashes[0].includes("..") && !hashes[0].includes("...")) {
4081
+ const range = hashes[0];
4082
+ const [startHash, endHash] = range.split("..");
4083
+ const inclusiveRange = `${startHash}^..${endHash}`;
4084
+ try {
4085
+ const output = execOutput(
4086
+ `git log ${inclusiveRange} --pretty=format:"%H|%h|%s|%an|%ad" --date=short --reverse`
4087
+ );
4088
+ if (!output) {
4089
+ console.log(colors.red(`\u274C \u65E0\u6548\u7684 commit \u8303\u56F4: ${range}`));
4090
+ process.exit(1);
4091
+ }
4092
+ commits = output.split("\n").filter(Boolean).map((line) => parseCommitLine(line)).filter((c) => c !== null);
4093
+ diff = execOutput(`git diff ${inclusiveRange}`) || "";
4094
+ } catch {
4095
+ console.log(colors.red(`\u274C \u65E0\u6548\u7684 commit \u8303\u56F4: ${range}`));
4088
4096
  process.exit(1);
4089
4097
  }
4090
- const [fullHash, shortHash, subject, author, date] = info.split("|");
4091
- return { hash: fullHash, shortHash, subject, author, date };
4092
- });
4093
- diff = getMultipleCommitsDiff(hashes);
4098
+ } else {
4099
+ commits = hashes.map((hash) => {
4100
+ const info = execOutput(
4101
+ `git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
4102
+ );
4103
+ if (!info) {
4104
+ console.log(colors.red(`\u274C \u627E\u4E0D\u5230 commit: ${hash}`));
4105
+ process.exit(1);
4106
+ }
4107
+ const commit2 = parseCommitLine(info);
4108
+ if (!commit2) {
4109
+ console.log(colors.red(`\u274C \u65E0\u6CD5\u89E3\u6790 commit \u4FE1\u606F: ${hash}`));
4110
+ process.exit(1);
4111
+ }
4112
+ return commit2;
4113
+ });
4114
+ diff = getMultipleCommitsDiff(hashes);
4115
+ }
4094
4116
  } else if (options.last) {
4095
4117
  commits = getRecentCommits3(options.last);
4096
4118
  diff = getMultipleCommitsDiff(commits.map((c) => c.hash));
4097
4119
  } else if (options.staged) {
4098
4120
  diff = getStagedDiff();
4099
4121
  } else {
4100
- const recentCommits = getRecentCommits3(20);
4122
+ const recentCommits = getRecentCommits3(10);
4101
4123
  const stagedDiff = getStagedDiff();
4102
4124
  const choices = [];
4103
4125
  if (stagedDiff) {
@@ -4210,7 +4232,7 @@ process.on("SIGTERM", () => {
4210
4232
  console.log("");
4211
4233
  process.exit(0);
4212
4234
  });
4213
- var version = true ? "0.5.1" : "0.0.0-dev";
4235
+ var version = true ? "0.5.3" : "0.0.0-dev";
4214
4236
  async function mainMenu() {
4215
4237
  console.log(
4216
4238
  colors.green(`
@@ -14,6 +14,9 @@ gw review abc1234
14
14
  # 审查多个 commits
15
15
  gw review abc1234 def5678
16
16
 
17
+ # 审查 commit 范围(包含 abc1234 和 def5678 的所有 commits)
18
+ gw review abc1234..def5678
19
+
17
20
  # 审查最近 N 个 commits
18
21
  gw review -n 3
19
22
  gw review --last 3
@@ -21,6 +21,9 @@ gw review abc1234
21
21
  # 审查多个 commits
22
22
  gw review abc1234 def5678
23
23
 
24
+ # 审查 commit 范围(包含 abc1234 和 def5678 的所有 commits)
25
+ gw review abc1234..def5678
26
+
24
27
  # 审查最近 N 个 commits
25
28
  gw review -n 3
26
29
 
@@ -137,6 +140,9 @@ gw c
137
140
  ```bash
138
141
  # 审查某个 PR 的所有 commits
139
142
  gw review abc1234 def5678 ghi9012
143
+
144
+ # 或使用范围语法审查从 abc1234 到 def5678 的所有 commits(包含两端)
145
+ gw review abc1234..def5678
140
146
  ```
141
147
 
142
148
  ### 3. 定期代码审计
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjex/git-workflow",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,6 +71,17 @@ const AI_PROVIDERS: Record<string, AIProvider> = {
71
71
 
72
72
  // ========== 辅助函数 ==========
73
73
 
74
+ /**
75
+ * 解析 git log 输出的 commit 信息
76
+ */
77
+ function parseCommitLine(line: string): CommitInfo | null {
78
+ const parts = line.split("|");
79
+ if (parts.length < 5) return null;
80
+
81
+ const [hash, shortHash, subject, author, date] = parts;
82
+ return { hash, shortHash, subject, author, date };
83
+ }
84
+
74
85
  /**
75
86
  * 获取最近的 commits 列表
76
87
  */
@@ -84,10 +95,8 @@ function getRecentCommits(limit: number = 20): CommitInfo[] {
84
95
  return output
85
96
  .split("\n")
86
97
  .filter(Boolean)
87
- .map((line) => {
88
- const [hash, shortHash, subject, author, date] = line.split("|");
89
- return { hash, shortHash, subject, author, date };
90
- });
98
+ .map((line) => parseCommitLine(line))
99
+ .filter((c): c is CommitInfo => c !== null);
91
100
  } catch {
92
101
  return [];
93
102
  }
@@ -629,19 +638,53 @@ export async function review(
629
638
 
630
639
  // 确定要审查的内容
631
640
  if (hashes && hashes.length > 0) {
632
- // 指定了 commit hash
633
- commits = hashes.map((hash) => {
634
- const info = execOutput(
635
- `git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
636
- );
637
- if (!info) {
638
- console.log(colors.red(`❌ 找不到 commit: ${hash}`));
641
+ // 检查是否是范围语法 (abc123..def456)
642
+ if (hashes.length === 1 && hashes[0].includes("..") && !hashes[0].includes("...")) {
643
+ const range = hashes[0];
644
+ const [startHash, endHash] = range.split("..");
645
+
646
+ // 使用 startHash^..endHash 来包含起始 commit(闭区间 [A, B])
647
+ const inclusiveRange = `${startHash}^..${endHash}`;
648
+
649
+ // 获取范围内的所有 commits
650
+ try {
651
+ const output = execOutput(
652
+ `git log ${inclusiveRange} --pretty=format:"%H|%h|%s|%an|%ad" --date=short --reverse`
653
+ );
654
+ if (!output) {
655
+ console.log(colors.red(`❌ 无效的 commit 范围: ${range}`));
656
+ process.exit(1);
657
+ }
658
+ commits = output
659
+ .split("\n")
660
+ .filter(Boolean)
661
+ .map((line) => parseCommitLine(line))
662
+ .filter((c): c is CommitInfo => c !== null);
663
+ // 获取范围 diff
664
+ diff = execOutput(`git diff ${inclusiveRange}`) || "";
665
+ } catch {
666
+ console.log(colors.red(`❌ 无效的 commit 范围: ${range}`));
639
667
  process.exit(1);
640
668
  }
641
- const [fullHash, shortHash, subject, author, date] = info.split("|");
642
- return { hash: fullHash, shortHash, subject, author, date };
643
- });
644
- diff = getMultipleCommitsDiff(hashes);
669
+ } else {
670
+ // 指定了单个或多个 commit hash
671
+ commits = hashes.map((hash) => {
672
+ const info = execOutput(
673
+ `git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
674
+ );
675
+ if (!info) {
676
+ console.log(colors.red(`❌ 找不到 commit: ${hash}`));
677
+ process.exit(1);
678
+ }
679
+ const commit = parseCommitLine(info);
680
+ if (!commit) {
681
+ console.log(colors.red(`❌ 无法解析 commit 信息: ${hash}`));
682
+ process.exit(1);
683
+ }
684
+ return commit;
685
+ });
686
+ diff = getMultipleCommitsDiff(hashes);
687
+ }
645
688
  } else if (options.last) {
646
689
  // 审查最近 N 个 commits
647
690
  commits = getRecentCommits(options.last);
@@ -651,7 +694,7 @@ export async function review(
651
694
  diff = getStagedDiff();
652
695
  } else {
653
696
  // 交互式选择
654
- const recentCommits = getRecentCommits(20);
697
+ const recentCommits = getRecentCommits(10);
655
698
  const stagedDiff = getStagedDiff();
656
699
 
657
700
  const choices: any[] = [];
@@ -1,4 +1,4 @@
1
- import { execSync } from "child_process";
1
+ import { execSync, spawn } from "child_process";
2
2
  import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
@@ -9,7 +9,6 @@ import semver from "semver";
9
9
  import { colors } from "./utils.js";
10
10
 
11
11
  const DISMISS_INTERVAL = 1000 * 60 * 60 * 24; // 24 小时后再次提示
12
- const CHECK_INTERVAL = 1000 * 60 * 60 * 1; // 已是最新版本时,1 小时检查一次
13
12
  const CACHE_FILE = ".gw-update-check";
14
13
 
15
14
  interface UpdateCache {
@@ -35,7 +34,7 @@ export async function checkForUpdates(
35
34
  const cache = readCache();
36
35
  const now = Date.now();
37
36
 
38
- // 1. 先用缓存的结果提示用户(如果有新版本)
37
+ // 1. 先用缓存的结果提示用户(如果有更新版本)
39
38
  if (
40
39
  cache?.latestVersion &&
41
40
  semver.gt(cache.latestVersion, currentVersion)
@@ -63,8 +62,8 @@ export async function checkForUpdates(
63
62
  }
64
63
  }
65
64
 
66
- // 2. 后台异步检查更新(每次都检查,不阻塞)
67
- backgroundCheck(currentVersion, packageName);
65
+ // 2. 后台子进程检查更新(每次都检查,不阻塞)
66
+ spawnBackgroundCheck(packageName);
68
67
  } catch (error) {
69
68
  // 如果是用户按 Ctrl+C,重新抛出让全局处理
70
69
  if (error?.constructor?.name === "ExitPromptError") {
@@ -75,42 +74,47 @@ export async function checkForUpdates(
75
74
  }
76
75
 
77
76
  /**
78
- * 后台异步检查更新(不阻塞)
79
- * - 有新版本时:每次都检查
80
- * - 已是最新版本时:1 小时检查一次
77
+ * 在子进程中检查更新(不阻塞主进程)
78
+ * 使用 unref() 确保主进程退出后子进程仍能完成
81
79
  */
82
- function backgroundCheck(currentVersion: string, packageName: string): void {
83
- const cache = readCache();
84
- const now = Date.now();
85
-
86
- // 如果已是最新版本,且距离上次检查不到 1 小时,跳过
87
- const isUpToDate =
88
- cache?.latestVersion && !semver.gt(cache.latestVersion, currentVersion);
89
- const recentlyChecked =
90
- cache?.lastCheck && now - cache.lastCheck < CHECK_INTERVAL;
91
-
92
- if (isUpToDate && recentlyChecked) {
93
- return;
94
- }
80
+ function spawnBackgroundCheck(packageName: string): void {
81
+ try {
82
+ const cacheFile = join(homedir(), CACHE_FILE);
83
+
84
+ // 使用 node -e 执行检查脚本
85
+ const script = `
86
+ const { execSync } = require('child_process');
87
+ const { writeFileSync, readFileSync, existsSync } = require('fs');
88
+ try {
89
+ const version = execSync('npm view ${packageName} version', {
90
+ encoding: 'utf-8',
91
+ timeout: 10000,
92
+ stdio: ['pipe', 'pipe', 'ignore']
93
+ }).trim();
94
+ if (version) {
95
+ let cache = {};
96
+ try {
97
+ if (existsSync('${cacheFile}')) {
98
+ cache = JSON.parse(readFileSync('${cacheFile}', 'utf-8'));
99
+ }
100
+ } catch {}
101
+ cache.lastCheck = Date.now();
102
+ cache.latestVersion = version;
103
+ writeFileSync('${cacheFile}', JSON.stringify(cache), 'utf-8');
104
+ }
105
+ } catch {}
106
+ `;
95
107
 
96
- // 使用 setImmediate 确保不阻塞主流程
97
- setImmediate(async () => {
98
- try {
99
- const latestVersion = await getLatestVersion(packageName);
100
-
101
- if (latestVersion) {
102
- const cache = readCache() || {};
103
- writeCache({
104
- ...cache,
105
- lastCheck: Date.now(),
106
- latestVersion,
107
- checkedVersion: currentVersion,
108
- });
109
- }
110
- } catch {
111
- // 静默失败
112
- }
113
- });
108
+ const child = spawn("node", ["-e", script], {
109
+ detached: true,
110
+ stdio: "ignore",
111
+ });
112
+
113
+ // unref 让主进程可以独立退出
114
+ child.unref();
115
+ } catch {
116
+ // 静默失败
117
+ }
114
118
  }
115
119
 
116
120
  /**
@@ -125,22 +129,6 @@ function isUsingVolta(): boolean {
125
129
  }
126
130
  }
127
131
 
128
- /**
129
- * 获取 npm 上的最新版本
130
- */
131
- async function getLatestVersion(packageName: string): Promise<string | null> {
132
- try {
133
- const result = execSync(`npm view ${packageName} version`, {
134
- encoding: "utf-8",
135
- timeout: 3000,
136
- stdio: ["pipe", "pipe", "ignore"], // 忽略 stderr
137
- });
138
- return result.trim();
139
- } catch {
140
- return null;
141
- }
142
- }
143
-
144
132
  /**
145
133
  * 显示简单的更新通知(非交互式,不阻塞)
146
134
  */
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { execSync } from "child_process";
2
+ import { execSync, spawn } from "child_process";
3
3
  import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { checkForUpdates, clearUpdateCache } from "../src/update-notifier";
@@ -28,6 +28,11 @@ describe("Update Notifier 模块测试", () => {
28
28
  vi.clearAllMocks();
29
29
  vi.mocked(homedir).mockReturnValue("/home/user");
30
30
  vi.useFakeTimers();
31
+
32
+ // Mock spawn 返回一个带 unref 的对象
33
+ vi.mocked(spawn).mockReturnValue({
34
+ unref: vi.fn(),
35
+ } as any);
31
36
  });
32
37
 
33
38
  afterEach(() => {
@@ -62,16 +67,20 @@ describe("Update Notifier 模块测试", () => {
62
67
  });
63
68
 
64
69
  describe("checkForUpdates 函数", () => {
65
- it("没有缓存时应该后台检查", async () => {
70
+ it("没有缓存时应该启动后台检查", async () => {
66
71
  vi.mocked(existsSync).mockReturnValue(false);
67
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
68
72
 
69
73
  await checkForUpdates("1.0.0");
70
74
 
71
- // 等待异步操作
72
- await vi.runAllTimersAsync();
73
-
74
- expect(writeFileSync).toHaveBeenCalled();
75
+ // 没有缓存时应该启动子进程检查
76
+ expect(spawn).toHaveBeenCalledWith(
77
+ "node",
78
+ expect.arrayContaining(["-e", expect.any(String)]),
79
+ expect.objectContaining({
80
+ detached: true,
81
+ stdio: "ignore",
82
+ })
83
+ );
75
84
  });
76
85
 
77
86
  it("版本相同时不应该显示提示", async () => {
@@ -153,62 +162,20 @@ describe("Update Notifier 模块测试", () => {
153
162
  consoleSpy.mockRestore();
154
163
  });
155
164
 
156
- it("已是最新版本且1小时内不应该重复检查", async () => {
157
- const mockCache = {
158
- lastCheck: Date.now(),
159
- latestVersion: "1.0.0",
160
- checkedVersion: "1.0.0",
161
- };
162
-
163
- vi.mocked(existsSync).mockReturnValue(true);
164
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
165
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
166
-
167
- await checkForUpdates("1.0.0");
168
- await vi.runAllTimersAsync();
169
-
170
- // writeFileSync 不应该被调用(因为已是最新版本且在1小时内)
171
- expect(writeFileSync).not.toHaveBeenCalled();
172
- });
173
-
174
- it("已是最新版本但超过1小时应该重新检查", async () => {
175
- const oneHourAgo = Date.now() - 2 * 60 * 60 * 1000; // 2小时前
176
- const mockCache = {
177
- lastCheck: oneHourAgo,
178
- latestVersion: "1.0.0",
179
- checkedVersion: "1.0.0",
180
- };
181
-
182
- vi.mocked(existsSync).mockReturnValue(true);
183
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
184
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
185
-
186
- await checkForUpdates("1.0.0");
187
- await vi.runAllTimersAsync();
188
-
189
- expect(writeFileSync).toHaveBeenCalled();
190
- });
191
-
192
- it("有新版本时每次都应该后台检查", async () => {
165
+ it("每次运行都应该启动后台检查", async () => {
193
166
  const mockCache = {
194
167
  lastCheck: Date.now(), // 刚刚检查过
195
- latestVersion: "1.0.1", // 有新版本
168
+ latestVersion: "1.0.0",
196
169
  checkedVersion: "1.0.0",
197
170
  };
198
171
 
199
172
  vi.mocked(existsSync).mockReturnValue(true);
200
173
  vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
201
- vi.mocked(execSync).mockReturnValue("1.0.2" as any);
202
-
203
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
204
174
 
205
175
  await checkForUpdates("1.0.0");
206
- await vi.runAllTimersAsync();
207
-
208
- // 即使刚检查过,有新版本时也应该继续检查
209
- expect(writeFileSync).toHaveBeenCalled();
210
176
 
211
- consoleSpy.mockRestore();
177
+ // 每次都应该启动子进程检查
178
+ expect(spawn).toHaveBeenCalled();
212
179
  });
213
180
 
214
181
  it("缓存文件损坏时应该静默处理", async () => {
@@ -311,7 +278,7 @@ describe("Update Notifier 模块测试", () => {
311
278
  { current: "1.0.0", latest: "1.0.1", shouldShow: true },
312
279
  { current: "1.0.0", latest: "1.1.0", shouldShow: true },
313
280
  { current: "1.0.0", latest: "2.0.0", shouldShow: true },
314
- { current: "1.0.1", latest: "1.0.0", shouldShow: false },
281
+ { current: "1.0.1", latest: "1.0.0", shouldShow: false }, // 本地版本更高,不提示
315
282
  { current: "1.0.0", latest: "1.0.0", shouldShow: false },
316
283
  ];
317
284
 
@@ -344,23 +311,8 @@ describe("Update Notifier 模块测试", () => {
344
311
  });
345
312
 
346
313
  describe("缓存读写", () => {
347
- it("应该正确写入缓存", async () => {
348
- vi.mocked(existsSync).mockReturnValue(false);
349
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
350
-
351
- await checkForUpdates("1.0.0");
352
- await vi.runAllTimersAsync();
353
-
354
- expect(writeFileSync).toHaveBeenCalledWith(
355
- "/home/user/.gw-update-check",
356
- expect.stringContaining("1.0.1"),
357
- "utf-8"
358
- );
359
- });
360
-
361
314
  it("写入缓存失败时应该静默处理", async () => {
362
315
  vi.mocked(existsSync).mockReturnValue(false);
363
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
364
316
  vi.mocked(writeFileSync).mockImplementation(() => {
365
317
  throw new Error("Write failed");
366
318
  });
@@ -381,24 +333,24 @@ describe("Update Notifier 模块测试", () => {
381
333
  describe("网络请求", () => {
382
334
  it("获取最新版本失败时应该静默处理", async () => {
383
335
  vi.mocked(existsSync).mockReturnValue(false);
384
- vi.mocked(execSync).mockImplementation(() => {
385
- throw new Error("Network error");
336
+ vi.mocked(spawn).mockImplementation(() => {
337
+ throw new Error("Spawn error");
386
338
  });
387
339
 
388
340
  await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
389
341
  });
390
342
 
391
- it("应该使用正确的 npm 命令", async () => {
343
+ it("后台检查应该使用正确的参数", async () => {
392
344
  vi.mocked(existsSync).mockReturnValue(false);
393
- vi.mocked(execSync).mockReturnValue("1.0.1" as any);
394
345
 
395
346
  await checkForUpdates("1.0.0", "@zjex/git-workflow");
396
- await vi.runAllTimersAsync();
397
347
 
398
- expect(execSync).toHaveBeenCalledWith(
399
- "npm view @zjex/git-workflow version",
348
+ expect(spawn).toHaveBeenCalledWith(
349
+ "node",
350
+ expect.arrayContaining(["-e", expect.stringContaining("npm view @zjex/git-workflow version")]),
400
351
  expect.objectContaining({
401
- timeout: 3000,
352
+ detached: true,
353
+ stdio: "ignore",
402
354
  })
403
355
  );
404
356
  });