@aipanel/core 1.2.26 → 1.2.28
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/es/node/diagnostics.d.ts +26 -2
- package/es/node/diagnostics.mjs +134 -81
- package/es/node/node-utils.mjs +12 -33
- package/lib/node/diagnostics.cjs +137 -81
- package/lib/node/diagnostics.d.ts +26 -2
- package/lib/node/node-utils.cjs +12 -33
- package/package.json +2 -1
package/es/node/diagnostics.d.ts
CHANGED
|
@@ -51,8 +51,20 @@ export declare function findTsconfigDir(filePath: string): string | null;
|
|
|
51
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
52
52
|
/** 运行 TypeScript 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
|
|
53
53
|
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只跑一次 `--build --noEmit`,
|
|
56
|
+
* 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
|
|
57
|
+
* 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
|
|
58
|
+
*/
|
|
59
|
+
export declare function runTypeChecksForFiles(files: string[], cwd: string): Promise<Map<string, TscResult>>;
|
|
54
60
|
/** 并行运行 ESLint + 类型检查(单文件或 glob) */
|
|
55
61
|
export declare function runAllChecks(pattern: string, cwd: string): Promise<DiagnosticsResult>;
|
|
62
|
+
/**
|
|
63
|
+
* 批量运行 ESLint + 类型检查:Lint 逐文件(ESLint 进程内、oxlint 单次 CLI),
|
|
64
|
+
* 类型检查按 tsconfig 项目合并为一次(见 runTypeChecksForFiles)。
|
|
65
|
+
* 返回"文件绝对路径 → 诊断结果",供 step 边界的一次性收尾诊断使用。
|
|
66
|
+
*/
|
|
67
|
+
export declare function runAllChecksForFiles(files: string[], cwd: string): Promise<Map<string, DiagnosticsResult>>;
|
|
56
68
|
/**
|
|
57
69
|
* 全量项目诊断:优先从根 tsconfig 运行一次类型检查 --build,
|
|
58
70
|
* 根无 tsconfig 时回退到逐个子目录 build;ESLint 以 "." 全量扫描。
|
|
@@ -62,5 +74,17 @@ export declare function runProjectDiagnostics(workspace: string): Promise<Diagno
|
|
|
62
74
|
export declare function tscSectionTitle(tscOutput: TscResult): string;
|
|
63
75
|
/** Lint 分区标题(单一来源:跟随实际运行的引擎组合,如 "ESLint + oxlint") */
|
|
64
76
|
export declare function lintSectionTitle(lintOutput: EslintOutput): string;
|
|
65
|
-
/**
|
|
66
|
-
export declare function
|
|
77
|
+
/** 分区发现被条数上限折叠时的提示(自动诊断摘要与 run_diagnostics 完整输出的分界) */
|
|
78
|
+
export declare function omittedFindingsHint(omitted: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* 组装统一的分区诊断文本(Lint / 类型检查)。
|
|
81
|
+
* 默认保留空分区的占位文案(run_diagnostics 要正面回答"有没有问题");
|
|
82
|
+
* `onlyFindings: true` 只输出有发现的分区、全空返回空串(编辑后自动诊断不刷占位噪音);
|
|
83
|
+
* `maxFindingsPerSection` 把每个分区的发现折叠成有界摘要——编辑后自动诊断每个 step 都会投递,
|
|
84
|
+
* 因此用条数上限控制重复成本,完整结果仍由不带上限的 run_diagnostics 给出。
|
|
85
|
+
* `title` 允许为空串,此时直接返回分区正文(追加式投递场景无需标题)。
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatDiagnosticsSections(title: string, eslintOutput: EslintOutput, tscOutput: TscResult, options?: {
|
|
88
|
+
onlyFindings?: boolean;
|
|
89
|
+
maxFindingsPerSection?: number;
|
|
90
|
+
}): string;
|
package/es/node/diagnostics.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { execa } from "execa";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { SEVERITY_ERROR, SEVERITY_WARN } from "../common/constants.mjs";
|
|
6
6
|
import { createLogger } from "./node-logger.mjs";
|
|
@@ -189,41 +189,35 @@ function parseOxlintOutput(stdout, cwd) {
|
|
|
189
189
|
async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
190
190
|
const bin = resolveOxlintBin(cwd);
|
|
191
191
|
if (!bin) return {};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
engines: ["oxlint"]
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
);
|
|
226
|
-
});
|
|
192
|
+
const result = await execa(
|
|
193
|
+
"node",
|
|
194
|
+
[bin, "--format=json", "--ignore-pattern", "node_modules", pattern],
|
|
195
|
+
{ cwd, timeout: 6e4, maxBuffer: 50 * 1024 * 1024, reject: false }
|
|
196
|
+
);
|
|
197
|
+
if (result.timedOut || result.isTerminated || result.isMaxBuffer) {
|
|
198
|
+
log.warn("oxlint timed out", { pattern });
|
|
199
|
+
return {
|
|
200
|
+
text: "[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A\u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u3002",
|
|
201
|
+
diagnostics: [],
|
|
202
|
+
engines: ["oxlint"]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const messages = parseOxlintOutput(result.stdout, cwd);
|
|
207
|
+
log.debug("oxlint lint", {
|
|
208
|
+
pattern,
|
|
209
|
+
messageCount: messages.length,
|
|
210
|
+
stderr: result.stderr || void 0
|
|
211
|
+
});
|
|
212
|
+
return formatLintMessages(messages, { label: "oxlint", source: "oxlint" }, warnLimit);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
log.warn("oxlint failed", { pattern, error: e.message });
|
|
215
|
+
return {
|
|
216
|
+
text: `[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A${e.message}`,
|
|
217
|
+
diagnostics: [],
|
|
218
|
+
engines: ["oxlint"]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
227
221
|
}
|
|
228
222
|
let _vueTscBin;
|
|
229
223
|
function resolveVueTscBin() {
|
|
@@ -348,6 +342,22 @@ function parseTscDiags(rawOutput, filePath, projectDir, source = "tsc") {
|
|
|
348
342
|
}
|
|
349
343
|
return diags;
|
|
350
344
|
}
|
|
345
|
+
function filterTscOutputForFile(rawOutput, filePath, projectDir) {
|
|
346
|
+
const resolved = path.resolve(filePath);
|
|
347
|
+
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
348
|
+
const filtered = [];
|
|
349
|
+
let keep = false;
|
|
350
|
+
for (const line of rawOutput.split("\n")) {
|
|
351
|
+
const m = errorLinePat.exec(line);
|
|
352
|
+
if (m) {
|
|
353
|
+
keep = path.resolve(projectDir, m[1]) === resolved;
|
|
354
|
+
} else if (!/^\s/.test(line)) {
|
|
355
|
+
keep = false;
|
|
356
|
+
}
|
|
357
|
+
if (keep) filtered.push(line);
|
|
358
|
+
}
|
|
359
|
+
return filtered.join("\n");
|
|
360
|
+
}
|
|
351
361
|
async function runTypeCheck(filePath, cwd) {
|
|
352
362
|
const dir = cwd;
|
|
353
363
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
@@ -364,45 +374,55 @@ async function runTypeCheck(filePath, cwd) {
|
|
|
364
374
|
}
|
|
365
375
|
const timeout = filePath ? 6e4 : 12e4;
|
|
366
376
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
let rawOutput = stdout + stderr;
|
|
373
|
-
const killed = error?.killed;
|
|
374
|
-
const exitCode = typeof error?.code === "number" ? error.code : killed ? 1 : 0;
|
|
375
|
-
if (killed && !rawOutput) {
|
|
376
|
-
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
377
|
-
}
|
|
378
|
-
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
379
|
-
if (filePath) {
|
|
380
|
-
const resolved = path.resolve(filePath);
|
|
381
|
-
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
382
|
-
const lines = rawOutput.split("\n");
|
|
383
|
-
const filtered = [];
|
|
384
|
-
let keep = false;
|
|
385
|
-
for (const line of lines) {
|
|
386
|
-
const m = errorLinePat.exec(line);
|
|
387
|
-
if (m) {
|
|
388
|
-
keep = path.resolve(projectDir, m[1]) === resolved;
|
|
389
|
-
} else if (!/^\s/.test(line)) {
|
|
390
|
-
keep = false;
|
|
391
|
-
}
|
|
392
|
-
if (keep) filtered.push(line);
|
|
393
|
-
}
|
|
394
|
-
rawOutput = filtered.join("\n");
|
|
395
|
-
}
|
|
396
|
-
log.debug("type-check finished", {
|
|
397
|
-
engine: engine.source,
|
|
398
|
-
filePath: filePath || "(all)",
|
|
399
|
-
exitCode,
|
|
400
|
-
outputLength: rawOutput.length
|
|
401
|
-
});
|
|
402
|
-
resolve({ rawOutput, exitCode, diagnostics, source: engine.source });
|
|
403
|
-
}
|
|
404
|
-
);
|
|
377
|
+
const result = await execa("node", [engine.bin, "--build", "--noEmit", "--pretty", "false"], {
|
|
378
|
+
cwd: projectDir,
|
|
379
|
+
timeout,
|
|
380
|
+
maxBuffer,
|
|
381
|
+
reject: false
|
|
405
382
|
});
|
|
383
|
+
let rawOutput = result.stdout + result.stderr;
|
|
384
|
+
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
385
|
+
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
386
|
+
if (killed && !rawOutput) {
|
|
387
|
+
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
388
|
+
}
|
|
389
|
+
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
390
|
+
if (filePath) {
|
|
391
|
+
rawOutput = filterTscOutputForFile(rawOutput, filePath, projectDir);
|
|
392
|
+
}
|
|
393
|
+
log.debug("type-check finished", {
|
|
394
|
+
engine: engine.source,
|
|
395
|
+
filePath: filePath || "(all)",
|
|
396
|
+
exitCode,
|
|
397
|
+
outputLength: rawOutput.length
|
|
398
|
+
});
|
|
399
|
+
return { rawOutput, exitCode, diagnostics, source: engine.source };
|
|
400
|
+
}
|
|
401
|
+
async function runTypeChecksForFiles(files, cwd) {
|
|
402
|
+
const groups = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const file of files) {
|
|
404
|
+
const resolved = path.resolve(file);
|
|
405
|
+
const projectDir = findTsconfigDir(resolved) ?? cwd;
|
|
406
|
+
const group = groups.get(projectDir);
|
|
407
|
+
if (group) group.push(resolved);
|
|
408
|
+
else groups.set(projectDir, [resolved]);
|
|
409
|
+
}
|
|
410
|
+
const results = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const [projectDir, group] of groups) {
|
|
412
|
+
const whole = await runTypeCheck(void 0, projectDir).catch(() => ({
|
|
413
|
+
rawOutput: "",
|
|
414
|
+
exitCode: 0
|
|
415
|
+
}));
|
|
416
|
+
for (const file of group) {
|
|
417
|
+
results.set(file, {
|
|
418
|
+
rawOutput: filterTscOutputForFile(whole.rawOutput, file, projectDir),
|
|
419
|
+
exitCode: whole.exitCode,
|
|
420
|
+
diagnostics: (whole.diagnostics ?? []).filter((d) => d.file === file),
|
|
421
|
+
source: whole.source
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return results;
|
|
406
426
|
}
|
|
407
427
|
async function runAllChecks(pattern, cwd) {
|
|
408
428
|
log.debug("runAllChecks", { pattern, cwd });
|
|
@@ -412,6 +432,18 @@ async function runAllChecks(pattern, cwd) {
|
|
|
412
432
|
]);
|
|
413
433
|
return { eslintOutput, tscOutput };
|
|
414
434
|
}
|
|
435
|
+
async function runAllChecksForFiles(files, cwd) {
|
|
436
|
+
const targets = [...new Set(files.map((file) => path.resolve(file)))];
|
|
437
|
+
const tscByFile = await runTypeChecksForFiles(targets, cwd);
|
|
438
|
+
const results = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const file of targets) {
|
|
440
|
+
results.set(file, {
|
|
441
|
+
eslintOutput: await lintFiles(file, cwd),
|
|
442
|
+
tscOutput: tscByFile.get(file) ?? { rawOutput: "", exitCode: 0 }
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return results;
|
|
446
|
+
}
|
|
415
447
|
async function runProjectDiagnostics(workspace) {
|
|
416
448
|
const tscDirs = fs.existsSync(path.join(workspace, "tsconfig.json")) ? [workspace] : findAllTsconfigDirs(workspace);
|
|
417
449
|
log.debug("Tsc dirs to check", { count: tscDirs.length, dirs: tscDirs });
|
|
@@ -433,18 +465,36 @@ function tscSectionTitle(tscOutput) {
|
|
|
433
465
|
function lintSectionTitle(lintOutput) {
|
|
434
466
|
return lintOutput.engines?.length ? lintOutput.engines.join(" + ") : "ESLint";
|
|
435
467
|
}
|
|
436
|
-
function
|
|
468
|
+
function omittedFindingsHint(omitted) {
|
|
469
|
+
return `\u2026\u8FD8\u6709 ${omitted} \u6761\uFF0C\u5B8C\u6574\u7ED3\u679C\u8BF7\u8C03\u7528 run_diagnostics`;
|
|
470
|
+
}
|
|
471
|
+
function boundFindings(text, maxFindings) {
|
|
472
|
+
if (!maxFindings || !text) return text;
|
|
473
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
474
|
+
if (lines.length <= maxFindings) return text;
|
|
475
|
+
return [...lines.slice(0, maxFindings), omittedFindingsHint(lines.length - maxFindings)].join(
|
|
476
|
+
"\n"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
function formatDiagnosticsSections(title, eslintOutput, tscOutput, options = {}) {
|
|
480
|
+
const lintLines = boundFindings(eslintOutput.text, options.maxFindingsPerSection);
|
|
481
|
+
const tscLines = boundFindings(tscOutput.rawOutput.trim(), options.maxFindingsPerSection);
|
|
437
482
|
const parts = [];
|
|
438
|
-
|
|
483
|
+
if (!options.onlyFindings || lintLines) {
|
|
484
|
+
parts.push(`## ${lintSectionTitle(eslintOutput)}
|
|
439
485
|
|
|
440
|
-
` + (
|
|
441
|
-
|
|
442
|
-
|
|
486
|
+
` + (lintLines || "\u6CA1\u6709\u53D1\u73B0\u95EE\u9898"));
|
|
487
|
+
}
|
|
488
|
+
if (!options.onlyFindings || tscLines) {
|
|
489
|
+
parts.push(`## ${tscSectionTitle(tscOutput)}
|
|
443
490
|
|
|
444
491
|
` + (tscLines || "\u6CA1\u6709\u53D1\u73B0\u7C7B\u578B\u9519\u8BEF"));
|
|
445
|
-
|
|
492
|
+
}
|
|
493
|
+
if (parts.length === 0) return "";
|
|
494
|
+
const body = parts.join("\n\n");
|
|
495
|
+
return title ? `${title}
|
|
446
496
|
|
|
447
|
-
`
|
|
497
|
+
${body}` : body;
|
|
448
498
|
}
|
|
449
499
|
export {
|
|
450
500
|
DIAGNOSTICS_TOOL_DESCRIPTION,
|
|
@@ -454,8 +504,11 @@ export {
|
|
|
454
504
|
isJsFile,
|
|
455
505
|
lintFiles,
|
|
456
506
|
lintSectionTitle,
|
|
507
|
+
omittedFindingsHint,
|
|
457
508
|
runAllChecks,
|
|
509
|
+
runAllChecksForFiles,
|
|
458
510
|
runProjectDiagnostics,
|
|
459
511
|
runTypeCheck,
|
|
512
|
+
runTypeChecksForFiles,
|
|
460
513
|
tscSectionTitle
|
|
461
514
|
};
|
package/es/node/node-utils.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { execa } from "execa";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import http from "node:http";
|
|
@@ -124,40 +125,18 @@ function findGitRoot(startDir, maxDepth = 10) {
|
|
|
124
125
|
}
|
|
125
126
|
async function checkCliInstalled(bin) {
|
|
126
127
|
const timer = new PerformanceTimer(`checkCliInstalled:${bin}`);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
proc.on("error", (err) => {
|
|
135
|
-
log.debug(`Failed to check ${bin} installation`, { error: err.message });
|
|
136
|
-
timer.end("\u2716 Check failed");
|
|
137
|
-
resolve(false);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
128
|
+
const result = await execa(bin, ["--version"], { reject: false, stdio: "ignore" });
|
|
129
|
+
const installed = result.exitCode === 0;
|
|
130
|
+
if (!installed && result.exitCode === void 0) {
|
|
131
|
+
log.debug(`Failed to check ${bin} installation`, { error: result.message });
|
|
132
|
+
}
|
|
133
|
+
timer.end(installed ? `\u2713 ${bin} is installed` : `\u2716 ${bin} not found`);
|
|
134
|
+
return installed;
|
|
140
135
|
}
|
|
141
|
-
function getCliVersion(bin) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let stderr = "";
|
|
146
|
-
proc.stdout?.on("data", (data) => {
|
|
147
|
-
stdout += data.toString();
|
|
148
|
-
});
|
|
149
|
-
proc.stderr?.on("data", (data) => {
|
|
150
|
-
stderr += data.toString();
|
|
151
|
-
});
|
|
152
|
-
proc.on("close", (code) => {
|
|
153
|
-
if (code !== 0) {
|
|
154
|
-
resolve(null);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
resolve(stdout.trim() || stderr.trim() || null);
|
|
158
|
-
});
|
|
159
|
-
proc.on("error", () => resolve(null));
|
|
160
|
-
});
|
|
136
|
+
async function getCliVersion(bin) {
|
|
137
|
+
const result = await execa(bin, ["--version"], { reject: false });
|
|
138
|
+
if (result.exitCode !== 0) return null;
|
|
139
|
+
return result.stdout.trim() || result.stderr.trim() || null;
|
|
161
140
|
}
|
|
162
141
|
function killOrphanCliProcesses(bin, options) {
|
|
163
142
|
const label = options.label ?? bin;
|
package/lib/node/diagnostics.cjs
CHANGED
|
@@ -34,15 +34,18 @@ __export(diagnostics_exports, {
|
|
|
34
34
|
isJsFile: () => isJsFile,
|
|
35
35
|
lintFiles: () => lintFiles,
|
|
36
36
|
lintSectionTitle: () => lintSectionTitle,
|
|
37
|
+
omittedFindingsHint: () => omittedFindingsHint,
|
|
37
38
|
runAllChecks: () => runAllChecks,
|
|
39
|
+
runAllChecksForFiles: () => runAllChecksForFiles,
|
|
38
40
|
runProjectDiagnostics: () => runProjectDiagnostics,
|
|
39
41
|
runTypeCheck: () => runTypeCheck,
|
|
42
|
+
runTypeChecksForFiles: () => runTypeChecksForFiles,
|
|
40
43
|
tscSectionTitle: () => tscSectionTitle
|
|
41
44
|
});
|
|
42
45
|
module.exports = __toCommonJS(diagnostics_exports);
|
|
43
46
|
var import_node_fs = __toESM(require("node:fs"));
|
|
44
47
|
var import_node_path = __toESM(require("node:path"));
|
|
45
|
-
var
|
|
48
|
+
var import_execa = require("execa");
|
|
46
49
|
var import_node_module = require("node:module");
|
|
47
50
|
var import_constants = require("../common/constants.cjs");
|
|
48
51
|
var import_node_logger = require("./node-logger.cjs");
|
|
@@ -232,41 +235,35 @@ function parseOxlintOutput(stdout, cwd) {
|
|
|
232
235
|
async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
233
236
|
const bin = resolveOxlintBin(cwd);
|
|
234
237
|
if (!bin) return {};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
engines: ["oxlint"]
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
});
|
|
238
|
+
const result = await (0, import_execa.execa)(
|
|
239
|
+
"node",
|
|
240
|
+
[bin, "--format=json", "--ignore-pattern", "node_modules", pattern],
|
|
241
|
+
{ cwd, timeout: 6e4, maxBuffer: 50 * 1024 * 1024, reject: false }
|
|
242
|
+
);
|
|
243
|
+
if (result.timedOut || result.isTerminated || result.isMaxBuffer) {
|
|
244
|
+
log.warn("oxlint timed out", { pattern });
|
|
245
|
+
return {
|
|
246
|
+
text: "[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A\u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u3002",
|
|
247
|
+
diagnostics: [],
|
|
248
|
+
engines: ["oxlint"]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const messages = parseOxlintOutput(result.stdout, cwd);
|
|
253
|
+
log.debug("oxlint lint", {
|
|
254
|
+
pattern,
|
|
255
|
+
messageCount: messages.length,
|
|
256
|
+
stderr: result.stderr || void 0
|
|
257
|
+
});
|
|
258
|
+
return formatLintMessages(messages, { label: "oxlint", source: "oxlint" }, warnLimit);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log.warn("oxlint failed", { pattern, error: e.message });
|
|
261
|
+
return {
|
|
262
|
+
text: `[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A${e.message}`,
|
|
263
|
+
diagnostics: [],
|
|
264
|
+
engines: ["oxlint"]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
270
267
|
}
|
|
271
268
|
let _vueTscBin;
|
|
272
269
|
function resolveVueTscBin() {
|
|
@@ -391,6 +388,22 @@ function parseTscDiags(rawOutput, filePath, projectDir, source = "tsc") {
|
|
|
391
388
|
}
|
|
392
389
|
return diags;
|
|
393
390
|
}
|
|
391
|
+
function filterTscOutputForFile(rawOutput, filePath, projectDir) {
|
|
392
|
+
const resolved = import_node_path.default.resolve(filePath);
|
|
393
|
+
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
394
|
+
const filtered = [];
|
|
395
|
+
let keep = false;
|
|
396
|
+
for (const line of rawOutput.split("\n")) {
|
|
397
|
+
const m = errorLinePat.exec(line);
|
|
398
|
+
if (m) {
|
|
399
|
+
keep = import_node_path.default.resolve(projectDir, m[1]) === resolved;
|
|
400
|
+
} else if (!/^\s/.test(line)) {
|
|
401
|
+
keep = false;
|
|
402
|
+
}
|
|
403
|
+
if (keep) filtered.push(line);
|
|
404
|
+
}
|
|
405
|
+
return filtered.join("\n");
|
|
406
|
+
}
|
|
394
407
|
async function runTypeCheck(filePath, cwd) {
|
|
395
408
|
const dir = cwd;
|
|
396
409
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
@@ -407,45 +420,55 @@ async function runTypeCheck(filePath, cwd) {
|
|
|
407
420
|
}
|
|
408
421
|
const timeout = filePath ? 6e4 : 12e4;
|
|
409
422
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
let rawOutput = stdout + stderr;
|
|
416
|
-
const killed = error?.killed;
|
|
417
|
-
const exitCode = typeof error?.code === "number" ? error.code : killed ? 1 : 0;
|
|
418
|
-
if (killed && !rawOutput) {
|
|
419
|
-
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
420
|
-
}
|
|
421
|
-
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
422
|
-
if (filePath) {
|
|
423
|
-
const resolved = import_node_path.default.resolve(filePath);
|
|
424
|
-
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
425
|
-
const lines = rawOutput.split("\n");
|
|
426
|
-
const filtered = [];
|
|
427
|
-
let keep = false;
|
|
428
|
-
for (const line of lines) {
|
|
429
|
-
const m = errorLinePat.exec(line);
|
|
430
|
-
if (m) {
|
|
431
|
-
keep = import_node_path.default.resolve(projectDir, m[1]) === resolved;
|
|
432
|
-
} else if (!/^\s/.test(line)) {
|
|
433
|
-
keep = false;
|
|
434
|
-
}
|
|
435
|
-
if (keep) filtered.push(line);
|
|
436
|
-
}
|
|
437
|
-
rawOutput = filtered.join("\n");
|
|
438
|
-
}
|
|
439
|
-
log.debug("type-check finished", {
|
|
440
|
-
engine: engine.source,
|
|
441
|
-
filePath: filePath || "(all)",
|
|
442
|
-
exitCode,
|
|
443
|
-
outputLength: rawOutput.length
|
|
444
|
-
});
|
|
445
|
-
resolve({ rawOutput, exitCode, diagnostics, source: engine.source });
|
|
446
|
-
}
|
|
447
|
-
);
|
|
423
|
+
const result = await (0, import_execa.execa)("node", [engine.bin, "--build", "--noEmit", "--pretty", "false"], {
|
|
424
|
+
cwd: projectDir,
|
|
425
|
+
timeout,
|
|
426
|
+
maxBuffer,
|
|
427
|
+
reject: false
|
|
448
428
|
});
|
|
429
|
+
let rawOutput = result.stdout + result.stderr;
|
|
430
|
+
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
431
|
+
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
432
|
+
if (killed && !rawOutput) {
|
|
433
|
+
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
434
|
+
}
|
|
435
|
+
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
436
|
+
if (filePath) {
|
|
437
|
+
rawOutput = filterTscOutputForFile(rawOutput, filePath, projectDir);
|
|
438
|
+
}
|
|
439
|
+
log.debug("type-check finished", {
|
|
440
|
+
engine: engine.source,
|
|
441
|
+
filePath: filePath || "(all)",
|
|
442
|
+
exitCode,
|
|
443
|
+
outputLength: rawOutput.length
|
|
444
|
+
});
|
|
445
|
+
return { rawOutput, exitCode, diagnostics, source: engine.source };
|
|
446
|
+
}
|
|
447
|
+
async function runTypeChecksForFiles(files, cwd) {
|
|
448
|
+
const groups = /* @__PURE__ */ new Map();
|
|
449
|
+
for (const file of files) {
|
|
450
|
+
const resolved = import_node_path.default.resolve(file);
|
|
451
|
+
const projectDir = findTsconfigDir(resolved) ?? cwd;
|
|
452
|
+
const group = groups.get(projectDir);
|
|
453
|
+
if (group) group.push(resolved);
|
|
454
|
+
else groups.set(projectDir, [resolved]);
|
|
455
|
+
}
|
|
456
|
+
const results = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const [projectDir, group] of groups) {
|
|
458
|
+
const whole = await runTypeCheck(void 0, projectDir).catch(() => ({
|
|
459
|
+
rawOutput: "",
|
|
460
|
+
exitCode: 0
|
|
461
|
+
}));
|
|
462
|
+
for (const file of group) {
|
|
463
|
+
results.set(file, {
|
|
464
|
+
rawOutput: filterTscOutputForFile(whole.rawOutput, file, projectDir),
|
|
465
|
+
exitCode: whole.exitCode,
|
|
466
|
+
diagnostics: (whole.diagnostics ?? []).filter((d) => d.file === file),
|
|
467
|
+
source: whole.source
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return results;
|
|
449
472
|
}
|
|
450
473
|
async function runAllChecks(pattern, cwd) {
|
|
451
474
|
log.debug("runAllChecks", { pattern, cwd });
|
|
@@ -455,6 +478,18 @@ async function runAllChecks(pattern, cwd) {
|
|
|
455
478
|
]);
|
|
456
479
|
return { eslintOutput, tscOutput };
|
|
457
480
|
}
|
|
481
|
+
async function runAllChecksForFiles(files, cwd) {
|
|
482
|
+
const targets = [...new Set(files.map((file) => import_node_path.default.resolve(file)))];
|
|
483
|
+
const tscByFile = await runTypeChecksForFiles(targets, cwd);
|
|
484
|
+
const results = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const file of targets) {
|
|
486
|
+
results.set(file, {
|
|
487
|
+
eslintOutput: await lintFiles(file, cwd),
|
|
488
|
+
tscOutput: tscByFile.get(file) ?? { rawOutput: "", exitCode: 0 }
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return results;
|
|
492
|
+
}
|
|
458
493
|
async function runProjectDiagnostics(workspace) {
|
|
459
494
|
const tscDirs = import_node_fs.default.existsSync(import_node_path.default.join(workspace, "tsconfig.json")) ? [workspace] : findAllTsconfigDirs(workspace);
|
|
460
495
|
log.debug("Tsc dirs to check", { count: tscDirs.length, dirs: tscDirs });
|
|
@@ -476,18 +511,36 @@ function tscSectionTitle(tscOutput) {
|
|
|
476
511
|
function lintSectionTitle(lintOutput) {
|
|
477
512
|
return lintOutput.engines?.length ? lintOutput.engines.join(" + ") : "ESLint";
|
|
478
513
|
}
|
|
479
|
-
function
|
|
514
|
+
function omittedFindingsHint(omitted) {
|
|
515
|
+
return `\u2026\u8FD8\u6709 ${omitted} \u6761\uFF0C\u5B8C\u6574\u7ED3\u679C\u8BF7\u8C03\u7528 run_diagnostics`;
|
|
516
|
+
}
|
|
517
|
+
function boundFindings(text, maxFindings) {
|
|
518
|
+
if (!maxFindings || !text) return text;
|
|
519
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
520
|
+
if (lines.length <= maxFindings) return text;
|
|
521
|
+
return [...lines.slice(0, maxFindings), omittedFindingsHint(lines.length - maxFindings)].join(
|
|
522
|
+
"\n"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
function formatDiagnosticsSections(title, eslintOutput, tscOutput, options = {}) {
|
|
526
|
+
const lintLines = boundFindings(eslintOutput.text, options.maxFindingsPerSection);
|
|
527
|
+
const tscLines = boundFindings(tscOutput.rawOutput.trim(), options.maxFindingsPerSection);
|
|
480
528
|
const parts = [];
|
|
481
|
-
|
|
529
|
+
if (!options.onlyFindings || lintLines) {
|
|
530
|
+
parts.push(`## ${lintSectionTitle(eslintOutput)}
|
|
482
531
|
|
|
483
|
-
` + (
|
|
484
|
-
|
|
485
|
-
|
|
532
|
+
` + (lintLines || "\u6CA1\u6709\u53D1\u73B0\u95EE\u9898"));
|
|
533
|
+
}
|
|
534
|
+
if (!options.onlyFindings || tscLines) {
|
|
535
|
+
parts.push(`## ${tscSectionTitle(tscOutput)}
|
|
486
536
|
|
|
487
537
|
` + (tscLines || "\u6CA1\u6709\u53D1\u73B0\u7C7B\u578B\u9519\u8BEF"));
|
|
488
|
-
|
|
538
|
+
}
|
|
539
|
+
if (parts.length === 0) return "";
|
|
540
|
+
const body = parts.join("\n\n");
|
|
541
|
+
return title ? `${title}
|
|
489
542
|
|
|
490
|
-
`
|
|
543
|
+
${body}` : body;
|
|
491
544
|
}
|
|
492
545
|
// Annotate the CommonJS export names for ESM import in node:
|
|
493
546
|
0 && (module.exports = {
|
|
@@ -498,8 +551,11 @@ function formatDiagnosticsSections(title, eslintOutput, tscOutput) {
|
|
|
498
551
|
isJsFile,
|
|
499
552
|
lintFiles,
|
|
500
553
|
lintSectionTitle,
|
|
554
|
+
omittedFindingsHint,
|
|
501
555
|
runAllChecks,
|
|
556
|
+
runAllChecksForFiles,
|
|
502
557
|
runProjectDiagnostics,
|
|
503
558
|
runTypeCheck,
|
|
559
|
+
runTypeChecksForFiles,
|
|
504
560
|
tscSectionTitle
|
|
505
561
|
});
|
|
@@ -51,8 +51,20 @@ export declare function findTsconfigDir(filePath: string): string | null;
|
|
|
51
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
52
52
|
/** 运行 TypeScript 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
|
|
53
53
|
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只跑一次 `--build --noEmit`,
|
|
56
|
+
* 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
|
|
57
|
+
* 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
|
|
58
|
+
*/
|
|
59
|
+
export declare function runTypeChecksForFiles(files: string[], cwd: string): Promise<Map<string, TscResult>>;
|
|
54
60
|
/** 并行运行 ESLint + 类型检查(单文件或 glob) */
|
|
55
61
|
export declare function runAllChecks(pattern: string, cwd: string): Promise<DiagnosticsResult>;
|
|
62
|
+
/**
|
|
63
|
+
* 批量运行 ESLint + 类型检查:Lint 逐文件(ESLint 进程内、oxlint 单次 CLI),
|
|
64
|
+
* 类型检查按 tsconfig 项目合并为一次(见 runTypeChecksForFiles)。
|
|
65
|
+
* 返回"文件绝对路径 → 诊断结果",供 step 边界的一次性收尾诊断使用。
|
|
66
|
+
*/
|
|
67
|
+
export declare function runAllChecksForFiles(files: string[], cwd: string): Promise<Map<string, DiagnosticsResult>>;
|
|
56
68
|
/**
|
|
57
69
|
* 全量项目诊断:优先从根 tsconfig 运行一次类型检查 --build,
|
|
58
70
|
* 根无 tsconfig 时回退到逐个子目录 build;ESLint 以 "." 全量扫描。
|
|
@@ -62,5 +74,17 @@ export declare function runProjectDiagnostics(workspace: string): Promise<Diagno
|
|
|
62
74
|
export declare function tscSectionTitle(tscOutput: TscResult): string;
|
|
63
75
|
/** Lint 分区标题(单一来源:跟随实际运行的引擎组合,如 "ESLint + oxlint") */
|
|
64
76
|
export declare function lintSectionTitle(lintOutput: EslintOutput): string;
|
|
65
|
-
/**
|
|
66
|
-
export declare function
|
|
77
|
+
/** 分区发现被条数上限折叠时的提示(自动诊断摘要与 run_diagnostics 完整输出的分界) */
|
|
78
|
+
export declare function omittedFindingsHint(omitted: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* 组装统一的分区诊断文本(Lint / 类型检查)。
|
|
81
|
+
* 默认保留空分区的占位文案(run_diagnostics 要正面回答"有没有问题");
|
|
82
|
+
* `onlyFindings: true` 只输出有发现的分区、全空返回空串(编辑后自动诊断不刷占位噪音);
|
|
83
|
+
* `maxFindingsPerSection` 把每个分区的发现折叠成有界摘要——编辑后自动诊断每个 step 都会投递,
|
|
84
|
+
* 因此用条数上限控制重复成本,完整结果仍由不带上限的 run_diagnostics 给出。
|
|
85
|
+
* `title` 允许为空串,此时直接返回分区正文(追加式投递场景无需标题)。
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatDiagnosticsSections(title: string, eslintOutput: EslintOutput, tscOutput: TscResult, options?: {
|
|
88
|
+
onlyFindings?: boolean;
|
|
89
|
+
maxFindingsPerSection?: number;
|
|
90
|
+
}): string;
|
package/lib/node/node-utils.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(node_utils_exports, {
|
|
|
40
40
|
});
|
|
41
41
|
module.exports = __toCommonJS(node_utils_exports);
|
|
42
42
|
var import_node_child_process = require("node:child_process");
|
|
43
|
+
var import_execa = require("execa");
|
|
43
44
|
var import_node_module = require("node:module");
|
|
44
45
|
var import_node_fs = __toESM(require("node:fs"));
|
|
45
46
|
var import_node_http = __toESM(require("node:http"));
|
|
@@ -161,40 +162,18 @@ function findGitRoot(startDir, maxDepth = 10) {
|
|
|
161
162
|
}
|
|
162
163
|
async function checkCliInstalled(bin) {
|
|
163
164
|
const timer = new import_node_logger.PerformanceTimer(`checkCliInstalled:${bin}`);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
proc.on("error", (err) => {
|
|
172
|
-
log.debug(`Failed to check ${bin} installation`, { error: err.message });
|
|
173
|
-
timer.end("\u2716 Check failed");
|
|
174
|
-
resolve(false);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
165
|
+
const result = await (0, import_execa.execa)(bin, ["--version"], { reject: false, stdio: "ignore" });
|
|
166
|
+
const installed = result.exitCode === 0;
|
|
167
|
+
if (!installed && result.exitCode === void 0) {
|
|
168
|
+
log.debug(`Failed to check ${bin} installation`, { error: result.message });
|
|
169
|
+
}
|
|
170
|
+
timer.end(installed ? `\u2713 ${bin} is installed` : `\u2716 ${bin} not found`);
|
|
171
|
+
return installed;
|
|
177
172
|
}
|
|
178
|
-
function getCliVersion(bin) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let stderr = "";
|
|
183
|
-
proc.stdout?.on("data", (data) => {
|
|
184
|
-
stdout += data.toString();
|
|
185
|
-
});
|
|
186
|
-
proc.stderr?.on("data", (data) => {
|
|
187
|
-
stderr += data.toString();
|
|
188
|
-
});
|
|
189
|
-
proc.on("close", (code) => {
|
|
190
|
-
if (code !== 0) {
|
|
191
|
-
resolve(null);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
resolve(stdout.trim() || stderr.trim() || null);
|
|
195
|
-
});
|
|
196
|
-
proc.on("error", () => resolve(null));
|
|
197
|
-
});
|
|
173
|
+
async function getCliVersion(bin) {
|
|
174
|
+
const result = await (0, import_execa.execa)(bin, ["--version"], { reject: false });
|
|
175
|
+
if (result.exitCode !== 0) return null;
|
|
176
|
+
return result.stdout.trim() || result.stderr.trim() || null;
|
|
198
177
|
}
|
|
199
178
|
function killOrphanCliProcesses(bin, options) {
|
|
200
179
|
const label = options.label ?? bin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipanel/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "lib/index.cjs",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"registry": "https://registry.npmjs.org/"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"execa": "^9.6.1",
|
|
35
36
|
"vue-tsc": "^3.3.9"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|