@zjex/git-workflow 0.5.2 → 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 +5 -0
- package/README.md +1 -1
- package/dist/index.js +88 -57
- package/docs/commands/review.md +3 -0
- package/docs/guide/ai-review.md +6 -0
- package/package.json +1 -1
- package/src/commands/review.ts +59 -16
- package/src/update-notifier.ts +46 -42
- package/tests/update-notifier.test.ts +29 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
+
|
|
3
8
|
## [v0.5.1](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.0...v0.5.1) (2026-02-06)
|
|
4
9
|
|
|
5
10
|
- 🔖 chore(release): 发布 v0.5.1 ([88c2089](https://github.com/iamzjt-front-end/git-workflow/commit/88c2089))
|
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-
|
|
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,18 +175,19 @@ __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";
|
|
182
182
|
import boxen2 from "boxen";
|
|
183
183
|
import { select as select7 } from "@inquirer/prompts";
|
|
184
184
|
import ora5 from "ora";
|
|
185
|
+
import semver from "semver";
|
|
185
186
|
async function checkForUpdates(currentVersion, packageName = "@zjex/git-workflow", interactive = false) {
|
|
186
187
|
try {
|
|
187
188
|
const cache = readCache();
|
|
188
189
|
const now = Date.now();
|
|
189
|
-
if (cache?.latestVersion && cache.latestVersion
|
|
190
|
+
if (cache?.latestVersion && semver.gt(cache.latestVersion, currentVersion)) {
|
|
190
191
|
const isDismissed = cache.lastDismiss && now - cache.lastDismiss < DISMISS_INTERVAL;
|
|
191
192
|
if (!isDismissed) {
|
|
192
193
|
if (interactive) {
|
|
@@ -205,29 +206,45 @@ async function checkForUpdates(currentVersion, packageName = "@zjex/git-workflow
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
|
-
|
|
209
|
+
spawnBackgroundCheck(packageName);
|
|
209
210
|
} catch (error) {
|
|
210
211
|
if (error?.constructor?.name === "ExitPromptError") {
|
|
211
212
|
throw error;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 {
|
|
247
|
+
}
|
|
231
248
|
}
|
|
232
249
|
function isUsingVolta() {
|
|
233
250
|
try {
|
|
@@ -237,19 +254,6 @@ function isUsingVolta() {
|
|
|
237
254
|
return false;
|
|
238
255
|
}
|
|
239
256
|
}
|
|
240
|
-
async function getLatestVersion(packageName) {
|
|
241
|
-
try {
|
|
242
|
-
const result = execSync3(`npm view ${packageName} version`, {
|
|
243
|
-
encoding: "utf-8",
|
|
244
|
-
timeout: 3e3,
|
|
245
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
246
|
-
// 忽略 stderr
|
|
247
|
-
});
|
|
248
|
-
return result.trim();
|
|
249
|
-
} catch {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
257
|
function showSimpleNotification(current, latest) {
|
|
254
258
|
const message = `${colors.yellow("\u{1F389} \u53D1\u73B0\u65B0\u7248\u672C")} ${colors.dim(
|
|
255
259
|
current
|
|
@@ -2808,10 +2812,10 @@ init_update_notifier();
|
|
|
2808
2812
|
|
|
2809
2813
|
// src/commands/update.ts
|
|
2810
2814
|
init_utils();
|
|
2811
|
-
import { execSync as execSync4, spawn as
|
|
2815
|
+
import { execSync as execSync4, spawn as spawn4 } from "child_process";
|
|
2812
2816
|
import ora6 from "ora";
|
|
2813
2817
|
import boxen3 from "boxen";
|
|
2814
|
-
import
|
|
2818
|
+
import semver2 from "semver";
|
|
2815
2819
|
import { existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
2816
2820
|
import { homedir as homedir4 } from "os";
|
|
2817
2821
|
import { join as join5 } from "path";
|
|
@@ -2825,9 +2829,9 @@ function clearUpdateCache2() {
|
|
|
2825
2829
|
} catch {
|
|
2826
2830
|
}
|
|
2827
2831
|
}
|
|
2828
|
-
async function
|
|
2832
|
+
async function getLatestVersion(packageName) {
|
|
2829
2833
|
return new Promise((resolve) => {
|
|
2830
|
-
const npmView =
|
|
2834
|
+
const npmView = spawn4("npm", ["view", packageName, "version"], {
|
|
2831
2835
|
stdio: ["ignore", "pipe", "ignore"],
|
|
2832
2836
|
timeout: 5e3
|
|
2833
2837
|
});
|
|
@@ -2865,14 +2869,14 @@ async function update(currentVersion) {
|
|
|
2865
2869
|
console.log("");
|
|
2866
2870
|
const spinner = ora6("\u6B63\u5728\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F...").start();
|
|
2867
2871
|
try {
|
|
2868
|
-
const latestVersion = await
|
|
2872
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
2869
2873
|
if (!latestVersion) {
|
|
2870
2874
|
spinner.fail("\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F");
|
|
2871
2875
|
console.log(colors.dim(" \u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5"));
|
|
2872
2876
|
return;
|
|
2873
2877
|
}
|
|
2874
2878
|
spinner.stop();
|
|
2875
|
-
if (
|
|
2879
|
+
if (semver2.gte(currentVersion, latestVersion)) {
|
|
2876
2880
|
console.log(
|
|
2877
2881
|
boxen3(
|
|
2878
2882
|
[
|
|
@@ -2916,7 +2920,7 @@ async function update(currentVersion) {
|
|
|
2916
2920
|
console.log("");
|
|
2917
2921
|
const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
|
|
2918
2922
|
const [command, ...args] = updateCommand.split(" ");
|
|
2919
|
-
const updateProcess =
|
|
2923
|
+
const updateProcess = spawn4(command, args, {
|
|
2920
2924
|
stdio: "inherit"
|
|
2921
2925
|
// 继承父进程的 stdio,显示实时输出
|
|
2922
2926
|
});
|
|
@@ -2987,7 +2991,7 @@ async function update(currentVersion) {
|
|
|
2987
2991
|
init_utils();
|
|
2988
2992
|
import { execSync as execSync5 } from "child_process";
|
|
2989
2993
|
import boxen4 from "boxen";
|
|
2990
|
-
import { spawn as
|
|
2994
|
+
import { spawn as spawn5 } from "child_process";
|
|
2991
2995
|
function parseGitLog(output) {
|
|
2992
2996
|
const commits = [];
|
|
2993
2997
|
const lines = output.trim().split("\n");
|
|
@@ -3205,7 +3209,7 @@ function formatTimelineStyle(commits) {
|
|
|
3205
3209
|
function startInteractivePager(content) {
|
|
3206
3210
|
const pager = process.env.PAGER || "less";
|
|
3207
3211
|
try {
|
|
3208
|
-
const pagerProcess =
|
|
3212
|
+
const pagerProcess = spawn5(pager, ["-R", "-S", "-F", "-X", "-i"], {
|
|
3209
3213
|
stdio: ["pipe", "inherit", "inherit"],
|
|
3210
3214
|
env: { ...process.env, LESS: "-R -S -F -X -i" }
|
|
3211
3215
|
});
|
|
@@ -3621,16 +3625,19 @@ var AI_PROVIDERS2 = {
|
|
|
3621
3625
|
defaultModel: "qwen2.5-coder:14b"
|
|
3622
3626
|
}
|
|
3623
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
|
+
}
|
|
3624
3634
|
function getRecentCommits3(limit = 20) {
|
|
3625
3635
|
try {
|
|
3626
3636
|
const output = execOutput(
|
|
3627
3637
|
`git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`
|
|
3628
3638
|
);
|
|
3629
3639
|
if (!output) return [];
|
|
3630
|
-
return output.split("\n").filter(Boolean).map((line) =>
|
|
3631
|
-
const [hash, shortHash, subject, author, date] = line.split("|");
|
|
3632
|
-
return { hash, shortHash, subject, author, date };
|
|
3633
|
-
});
|
|
3640
|
+
return output.split("\n").filter(Boolean).map((line) => parseCommitLine(line)).filter((c) => c !== null);
|
|
3634
3641
|
} catch {
|
|
3635
3642
|
return [];
|
|
3636
3643
|
}
|
|
@@ -4070,25 +4077,49 @@ async function review(hashes, options = {}) {
|
|
|
4070
4077
|
let diff = "";
|
|
4071
4078
|
let commits = [];
|
|
4072
4079
|
if (hashes && hashes.length > 0) {
|
|
4073
|
-
|
|
4074
|
-
const
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
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}`));
|
|
4079
4096
|
process.exit(1);
|
|
4080
4097
|
}
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
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
|
+
}
|
|
4085
4116
|
} else if (options.last) {
|
|
4086
4117
|
commits = getRecentCommits3(options.last);
|
|
4087
4118
|
diff = getMultipleCommitsDiff(commits.map((c) => c.hash));
|
|
4088
4119
|
} else if (options.staged) {
|
|
4089
4120
|
diff = getStagedDiff();
|
|
4090
4121
|
} else {
|
|
4091
|
-
const recentCommits = getRecentCommits3(
|
|
4122
|
+
const recentCommits = getRecentCommits3(10);
|
|
4092
4123
|
const stagedDiff = getStagedDiff();
|
|
4093
4124
|
const choices = [];
|
|
4094
4125
|
if (stagedDiff) {
|
|
@@ -4201,7 +4232,7 @@ process.on("SIGTERM", () => {
|
|
|
4201
4232
|
console.log("");
|
|
4202
4233
|
process.exit(0);
|
|
4203
4234
|
});
|
|
4204
|
-
var version = true ? "0.5.
|
|
4235
|
+
var version = true ? "0.5.3" : "0.0.0-dev";
|
|
4205
4236
|
async function mainMenu() {
|
|
4206
4237
|
console.log(
|
|
4207
4238
|
colors.green(`
|
package/docs/commands/review.md
CHANGED
package/docs/guide/ai-review.md
CHANGED
|
@@ -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
package/src/commands/review.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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(
|
|
697
|
+
const recentCommits = getRecentCommits(10);
|
|
655
698
|
const stagedDiff = getStagedDiff();
|
|
656
699
|
|
|
657
700
|
const choices: any[] = [];
|
package/src/update-notifier.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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";
|
|
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
|
|
|
10
11
|
const DISMISS_INTERVAL = 1000 * 60 * 60 * 24; // 24 小时后再次提示
|
|
@@ -33,10 +34,10 @@ export async function checkForUpdates(
|
|
|
33
34
|
const cache = readCache();
|
|
34
35
|
const now = Date.now();
|
|
35
36
|
|
|
36
|
-
// 1.
|
|
37
|
+
// 1. 先用缓存的结果提示用户(如果有更新版本)
|
|
37
38
|
if (
|
|
38
39
|
cache?.latestVersion &&
|
|
39
|
-
cache.latestVersion
|
|
40
|
+
semver.gt(cache.latestVersion, currentVersion)
|
|
40
41
|
) {
|
|
41
42
|
// 检查用户是否在 24 小时内关闭过提示
|
|
42
43
|
const isDismissed =
|
|
@@ -61,8 +62,8 @@ export async function checkForUpdates(
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// 2.
|
|
65
|
-
|
|
65
|
+
// 2. 后台子进程检查更新(每次都检查,不阻塞)
|
|
66
|
+
spawnBackgroundCheck(packageName);
|
|
66
67
|
} catch (error) {
|
|
67
68
|
// 如果是用户按 Ctrl+C,重新抛出让全局处理
|
|
68
69
|
if (error?.constructor?.name === "ExitPromptError") {
|
|
@@ -73,28 +74,47 @@ export async function checkForUpdates(
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
77
|
+
* 在子进程中检查更新(不阻塞主进程)
|
|
78
|
+
* 使用 unref() 确保主进程退出后子进程仍能完成
|
|
78
79
|
*/
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
`;
|
|
107
|
+
|
|
108
|
+
const child = spawn("node", ["-e", script], {
|
|
109
|
+
detached: true,
|
|
110
|
+
stdio: "ignore",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// unref 让主进程可以独立退出
|
|
114
|
+
child.unref();
|
|
115
|
+
} catch {
|
|
116
|
+
// 静默失败
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
/**
|
|
@@ -109,22 +129,6 @@ function isUsingVolta(): boolean {
|
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
/**
|
|
113
|
-
* 获取 npm 上的最新版本
|
|
114
|
-
*/
|
|
115
|
-
async function getLatestVersion(packageName: string): Promise<string | null> {
|
|
116
|
-
try {
|
|
117
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
118
|
-
encoding: "utf-8",
|
|
119
|
-
timeout: 3000,
|
|
120
|
-
stdio: ["pipe", "pipe", "ignore"], // 忽略 stderr
|
|
121
|
-
});
|
|
122
|
-
return result.trim();
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
132
|
/**
|
|
129
133
|
* 显示简单的更新通知(非交互式,不阻塞)
|
|
130
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("
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,61 +162,20 @@ describe("Update Notifier 模块测试", () => {
|
|
|
153
162
|
consoleSpy.mockRestore();
|
|
154
163
|
});
|
|
155
164
|
|
|
156
|
-
it("
|
|
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
|
-
// 每次运行都应该后台检查
|
|
171
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("后台检查应该更新缓存中的最新版本", async () => {
|
|
175
|
-
const mockCache = {
|
|
176
|
-
lastCheck: Date.now() - 2 * 60 * 60 * 1000,
|
|
177
|
-
latestVersion: "1.0.0",
|
|
178
|
-
checkedVersion: "1.0.0",
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
182
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
183
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
184
|
-
|
|
185
|
-
await checkForUpdates("1.0.0");
|
|
186
|
-
await vi.runAllTimersAsync();
|
|
187
|
-
|
|
188
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("有新版本时每次都应该后台检查", async () => {
|
|
165
|
+
it("每次运行都应该启动后台检查", async () => {
|
|
192
166
|
const mockCache = {
|
|
193
167
|
lastCheck: Date.now(), // 刚刚检查过
|
|
194
|
-
latestVersion: "1.0.
|
|
168
|
+
latestVersion: "1.0.0",
|
|
195
169
|
checkedVersion: "1.0.0",
|
|
196
170
|
};
|
|
197
171
|
|
|
198
172
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
199
173
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
200
|
-
vi.mocked(execSync).mockReturnValue("1.0.2" as any);
|
|
201
|
-
|
|
202
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
203
174
|
|
|
204
175
|
await checkForUpdates("1.0.0");
|
|
205
|
-
await vi.runAllTimersAsync();
|
|
206
|
-
|
|
207
|
-
// 即使刚检查过,有新版本时也应该继续检查
|
|
208
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
209
176
|
|
|
210
|
-
|
|
177
|
+
// 每次都应该启动子进程检查
|
|
178
|
+
expect(spawn).toHaveBeenCalled();
|
|
211
179
|
});
|
|
212
180
|
|
|
213
181
|
it("缓存文件损坏时应该静默处理", async () => {
|
|
@@ -310,7 +278,7 @@ describe("Update Notifier 模块测试", () => {
|
|
|
310
278
|
{ current: "1.0.0", latest: "1.0.1", shouldShow: true },
|
|
311
279
|
{ current: "1.0.0", latest: "1.1.0", shouldShow: true },
|
|
312
280
|
{ current: "1.0.0", latest: "2.0.0", shouldShow: true },
|
|
313
|
-
{ current: "1.0.1", latest: "1.0.0", shouldShow:
|
|
281
|
+
{ current: "1.0.1", latest: "1.0.0", shouldShow: false }, // 本地版本更高,不提示
|
|
314
282
|
{ current: "1.0.0", latest: "1.0.0", shouldShow: false },
|
|
315
283
|
];
|
|
316
284
|
|
|
@@ -343,23 +311,8 @@ describe("Update Notifier 模块测试", () => {
|
|
|
343
311
|
});
|
|
344
312
|
|
|
345
313
|
describe("缓存读写", () => {
|
|
346
|
-
it("应该正确写入缓存", async () => {
|
|
347
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
348
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
349
|
-
|
|
350
|
-
await checkForUpdates("1.0.0");
|
|
351
|
-
await vi.runAllTimersAsync();
|
|
352
|
-
|
|
353
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
354
|
-
"/home/user/.gw-update-check",
|
|
355
|
-
expect.stringContaining("1.0.1"),
|
|
356
|
-
"utf-8"
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
314
|
it("写入缓存失败时应该静默处理", async () => {
|
|
361
315
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
362
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
363
316
|
vi.mocked(writeFileSync).mockImplementation(() => {
|
|
364
317
|
throw new Error("Write failed");
|
|
365
318
|
});
|
|
@@ -380,24 +333,24 @@ describe("Update Notifier 模块测试", () => {
|
|
|
380
333
|
describe("网络请求", () => {
|
|
381
334
|
it("获取最新版本失败时应该静默处理", async () => {
|
|
382
335
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
383
|
-
vi.mocked(
|
|
384
|
-
throw new Error("
|
|
336
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
337
|
+
throw new Error("Spawn error");
|
|
385
338
|
});
|
|
386
339
|
|
|
387
340
|
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
388
341
|
});
|
|
389
342
|
|
|
390
|
-
it("
|
|
343
|
+
it("后台检查应该使用正确的参数", async () => {
|
|
391
344
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
392
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
393
345
|
|
|
394
346
|
await checkForUpdates("1.0.0", "@zjex/git-workflow");
|
|
395
|
-
await vi.runAllTimersAsync();
|
|
396
347
|
|
|
397
|
-
expect(
|
|
398
|
-
"
|
|
348
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
349
|
+
"node",
|
|
350
|
+
expect.arrayContaining(["-e", expect.stringContaining("npm view @zjex/git-workflow version")]),
|
|
399
351
|
expect.objectContaining({
|
|
400
|
-
|
|
352
|
+
detached: true,
|
|
353
|
+
stdio: "ignore",
|
|
401
354
|
})
|
|
402
355
|
);
|
|
403
356
|
});
|