@git-ai/cli 1.0.0
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 +369 -0
- package/bin/index.cjs +15 -0
- package/bin/index.mjs +41 -0
- package/package.json +46 -0
- package/src/actions/BaseAction.mjs +44 -0
- package/src/actions/BaseUrlAction.mjs +24 -0
- package/src/actions/CommitAction.mjs +484 -0
- package/src/actions/MaxTokenAction.mjs +38 -0
- package/src/actions/ModelAction.mjs +131 -0
- package/src/actions/SelectModelAction.mjs +161 -0
- package/src/actions/TokenAction.mjs +25 -0
- package/src/const.mjs +41 -0
- package/src/index.mjs +69 -0
- package/src/services/AIService.mjs +113 -0
- package/src/services/GitService.mjs +388 -0
- package/src/utils/ConflictUtils.mjs +39 -0
- package/src/utils/Log.mjs +146 -0
- package/src/utils/Logger.mjs +34 -0
- package/src/utils/MessageUtils.mjs +197 -0
- package/src/utils/OpenAI.mjs +67 -0
- package/src/utils/Spinner.mjs +39 -0
- package/src/utils/Storage.mjs +7 -0
- package/src/utils/Utils.mjs +72 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { collectGitExecLog } from "../utils/Log.mjs";
|
|
3
|
+
import Logger from "../utils/Logger.mjs";
|
|
4
|
+
|
|
5
|
+
const sanitizeExecOptions = (options = {}) => {
|
|
6
|
+
return Object.entries(options).reduce((acc, [key, value]) => {
|
|
7
|
+
if (typeof value === "function") {
|
|
8
|
+
acc[key] = `[Function ${value.name || "anonymous"}]`;
|
|
9
|
+
return acc;
|
|
10
|
+
}
|
|
11
|
+
if (value instanceof Buffer) {
|
|
12
|
+
acc[key] = `[Buffer length=${value.length}]`;
|
|
13
|
+
return acc;
|
|
14
|
+
}
|
|
15
|
+
acc[key] = value;
|
|
16
|
+
return acc;
|
|
17
|
+
}, {});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Git 操作服务类
|
|
22
|
+
* 封装所有 Git 相关操作
|
|
23
|
+
*/
|
|
24
|
+
export class GitService {
|
|
25
|
+
constructor() {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 执行 Git 命令
|
|
29
|
+
*/
|
|
30
|
+
exec(command, options = {}) {
|
|
31
|
+
const execOptions = {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
34
|
+
...options,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = execSync(command, execOptions);
|
|
39
|
+
collectGitExecLog({
|
|
40
|
+
command,
|
|
41
|
+
options: sanitizeExecOptions(execOptions),
|
|
42
|
+
status: "success",
|
|
43
|
+
output: typeof result === "string" ? result : "",
|
|
44
|
+
});
|
|
45
|
+
return result;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
collectGitExecLog({
|
|
48
|
+
command,
|
|
49
|
+
options: sanitizeExecOptions(execOptions),
|
|
50
|
+
status: "error",
|
|
51
|
+
error: error && error.message ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 检查是否安装 Git
|
|
59
|
+
*/
|
|
60
|
+
checkInstalled() {
|
|
61
|
+
try {
|
|
62
|
+
const hasGit = this.exec("git --version").toString().trim();
|
|
63
|
+
if (!hasGit) {
|
|
64
|
+
throw "当前没有安装 git,请先安装 git...";
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw error && error.message ? error.message : error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 检查是否为 Git 仓库
|
|
73
|
+
*/
|
|
74
|
+
checkRepository() {
|
|
75
|
+
try {
|
|
76
|
+
this.exec("git rev-parse --is-inside-work-tree");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw `当前目录不是 git 仓库,请先初始化 git 仓库...\n${
|
|
79
|
+
error && error.message ? error.message : error
|
|
80
|
+
}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取工作目录前缀
|
|
86
|
+
*/
|
|
87
|
+
getWorkingPrefix() {
|
|
88
|
+
try {
|
|
89
|
+
const workingPrefix =
|
|
90
|
+
this.exec("git rev-parse --show-prefix", {
|
|
91
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
92
|
+
}) || "";
|
|
93
|
+
return workingPrefix.trim();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
Logger.warn(`${error && error.message ? error.message : error}`);
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 执行 git add
|
|
102
|
+
*/
|
|
103
|
+
add() {
|
|
104
|
+
this.exec("git add .", { stdio: ["pipe", "pipe", "ignore"] });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取用户名
|
|
109
|
+
*/
|
|
110
|
+
getUserName() {
|
|
111
|
+
return this.exec("git config user.name").toString().trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取差异字符串
|
|
116
|
+
*/
|
|
117
|
+
getDiff(maxToken) {
|
|
118
|
+
try {
|
|
119
|
+
let diffString = this.exec("git diff --staged -U0", {
|
|
120
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
121
|
+
}).toString();
|
|
122
|
+
|
|
123
|
+
if (!diffString || diffString.length <= maxToken) {
|
|
124
|
+
return diffString.trim();
|
|
125
|
+
}
|
|
126
|
+
return this.exec("git diff --staged --stat", {
|
|
127
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
128
|
+
})
|
|
129
|
+
.toString()
|
|
130
|
+
.trim();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return this.exec("git diff --staged --stat", {
|
|
133
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
134
|
+
})
|
|
135
|
+
.toString()
|
|
136
|
+
.trim();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取 Git 状态
|
|
142
|
+
*/
|
|
143
|
+
getStatus() {
|
|
144
|
+
return this.exec("git status --porcelain");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 执行 commit
|
|
149
|
+
*/
|
|
150
|
+
async commit(commitMessage) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
try {
|
|
153
|
+
execSync(commitMessage, {
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
156
|
+
});
|
|
157
|
+
resolve();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const stdout = this.normalizeExecOutput(error && error.stdout);
|
|
160
|
+
const stderr = this.normalizeExecOutput(error && error.stderr);
|
|
161
|
+
const messageParts = [
|
|
162
|
+
error && error.message ? error.message : "",
|
|
163
|
+
stderr,
|
|
164
|
+
stdout,
|
|
165
|
+
].filter(Boolean);
|
|
166
|
+
error.stdout = stdout;
|
|
167
|
+
error.stderr = stderr;
|
|
168
|
+
error.message = messageParts.join("\n").trim() || "git commit 失败";
|
|
169
|
+
reject(error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
normalizeExecOutput(output) {
|
|
175
|
+
if (!output) {
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
if (typeof output === "string") {
|
|
179
|
+
return output.trim();
|
|
180
|
+
}
|
|
181
|
+
if (Buffer.isBuffer(output)) {
|
|
182
|
+
return output.toString("utf8").trim();
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 重置暂存区
|
|
189
|
+
*/
|
|
190
|
+
reset() {
|
|
191
|
+
this.exec("git reset", { stdio: ["pipe", "pipe", "ignore"] });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 获取当前分支名
|
|
196
|
+
*/
|
|
197
|
+
getCurrentBranch() {
|
|
198
|
+
return this.exec("git rev-parse --abbrev-ref HEAD").trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 获取远程仓库名称
|
|
203
|
+
*/
|
|
204
|
+
async getRemoteName(currentBranch) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
let remoteName = "";
|
|
207
|
+
|
|
208
|
+
// 优先从当前分支配置中读取 remote
|
|
209
|
+
try {
|
|
210
|
+
remoteName = this.exec(
|
|
211
|
+
`git config --get branch.${currentBranch}.remote`,
|
|
212
|
+
{
|
|
213
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
214
|
+
}
|
|
215
|
+
).trim();
|
|
216
|
+
} catch {}
|
|
217
|
+
|
|
218
|
+
if (!remoteName) {
|
|
219
|
+
try {
|
|
220
|
+
const upstream = this.exec(
|
|
221
|
+
"git rev-parse --abbrev-ref --symbolic-full-name @{u}",
|
|
222
|
+
{
|
|
223
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
224
|
+
}
|
|
225
|
+
).trim();
|
|
226
|
+
if (upstream && upstream.includes("/")) {
|
|
227
|
+
remoteName = upstream.split("/")[0];
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 回退:选择已存在的 remote(优先 origin)
|
|
233
|
+
if (!remoteName) {
|
|
234
|
+
try {
|
|
235
|
+
const remotesOutput = this.exec("git remote", {
|
|
236
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
237
|
+
}).trim();
|
|
238
|
+
const remotes = remotesOutput
|
|
239
|
+
? remotesOutput.split("\n").filter(Boolean)
|
|
240
|
+
: [];
|
|
241
|
+
if (remotes.includes("origin")) {
|
|
242
|
+
remoteName = "origin";
|
|
243
|
+
} else if (remotes.length > 0) {
|
|
244
|
+
remoteName = remotes[0];
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
reject(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!remoteName) {
|
|
252
|
+
reject("未找到任何远程仓库配置");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// 验证 remote 是否存在
|
|
257
|
+
this.exec(`git remote show ${remoteName}`, {
|
|
258
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
259
|
+
});
|
|
260
|
+
// 返回 remote
|
|
261
|
+
resolve(remoteName);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
reject(error);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 检查是否处于合并状态
|
|
270
|
+
*/
|
|
271
|
+
isMerging() {
|
|
272
|
+
try {
|
|
273
|
+
this.exec("git rev-parse --verify --quiet MERGE_HEAD", {
|
|
274
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
275
|
+
});
|
|
276
|
+
return true;
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 完成合并
|
|
284
|
+
*/
|
|
285
|
+
finishMerge() {
|
|
286
|
+
this.exec("git commit --no-edit", {
|
|
287
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Fetch 远程分支
|
|
293
|
+
*/
|
|
294
|
+
fetch(remoteName) {
|
|
295
|
+
this.exec(`git fetch ${remoteName}`, {
|
|
296
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 合并远程分支
|
|
302
|
+
*/
|
|
303
|
+
merge(remoteName, currentBranch) {
|
|
304
|
+
this.exec(`git merge ${remoteName}/${currentBranch}`, {
|
|
305
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 获取本地领先远程的提交数
|
|
311
|
+
*/
|
|
312
|
+
getAheadCount(remoteName, currentBranch) {
|
|
313
|
+
const aheadOutput = this.exec(
|
|
314
|
+
`git log --oneline HEAD..${remoteName}/${currentBranch}`,
|
|
315
|
+
{
|
|
316
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
317
|
+
}
|
|
318
|
+
).trim();
|
|
319
|
+
return aheadOutput
|
|
320
|
+
? aheadOutput.split("\n").filter((line) => line.trim()).length
|
|
321
|
+
: 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 获取需要推送的提交数
|
|
326
|
+
*/
|
|
327
|
+
getPushCount(remoteName, currentBranch) {
|
|
328
|
+
// 先验证远程分支是否存在
|
|
329
|
+
try {
|
|
330
|
+
this.exec(
|
|
331
|
+
`git rev-parse --verify --quiet ${remoteName}/${currentBranch}`,
|
|
332
|
+
{
|
|
333
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
} catch {
|
|
337
|
+
return -1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const pushOutput = this.exec(
|
|
342
|
+
`git log --oneline ${remoteName}/${currentBranch}..HEAD`,
|
|
343
|
+
{
|
|
344
|
+
encoding: "utf8",
|
|
345
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
346
|
+
}
|
|
347
|
+
).trim();
|
|
348
|
+
return pushOutput
|
|
349
|
+
? pushOutput.split("\n").filter((line) => line.trim()).length
|
|
350
|
+
: 0;
|
|
351
|
+
} catch {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 推送到远程
|
|
358
|
+
*/
|
|
359
|
+
push(remoteName, currentBranch) {
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
try {
|
|
362
|
+
this.exec(`git push -u ${remoteName} ${currentBranch}`, {
|
|
363
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
364
|
+
});
|
|
365
|
+
resolve();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
reject(error);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 检查远程分支是否存在
|
|
374
|
+
*/
|
|
375
|
+
remoteHasBranch(remoteName, currentBranch) {
|
|
376
|
+
try {
|
|
377
|
+
this.exec(
|
|
378
|
+
`git rev-parse --verify --quiet ${remoteName}/${currentBranch}`,
|
|
379
|
+
{
|
|
380
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
return true;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 查找冲突文件
|
|
5
|
+
*/
|
|
6
|
+
export const findConflictFiles = (paths, workingPrefix = "") => {
|
|
7
|
+
const conflictFiles = [];
|
|
8
|
+
const ignoreFiles = [];
|
|
9
|
+
const pathsCopy = [...paths];
|
|
10
|
+
|
|
11
|
+
while (pathsCopy.length) {
|
|
12
|
+
const p = pathsCopy.shift();
|
|
13
|
+
|
|
14
|
+
// 如果有 workingPrefix,检查文件是否在当前工作目录下
|
|
15
|
+
// p 是相对于 Git 根目录的路径,workingPrefix 也是相对于 Git 根目录的路径
|
|
16
|
+
if (workingPrefix && !p.startsWith(workingPrefix)) {
|
|
17
|
+
ignoreFiles.push(p);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(p, "utf8");
|
|
23
|
+
// 检查是否有 git 冲突标记
|
|
24
|
+
// 仅当冲突标记出现在行首时才判定为真实冲突,避免代码字符串中的误判
|
|
25
|
+
const hasStartMarker = /^<<<<<<< [^\n\r]*/m.test(content);
|
|
26
|
+
const hasMidMarker = /^=======$/m.test(content);
|
|
27
|
+
const hasEndMarker = /^>>>>>>> [^\n\r]*/m.test(content);
|
|
28
|
+
|
|
29
|
+
if (hasStartMarker && hasMidMarker && hasEndMarker) {
|
|
30
|
+
conflictFiles.push(p);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// 文件读取失败,可能是被忽略的文件
|
|
34
|
+
ignoreFiles.push(p);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { conflictFiles, ignoreFiles };
|
|
39
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { NAME } from "../const.mjs";
|
|
5
|
+
import npmlog from "npmlog";
|
|
6
|
+
|
|
7
|
+
// 日志收集器
|
|
8
|
+
const logCollector = [];
|
|
9
|
+
const MAX_DETAIL_LENGTH = 2000;
|
|
10
|
+
|
|
11
|
+
const truncate = (content = "") => {
|
|
12
|
+
if (typeof content !== "string") {
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
return content.length > MAX_DETAIL_LENGTH
|
|
16
|
+
? `${content.slice(0, MAX_DETAIL_LENGTH)}\n...[truncated]`
|
|
17
|
+
: content;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pushLog = (entry) => {
|
|
21
|
+
logCollector.push({
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
...entry,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function collectError(err, context = "") {
|
|
28
|
+
const errorMessage = (err && (err.stack || err.message)) || String(err);
|
|
29
|
+
pushLog({
|
|
30
|
+
source: "error",
|
|
31
|
+
level: "error",
|
|
32
|
+
context,
|
|
33
|
+
message: truncate(errorMessage),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function collectWarning(message = "", context = "") {
|
|
38
|
+
pushLog({
|
|
39
|
+
source: "warning",
|
|
40
|
+
level: "warning",
|
|
41
|
+
context,
|
|
42
|
+
message: truncate(message),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function collectGitExecLog(entry = {}) {
|
|
47
|
+
const {
|
|
48
|
+
command = "",
|
|
49
|
+
options = {},
|
|
50
|
+
status = "success",
|
|
51
|
+
output = "",
|
|
52
|
+
error = "",
|
|
53
|
+
} = entry;
|
|
54
|
+
pushLog({
|
|
55
|
+
source: "git-exec",
|
|
56
|
+
level: status,
|
|
57
|
+
command,
|
|
58
|
+
options,
|
|
59
|
+
message: truncate(output),
|
|
60
|
+
error: truncate(error),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function collectLoggerLog(level, message) {
|
|
65
|
+
const payload = Array.isArray(message) ? message : [message];
|
|
66
|
+
pushLog({
|
|
67
|
+
source: "logger",
|
|
68
|
+
level,
|
|
69
|
+
message: truncate(payload.filter(Boolean).join(" ")),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function collectSpinnerState(state, text = "") {
|
|
74
|
+
pushLog({
|
|
75
|
+
source: "spinner",
|
|
76
|
+
level: state,
|
|
77
|
+
message: truncate(text),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeLogFile() {
|
|
82
|
+
if (logCollector.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const now = new Date();
|
|
87
|
+
const year = now.getFullYear();
|
|
88
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
89
|
+
const logDir = path.join(
|
|
90
|
+
os.homedir(),
|
|
91
|
+
".config",
|
|
92
|
+
NAME.replace("@", "/"),
|
|
93
|
+
"logs",
|
|
94
|
+
`${year}-${month}`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(logDir)) {
|
|
98
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
102
|
+
const hour = String(now.getHours()).padStart(2, "0");
|
|
103
|
+
const minute = String(now.getMinutes()).padStart(2, "0");
|
|
104
|
+
const second = String(now.getSeconds()).padStart(2, "0");
|
|
105
|
+
const dateStr = `${year}-${month}-${day}_${hour}-${minute}-${second}`;
|
|
106
|
+
const logFile = path.join(logDir, `log-${dateStr}.txt`);
|
|
107
|
+
|
|
108
|
+
let logContent = `日志收集器\n生成时间: ${now.toLocaleString("zh-CN")}\n`;
|
|
109
|
+
logContent += `${"=".repeat(80)}\n\n`;
|
|
110
|
+
|
|
111
|
+
logCollector.forEach((item, index) => {
|
|
112
|
+
logContent += `[日志 ${index + 1}]\n`;
|
|
113
|
+
logContent += `时间: ${item.timestamp}\n`;
|
|
114
|
+
logContent += `来源: ${item.source}\n`;
|
|
115
|
+
if (item.level) {
|
|
116
|
+
logContent += `级别: ${item.level}\n`;
|
|
117
|
+
}
|
|
118
|
+
if (item.context) {
|
|
119
|
+
logContent += `上下文: ${item.context}\n`;
|
|
120
|
+
}
|
|
121
|
+
if (item.command) {
|
|
122
|
+
logContent += `命令: ${item.command}\n`;
|
|
123
|
+
}
|
|
124
|
+
if (item.options && Object.keys(item.options).length) {
|
|
125
|
+
logContent += `参数: ${JSON.stringify(item.options)}\n`;
|
|
126
|
+
}
|
|
127
|
+
if (item.message) {
|
|
128
|
+
logContent += `内容:\n${item.message}\n`;
|
|
129
|
+
}
|
|
130
|
+
if (item.error) {
|
|
131
|
+
logContent += `错误:\n${item.error}\n`;
|
|
132
|
+
}
|
|
133
|
+
logContent += `${"-".repeat(80)}\n\n`;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
fs.writeFileSync(logFile, logContent, "utf8");
|
|
137
|
+
|
|
138
|
+
const errorCount = logCollector.filter(
|
|
139
|
+
(item) => item.level === "error"
|
|
140
|
+
).length;
|
|
141
|
+
if (errorCount > 0) {
|
|
142
|
+
npmlog.verbose(`日志地址: ${logFile}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return logFile;
|
|
146
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import npmlog from "npmlog";
|
|
2
|
+
import { BIN } from "../const.mjs";
|
|
3
|
+
import { collectLoggerLog } from "./Log.mjs";
|
|
4
|
+
|
|
5
|
+
npmlog.level = "info";
|
|
6
|
+
npmlog.heading = BIN.replace(" ", "-"); // 修改前缀
|
|
7
|
+
npmlog.headingStyle = { fg: "red", bg: "black" }; // 修改前缀样式
|
|
8
|
+
npmlog.addLevel("success", 2000, { fg: "green", bold: true }); // 添加自定义命令
|
|
9
|
+
npmlog.addLevel("verbose", 2000, { fg: "blue", bg: "black" }, "verb"); // 添加自定义命令,Number 数字1000会无法显示,建议2000
|
|
10
|
+
|
|
11
|
+
const levelsToWrap = [
|
|
12
|
+
"info",
|
|
13
|
+
"warn",
|
|
14
|
+
"error",
|
|
15
|
+
"success",
|
|
16
|
+
"verb",
|
|
17
|
+
"verbose",
|
|
18
|
+
"silly",
|
|
19
|
+
"http",
|
|
20
|
+
"notice",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
levelsToWrap.forEach((level) => {
|
|
24
|
+
if (typeof npmlog[level] !== "function") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const original = npmlog[level].bind(npmlog);
|
|
28
|
+
npmlog[level] = (...args) => {
|
|
29
|
+
collectLoggerLog(level, args);
|
|
30
|
+
return original(...args);
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default npmlog;
|