pi-rtk-optimizer 0.3.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/config/config.example.json +35 -0
- package/index.ts +3 -0
- package/package.json +59 -0
- package/src/command-completions.ts +49 -0
- package/src/command-rewriter.ts +349 -0
- package/src/compat-commands.ts +207 -0
- package/src/config-modal.ts +600 -0
- package/src/config-store.ts +217 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +291 -0
- package/src/output-compactor-test.ts +120 -0
- package/src/output-compactor.ts +343 -0
- package/src/output-metrics.ts +69 -0
- package/src/rewrite-rules.ts +248 -0
- package/src/techniques/ansi.ts +13 -0
- package/src/techniques/build.ts +155 -0
- package/src/techniques/command-detection.ts +53 -0
- package/src/techniques/git.ts +229 -0
- package/src/techniques/index.ts +16 -0
- package/src/techniques/linter.ts +161 -0
- package/src/techniques/search.ts +76 -0
- package/src/techniques/source.ts +230 -0
- package/src/techniques/test-output.ts +172 -0
- package/src/techniques/truncate.ts +11 -0
- package/src/types-shims.d.ts +131 -0
- package/src/types.ts +114 -0
- package/src/windows-command-helpers.ts +84 -0
- package/src/zellij-modal.ts +1001 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { matchesCommandPatterns } from "./command-detection.js";
|
|
2
|
+
|
|
3
|
+
interface BuildStats {
|
|
4
|
+
compiled: number;
|
|
5
|
+
errors: string[][];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BUILD_COMMAND_PATTERNS = [
|
|
10
|
+
/^cargo\s+(build|check)\b/,
|
|
11
|
+
/^bun\s+build\b/,
|
|
12
|
+
/^npm\s+run\s+build\b/,
|
|
13
|
+
/^yarn\s+build\b/,
|
|
14
|
+
/^pnpm\s+build\b/,
|
|
15
|
+
/^(?:npx\s+)?tsc\b/,
|
|
16
|
+
/^make\b/,
|
|
17
|
+
/^cmake\b/,
|
|
18
|
+
/^gradle\b/,
|
|
19
|
+
/^mvn\b/,
|
|
20
|
+
/^go\s+(build|install)\b/,
|
|
21
|
+
/^python\s+setup\.py\s+build\b/,
|
|
22
|
+
/^pip\s+install\b/,
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const SKIP_PATTERNS = [
|
|
26
|
+
/^\s*Compiling\s+/,
|
|
27
|
+
/^\s*Checking\s+/,
|
|
28
|
+
/^\s*Downloading\s+/,
|
|
29
|
+
/^\s*Downloaded\s+/,
|
|
30
|
+
/^\s*Fetching\s+/,
|
|
31
|
+
/^\s*Fetched\s+/,
|
|
32
|
+
/^\s*Updating\s+/,
|
|
33
|
+
/^\s*Updated\s+/,
|
|
34
|
+
/^\s*Building\s+/,
|
|
35
|
+
/^\s*Generated\s+/,
|
|
36
|
+
/^\s*Creating\s+/,
|
|
37
|
+
/^\s*Running\s+/,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const ERROR_START_PATTERNS = [/^error\[/, /^error:/, /^\[ERROR\]/, /^FAIL/];
|
|
41
|
+
const WARNING_PATTERNS = [/^warning:/, /^\[WARNING\]/, /^warn:/];
|
|
42
|
+
|
|
43
|
+
function isSkipLine(line: string): boolean {
|
|
44
|
+
return SKIP_PATTERNS.some((pattern) => pattern.test(line));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isErrorStart(line: string): boolean {
|
|
48
|
+
return ERROR_START_PATTERNS.some((pattern) => pattern.test(line));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isWarning(line: string): boolean {
|
|
52
|
+
return WARNING_PATTERNS.some((pattern) => pattern.test(line));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isBuildCommand(command: string | undefined | null): boolean {
|
|
56
|
+
return matchesCommandPatterns(command, BUILD_COMMAND_PATTERNS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function filterBuildOutput(output: string, command: string | undefined | null): string | null {
|
|
60
|
+
if (!isBuildCommand(command)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lines = output.split("\n");
|
|
65
|
+
const stats: BuildStats = {
|
|
66
|
+
compiled: 0,
|
|
67
|
+
errors: [],
|
|
68
|
+
warnings: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let inErrorBlock = false;
|
|
72
|
+
let currentError: string[] = [];
|
|
73
|
+
let blankCount = 0;
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.match(/^\s*(Compiling|Checking|Building)\s+/)) {
|
|
77
|
+
stats.compiled++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isSkipLine(line)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isErrorStart(line)) {
|
|
86
|
+
if (inErrorBlock && currentError.length > 0) {
|
|
87
|
+
stats.errors.push([...currentError]);
|
|
88
|
+
}
|
|
89
|
+
inErrorBlock = true;
|
|
90
|
+
currentError = [line];
|
|
91
|
+
blankCount = 0;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isWarning(line)) {
|
|
96
|
+
stats.warnings.push(line);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!inErrorBlock) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (line.trim() === "") {
|
|
105
|
+
blankCount++;
|
|
106
|
+
if (blankCount >= 2 && currentError.length > 3) {
|
|
107
|
+
stats.errors.push([...currentError]);
|
|
108
|
+
inErrorBlock = false;
|
|
109
|
+
currentError = [];
|
|
110
|
+
} else {
|
|
111
|
+
currentError.push(line);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (line.match(/^\s/) || line.match(/^-->/)) {
|
|
117
|
+
currentError.push(line);
|
|
118
|
+
blankCount = 0;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stats.errors.push([...currentError]);
|
|
123
|
+
inErrorBlock = false;
|
|
124
|
+
currentError = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (inErrorBlock && currentError.length > 0) {
|
|
128
|
+
stats.errors.push(currentError);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (stats.errors.length === 0 && stats.warnings.length === 0) {
|
|
132
|
+
return `✓ Build successful (${stats.compiled} units compiled)`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result: string[] = [];
|
|
136
|
+
|
|
137
|
+
if (stats.errors.length > 0) {
|
|
138
|
+
result.push(`❌ ${stats.errors.length} error(s):`);
|
|
139
|
+
for (const error of stats.errors.slice(0, 5)) {
|
|
140
|
+
result.push(...error.slice(0, 10));
|
|
141
|
+
if (error.length > 10) {
|
|
142
|
+
result.push(" ...");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (stats.errors.length > 5) {
|
|
146
|
+
result.push(`... and ${stats.errors.length - 5} more errors`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (stats.warnings.length > 0) {
|
|
151
|
+
result.push(`\n⚠️ ${stats.warnings.length} warning(s)`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result.join("\n");
|
|
155
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const ENV_PREFIX_PATTERN = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*/;
|
|
2
|
+
const CHAIN_OPERATORS = ["&&", "||", ";", "|"] as const;
|
|
3
|
+
|
|
4
|
+
function sliceFirstSegment(command: string): string {
|
|
5
|
+
let cutIndex = -1;
|
|
6
|
+
for (const operator of CHAIN_OPERATORS) {
|
|
7
|
+
const index = command.indexOf(operator);
|
|
8
|
+
if (index === -1) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (cutIndex === -1 || index < cutIndex) {
|
|
12
|
+
cutIndex = index;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (cutIndex === -1) {
|
|
17
|
+
return command;
|
|
18
|
+
}
|
|
19
|
+
return command.slice(0, cutIndex);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeCommandForDetection(command: string | undefined | null): string | null {
|
|
23
|
+
if (typeof command !== "string") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const firstNonEmptyLine = command
|
|
28
|
+
.split(/\r?\n/)
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.find((line) => line.length > 0);
|
|
31
|
+
if (!firstNonEmptyLine) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const withoutEnvPrefix = firstNonEmptyLine.replace(ENV_PREFIX_PATTERN, "").trim();
|
|
36
|
+
if (!withoutEnvPrefix) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const firstSegment = sliceFirstSegment(withoutEnvPrefix).trim().toLowerCase();
|
|
41
|
+
return firstSegment || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function matchesCommandPatterns(
|
|
45
|
+
command: string | undefined | null,
|
|
46
|
+
patterns: readonly RegExp[],
|
|
47
|
+
): boolean {
|
|
48
|
+
const normalized = normalizeCommandForDetection(command);
|
|
49
|
+
if (!normalized) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
53
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
|
+
|
|
3
|
+
const GIT_COMMAND_PATTERNS = [/^git\s+(diff|status|log|show|stash)\b/] as const;
|
|
4
|
+
|
|
5
|
+
export function isGitCommand(command: string | undefined | null): boolean {
|
|
6
|
+
return matchesCommandPatterns(command, GIT_COMMAND_PATTERNS);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function compactDiff(output: string, maxLines = 50): string {
|
|
10
|
+
const lines = output.split("\n");
|
|
11
|
+
const result: string[] = [];
|
|
12
|
+
let currentFile = "";
|
|
13
|
+
let added = 0;
|
|
14
|
+
let removed = 0;
|
|
15
|
+
let inHunk = false;
|
|
16
|
+
let hunkLines = 0;
|
|
17
|
+
const maxHunkLines = 10;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
if (result.length >= maxLines) {
|
|
21
|
+
result.push("\n... (more changes truncated)");
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (line.startsWith("diff --git")) {
|
|
26
|
+
if (currentFile && (added > 0 || removed > 0)) {
|
|
27
|
+
result.push(` +${added} -${removed}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
|
|
31
|
+
currentFile = match?.[2] ?? "unknown";
|
|
32
|
+
result.push(`\n📄 ${currentFile}`);
|
|
33
|
+
added = 0;
|
|
34
|
+
removed = 0;
|
|
35
|
+
inHunk = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (line.startsWith("@@")) {
|
|
40
|
+
inHunk = true;
|
|
41
|
+
hunkLines = 0;
|
|
42
|
+
const hunkInfo = line.match(/@@ .+ @@/)?.[0] ?? "@@";
|
|
43
|
+
result.push(` ${hunkInfo}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!inHunk) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
52
|
+
added++;
|
|
53
|
+
if (hunkLines < maxHunkLines) {
|
|
54
|
+
result.push(` ${line}`);
|
|
55
|
+
hunkLines++;
|
|
56
|
+
}
|
|
57
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
58
|
+
removed++;
|
|
59
|
+
if (hunkLines < maxHunkLines) {
|
|
60
|
+
result.push(` ${line}`);
|
|
61
|
+
hunkLines++;
|
|
62
|
+
}
|
|
63
|
+
} else if (hunkLines < maxHunkLines && !line.startsWith("\\")) {
|
|
64
|
+
if (hunkLines > 0) {
|
|
65
|
+
result.push(` ${line}`);
|
|
66
|
+
hunkLines++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (hunkLines === maxHunkLines) {
|
|
71
|
+
result.push(" ... (truncated)");
|
|
72
|
+
hunkLines++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (currentFile && (added > 0 || removed > 0)) {
|
|
77
|
+
result.push(` +${added} -${removed}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface StatusStats {
|
|
84
|
+
staged: number;
|
|
85
|
+
modified: number;
|
|
86
|
+
untracked: number;
|
|
87
|
+
conflicts: number;
|
|
88
|
+
stagedFiles: string[];
|
|
89
|
+
modifiedFiles: string[];
|
|
90
|
+
untrackedFiles: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function compactStatus(output: string): string {
|
|
94
|
+
const lines = output.split("\n");
|
|
95
|
+
|
|
96
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0]?.trim() === "")) {
|
|
97
|
+
return "Clean working tree";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stats: StatusStats = {
|
|
101
|
+
staged: 0,
|
|
102
|
+
modified: 0,
|
|
103
|
+
untracked: 0,
|
|
104
|
+
conflicts: 0,
|
|
105
|
+
stagedFiles: [],
|
|
106
|
+
modifiedFiles: [],
|
|
107
|
+
untrackedFiles: [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let branchName = "";
|
|
111
|
+
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
if (line.startsWith("##")) {
|
|
114
|
+
const match = line.match(/## (.+)/);
|
|
115
|
+
if (match?.[1]) {
|
|
116
|
+
branchName = match[1].split("...")[0] ?? match[1];
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (line.length < 3) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const status = line.slice(0, 2);
|
|
126
|
+
const filename = line.slice(3);
|
|
127
|
+
const indexStatus = status[0];
|
|
128
|
+
const worktreeStatus = status[1];
|
|
129
|
+
|
|
130
|
+
if (["M", "A", "D", "R", "C"].includes(indexStatus)) {
|
|
131
|
+
stats.staged++;
|
|
132
|
+
stats.stagedFiles.push(filename);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (indexStatus === "U") {
|
|
136
|
+
stats.conflicts++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (["M", "D"].includes(worktreeStatus)) {
|
|
140
|
+
stats.modified++;
|
|
141
|
+
stats.modifiedFiles.push(filename);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (status === "??") {
|
|
145
|
+
stats.untracked++;
|
|
146
|
+
stats.untrackedFiles.push(filename);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let result = `📌 ${branchName}\n`;
|
|
151
|
+
|
|
152
|
+
if (stats.staged > 0) {
|
|
153
|
+
result += `✅ Staged: ${stats.staged} files\n`;
|
|
154
|
+
for (const file of stats.stagedFiles.slice(0, 5)) {
|
|
155
|
+
result += ` ${file}\n`;
|
|
156
|
+
}
|
|
157
|
+
if (stats.staged > 5) {
|
|
158
|
+
result += ` ... +${stats.staged - 5} more\n`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (stats.modified > 0) {
|
|
163
|
+
result += `📝 Modified: ${stats.modified} files\n`;
|
|
164
|
+
for (const file of stats.modifiedFiles.slice(0, 5)) {
|
|
165
|
+
result += ` ${file}\n`;
|
|
166
|
+
}
|
|
167
|
+
if (stats.modified > 5) {
|
|
168
|
+
result += ` ... +${stats.modified - 5} more\n`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (stats.untracked > 0) {
|
|
173
|
+
result += `❓ Untracked: ${stats.untracked} files\n`;
|
|
174
|
+
for (const file of stats.untrackedFiles.slice(0, 3)) {
|
|
175
|
+
result += ` ${file}\n`;
|
|
176
|
+
}
|
|
177
|
+
if (stats.untracked > 3) {
|
|
178
|
+
result += ` ... +${stats.untracked - 3} more\n`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (stats.conflicts > 0) {
|
|
183
|
+
result += `⚠️ Conflicts: ${stats.conflicts} files\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result.trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function compactLog(output: string, limit = 20): string {
|
|
190
|
+
const lines = output.split("\n");
|
|
191
|
+
const result: string[] = [];
|
|
192
|
+
|
|
193
|
+
for (const line of lines.slice(0, limit)) {
|
|
194
|
+
if (line.length > 80) {
|
|
195
|
+
result.push(`${line.slice(0, 77)}...`);
|
|
196
|
+
} else {
|
|
197
|
+
result.push(line);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (lines.length > limit) {
|
|
202
|
+
result.push(`... and ${lines.length - limit} more commits`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function compactGitOutput(output: string, command: string | undefined | null): string | null {
|
|
209
|
+
if (!isGitCommand(command)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const normalized = normalizeCommandForDetection(command);
|
|
214
|
+
if (!normalized) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (normalized.startsWith("git diff")) {
|
|
219
|
+
return compactDiff(output);
|
|
220
|
+
}
|
|
221
|
+
if (normalized.startsWith("git status")) {
|
|
222
|
+
return compactStatus(output);
|
|
223
|
+
}
|
|
224
|
+
if (normalized.startsWith("git log")) {
|
|
225
|
+
return compactLog(output);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { stripAnsi, stripAnsiFast } from "./ansi.js";
|
|
2
|
+
export { truncate } from "./truncate.js";
|
|
3
|
+
export { filterBuildOutput, isBuildCommand } from "./build.js";
|
|
4
|
+
export { aggregateTestOutput, isTestCommand } from "./test-output.js";
|
|
5
|
+
export { aggregateLinterOutput, isLinterCommand } from "./linter.js";
|
|
6
|
+
export {
|
|
7
|
+
detectLanguage,
|
|
8
|
+
filterMinimal,
|
|
9
|
+
filterAggressive,
|
|
10
|
+
smartTruncate,
|
|
11
|
+
filterSourceCode,
|
|
12
|
+
type Language,
|
|
13
|
+
} from "./source.js";
|
|
14
|
+
export { compactDiff, compactStatus, compactLog, compactGitOutput, isGitCommand } from "./git.js";
|
|
15
|
+
export { groupSearchResults } from "./search.js";
|
|
16
|
+
export { normalizeCommandForDetection, matchesCommandPatterns } from "./command-detection.js";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
|
+
|
|
3
|
+
const LINTER_COMMAND_PATTERNS = [
|
|
4
|
+
/^(?:pnpm\s+)?(?:npx\s+)?eslint\b/,
|
|
5
|
+
/^(?:npx\s+)?prettier\b/,
|
|
6
|
+
/^ruff\b/,
|
|
7
|
+
/^pylint\b/,
|
|
8
|
+
/^mypy\b/,
|
|
9
|
+
/^flake8\b/,
|
|
10
|
+
/^black\b/,
|
|
11
|
+
/^cargo\s+clippy\b/,
|
|
12
|
+
/^golangci-lint\b/,
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
interface Issue {
|
|
16
|
+
severity: "ERROR" | "WARNING";
|
|
17
|
+
rule: string;
|
|
18
|
+
file: string;
|
|
19
|
+
line?: number;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isLinterCommand(command: string | undefined | null): boolean {
|
|
24
|
+
return matchesCommandPatterns(command, LINTER_COMMAND_PATTERNS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseLine(line: string): Issue | null {
|
|
28
|
+
const fileLinePattern = /^(.+):(\d+):(\d+):\s*(.+)$/;
|
|
29
|
+
const rustPattern = /^(error|warning):\s*(.+?)\s+at\s+(.+):(\d+):(\d+)$/;
|
|
30
|
+
|
|
31
|
+
const fileLineMatch = line.match(fileLinePattern);
|
|
32
|
+
if (fileLineMatch) {
|
|
33
|
+
const file = fileLineMatch[1] ?? "unknown";
|
|
34
|
+
const lineNumber = Number.parseInt(fileLineMatch[2] ?? "0", 10);
|
|
35
|
+
const content = fileLineMatch[4] ?? line;
|
|
36
|
+
const severity = /warning/i.test(content) ? "WARNING" : "ERROR";
|
|
37
|
+
const rule = content.match(/\[(.+?)\]$/)?.[1] ?? "unknown";
|
|
38
|
+
return {
|
|
39
|
+
severity,
|
|
40
|
+
rule,
|
|
41
|
+
file,
|
|
42
|
+
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
|
|
43
|
+
message: content,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rustMatch = line.match(rustPattern);
|
|
48
|
+
if (rustMatch) {
|
|
49
|
+
const severity = (rustMatch[1]?.toUpperCase() ?? "ERROR") as "ERROR" | "WARNING";
|
|
50
|
+
const message = rustMatch[2] ?? line;
|
|
51
|
+
const file = rustMatch[3] ?? "unknown";
|
|
52
|
+
const lineNumber = Number.parseInt(rustMatch[4] ?? "0", 10);
|
|
53
|
+
return {
|
|
54
|
+
severity,
|
|
55
|
+
rule: "unknown",
|
|
56
|
+
file,
|
|
57
|
+
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
|
|
58
|
+
message,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseIssues(output: string): Issue[] {
|
|
66
|
+
const issues: Issue[] = [];
|
|
67
|
+
for (const line of output.split("\n")) {
|
|
68
|
+
const parsed = parseLine(line);
|
|
69
|
+
if (parsed) {
|
|
70
|
+
issues.push(parsed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return issues;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detectLinterType(command: string | undefined | null): string {
|
|
77
|
+
const normalized = normalizeCommandForDetection(command);
|
|
78
|
+
if (!normalized) {
|
|
79
|
+
return "Linter";
|
|
80
|
+
}
|
|
81
|
+
if (/(?:^|\s)eslint\b/.test(normalized)) return "ESLint";
|
|
82
|
+
if (/^ruff\b/.test(normalized)) return "Ruff";
|
|
83
|
+
if (/^pylint\b/.test(normalized)) return "Pylint";
|
|
84
|
+
if (/^mypy\b/.test(normalized)) return "MyPy";
|
|
85
|
+
if (/^flake8\b/.test(normalized)) return "Flake8";
|
|
86
|
+
if (/clippy\b/.test(normalized)) return "Clippy";
|
|
87
|
+
if (/^golangci-lint\b/.test(normalized)) return "GolangCI-Lint";
|
|
88
|
+
if (/prettier\b/.test(normalized)) return "Prettier";
|
|
89
|
+
return "Linter";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compactPath(path: string, maxLength: number): string {
|
|
93
|
+
if (path.length <= maxLength) {
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
96
|
+
const parts = path.split("/");
|
|
97
|
+
if (parts.length <= 3) {
|
|
98
|
+
return path;
|
|
99
|
+
}
|
|
100
|
+
return `${parts[0]}/.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function aggregateLinterOutput(output: string, command: string | undefined | null): string | null {
|
|
104
|
+
if (!isLinterCommand(command)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const linterType = detectLinterType(command);
|
|
109
|
+
const issues = parseIssues(output);
|
|
110
|
+
|
|
111
|
+
if (issues.length === 0) {
|
|
112
|
+
return `✓ ${linterType}: No issues found`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const errors = issues.filter((issue) => issue.severity === "ERROR").length;
|
|
116
|
+
const warnings = issues.filter((issue) => issue.severity === "WARNING").length;
|
|
117
|
+
|
|
118
|
+
const byRule = new Map<string, number>();
|
|
119
|
+
for (const issue of issues) {
|
|
120
|
+
byRule.set(issue.rule, (byRule.get(issue.rule) ?? 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const byFile = new Map<string, Issue[]>();
|
|
124
|
+
for (const issue of issues) {
|
|
125
|
+
const existing = byFile.get(issue.file) ?? [];
|
|
126
|
+
existing.push(issue);
|
|
127
|
+
byFile.set(issue.file, existing);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let result = `${linterType}: ${errors} errors, ${warnings} warnings in ${byFile.size} files\n`;
|
|
131
|
+
result += "═══════════════════════════════════════\n";
|
|
132
|
+
|
|
133
|
+
result += "Top rules:\n";
|
|
134
|
+
const sortedRules = Array.from(byRule.entries())
|
|
135
|
+
.sort((left, right) => right[1] - left[1])
|
|
136
|
+
.slice(0, 10);
|
|
137
|
+
for (const [rule, count] of sortedRules) {
|
|
138
|
+
result += ` ${rule} (${count}x)\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result += "\nTop files:\n";
|
|
142
|
+
const sortedFiles = Array.from(byFile.entries())
|
|
143
|
+
.sort((left, right) => right[1].length - left[1].length)
|
|
144
|
+
.slice(0, 10);
|
|
145
|
+
|
|
146
|
+
for (const [file, fileIssues] of sortedFiles) {
|
|
147
|
+
result += ` ${compactPath(file, 40)} (${fileIssues.length} issues)\n`;
|
|
148
|
+
const fileRules = new Map<string, number>();
|
|
149
|
+
for (const issue of fileIssues) {
|
|
150
|
+
fileRules.set(issue.rule, (fileRules.get(issue.rule) ?? 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
const topRules = Array.from(fileRules.entries())
|
|
153
|
+
.sort((left, right) => right[1] - left[1])
|
|
154
|
+
.slice(0, 3);
|
|
155
|
+
for (const [rule, count] of topRules) {
|
|
156
|
+
result += ` ${rule} (${count})\n`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
interface SearchResult {
|
|
2
|
+
file: string;
|
|
3
|
+
lineNumber: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function compactPath(path: string, maxLength: number): string {
|
|
8
|
+
if (path.length <= maxLength) {
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
const parts = path.split("/");
|
|
12
|
+
if (parts.length <= 3) {
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
return `${parts[0]}/.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function groupSearchResults(output: string, maxResults = 50): string | null {
|
|
19
|
+
const results: SearchResult[] = [];
|
|
20
|
+
for (const line of output.split("\n")) {
|
|
21
|
+
if (!line.trim()) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const match = line.match(/^(.+?):(\d+)?:(.+)$/);
|
|
25
|
+
if (!match) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
results.push({
|
|
29
|
+
file: match[1] ?? "unknown",
|
|
30
|
+
lineNumber: match[2] ?? "?",
|
|
31
|
+
content: match[3] ?? "",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (results.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const byFile = new Map<string, SearchResult[]>();
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
const existing = byFile.get(result.file) ?? [];
|
|
42
|
+
existing.push(result);
|
|
43
|
+
byFile.set(result.file, existing);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let outputText = `🔍 ${results.length} matches in ${byFile.size} files:\n\n`;
|
|
47
|
+
const sortedFiles = Array.from(byFile.entries()).sort((left, right) =>
|
|
48
|
+
left[0].localeCompare(right[0]),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
let shown = 0;
|
|
52
|
+
for (const [file, matches] of sortedFiles) {
|
|
53
|
+
if (shown >= maxResults) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
outputText += `📄 ${compactPath(file, 50)} (${matches.length} matches):\n`;
|
|
57
|
+
for (const match of matches.slice(0, 10)) {
|
|
58
|
+
let cleaned = match.content.trim();
|
|
59
|
+
if (cleaned.length > 70) {
|
|
60
|
+
cleaned = `${cleaned.slice(0, 67)}...`;
|
|
61
|
+
}
|
|
62
|
+
outputText += ` ${match.lineNumber}: ${cleaned}\n`;
|
|
63
|
+
shown++;
|
|
64
|
+
}
|
|
65
|
+
if (matches.length > 10) {
|
|
66
|
+
outputText += ` +${matches.length - 10} more\n`;
|
|
67
|
+
}
|
|
68
|
+
outputText += "\n";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (results.length > shown) {
|
|
72
|
+
outputText += `... +${results.length - shown} more\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return outputText;
|
|
76
|
+
}
|