pi-rtk-optimizer 0.3.2 → 0.4.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.
@@ -1,161 +1,151 @@
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
- }
1
+ import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
2
+ import { compactPath } from "./path-utils.js";
3
+
4
+ const LINTER_COMMAND_PATTERNS = [
5
+ /^(?:pnpm\s+)?(?:npx\s+)?eslint\b/,
6
+ /^(?:npx\s+)?prettier\b/,
7
+ /^ruff\b/,
8
+ /^pylint\b/,
9
+ /^mypy\b/,
10
+ /^flake8\b/,
11
+ /^black\b/,
12
+ /^cargo\s+clippy\b/,
13
+ /^golangci-lint\b/,
14
+ ] as const;
15
+
16
+ interface Issue {
17
+ severity: "ERROR" | "WARNING";
18
+ rule: string;
19
+ file: string;
20
+ line?: number;
21
+ message: string;
22
+ }
23
+
24
+ export function isLinterCommand(command: string | undefined | null): boolean {
25
+ return matchesCommandPatterns(command, LINTER_COMMAND_PATTERNS);
26
+ }
27
+
28
+ function parseLine(line: string): Issue | null {
29
+ const fileLinePattern = /^(.+):(\d+):(\d+):\s*(.+)$/;
30
+ const rustPattern = /^(error|warning):\s*(.+?)\s+at\s+(.+):(\d+):(\d+)$/;
31
+
32
+ const fileLineMatch = line.match(fileLinePattern);
33
+ if (fileLineMatch) {
34
+ const file = fileLineMatch[1] ?? "unknown";
35
+ const lineNumber = Number.parseInt(fileLineMatch[2] ?? "0", 10);
36
+ const content = fileLineMatch[4] ?? line;
37
+ const severity = /warning/i.test(content) ? "WARNING" : "ERROR";
38
+ const rule = content.match(/\[(.+?)\]$/)?.[1] ?? "unknown";
39
+ return {
40
+ severity,
41
+ rule,
42
+ file,
43
+ line: Number.isNaN(lineNumber) ? undefined : lineNumber,
44
+ message: content,
45
+ };
46
+ }
47
+
48
+ const rustMatch = line.match(rustPattern);
49
+ if (rustMatch) {
50
+ const severity = (rustMatch[1]?.toUpperCase() ?? "ERROR") as "ERROR" | "WARNING";
51
+ const message = rustMatch[2] ?? line;
52
+ const file = rustMatch[3] ?? "unknown";
53
+ const lineNumber = Number.parseInt(rustMatch[4] ?? "0", 10);
54
+ return {
55
+ severity,
56
+ rule: "unknown",
57
+ file,
58
+ line: Number.isNaN(lineNumber) ? undefined : lineNumber,
59
+ message,
60
+ };
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ function parseIssues(output: string): Issue[] {
67
+ const issues: Issue[] = [];
68
+ for (const line of output.split("\n")) {
69
+ const parsed = parseLine(line);
70
+ if (parsed) {
71
+ issues.push(parsed);
72
+ }
73
+ }
74
+ return issues;
75
+ }
76
+
77
+ function detectLinterType(command: string | undefined | null): string {
78
+ const normalized = normalizeCommandForDetection(command);
79
+ if (!normalized) {
80
+ return "Linter";
81
+ }
82
+ if (/(?:^|\s)eslint\b/.test(normalized)) return "ESLint";
83
+ if (/^ruff\b/.test(normalized)) return "Ruff";
84
+ if (/^pylint\b/.test(normalized)) return "Pylint";
85
+ if (/^mypy\b/.test(normalized)) return "MyPy";
86
+ if (/^flake8\b/.test(normalized)) return "Flake8";
87
+ if (/clippy\b/.test(normalized)) return "Clippy";
88
+ if (/^golangci-lint\b/.test(normalized)) return "GolangCI-Lint";
89
+ if (/prettier\b/.test(normalized)) return "Prettier";
90
+ return "Linter";
91
+ }
92
+
93
+ export function aggregateLinterOutput(output: string, command: string | undefined | null): string | null {
94
+ if (!isLinterCommand(command)) {
95
+ return null;
96
+ }
97
+
98
+ const linterType = detectLinterType(command);
99
+ const issues = parseIssues(output);
100
+
101
+ if (issues.length === 0) {
102
+ return `[OK] ${linterType}: No issues found`;
103
+ }
104
+
105
+ const errors = issues.filter((issue) => issue.severity === "ERROR").length;
106
+ const warnings = issues.filter((issue) => issue.severity === "WARNING").length;
107
+
108
+ const byRule = new Map<string, number>();
109
+ for (const issue of issues) {
110
+ byRule.set(issue.rule, (byRule.get(issue.rule) ?? 0) + 1);
111
+ }
112
+
113
+ const byFile = new Map<string, Issue[]>();
114
+ for (const issue of issues) {
115
+ const existing = byFile.get(issue.file) ?? [];
116
+ existing.push(issue);
117
+ byFile.set(issue.file, existing);
118
+ }
119
+
120
+ let result = `${linterType}: ${errors} errors, ${warnings} warnings in ${byFile.size} files\n`;
121
+ result += "═══════════════════════════════════════\n";
122
+
123
+ result += "Top rules:\n";
124
+ const sortedRules = Array.from(byRule.entries())
125
+ .sort((left, right) => right[1] - left[1])
126
+ .slice(0, 10);
127
+ for (const [rule, count] of sortedRules) {
128
+ result += ` ${rule} (${count}x)\n`;
129
+ }
130
+
131
+ result += "\nTop files:\n";
132
+ const sortedFiles = Array.from(byFile.entries())
133
+ .sort((left, right) => right[1].length - left[1].length)
134
+ .slice(0, 10);
135
+
136
+ for (const [file, fileIssues] of sortedFiles) {
137
+ result += ` ${compactPath(file, 40)} (${fileIssues.length} issues)\n`;
138
+ const fileRules = new Map<string, number>();
139
+ for (const issue of fileIssues) {
140
+ fileRules.set(issue.rule, (fileRules.get(issue.rule) ?? 0) + 1);
141
+ }
142
+ const topRules = Array.from(fileRules.entries())
143
+ .sort((left, right) => right[1] - left[1])
144
+ .slice(0, 3);
145
+ for (const [rule, count] of topRules) {
146
+ result += ` ${rule} (${count})\n`;
147
+ }
148
+ }
149
+
150
+ return result;
151
+ }
@@ -0,0 +1,67 @@
1
+ function detectPathSeparator(path: string): "/" | "\\" {
2
+ return path.includes("\\") && !path.includes("/") ? "\\" : "/";
3
+ }
4
+
5
+ function detectPathPrefix(path: string, separator: "/" | "\\"): string {
6
+ if (/^[A-Za-z]:[\\/]/.test(path)) {
7
+ return `${path.slice(0, 2)}${separator}`;
8
+ }
9
+
10
+ if (path.startsWith("\\\\") || path.startsWith("//")) {
11
+ const parts = path.split(/[\\/]+/).filter((part) => part.length > 0);
12
+ if (parts.length >= 2) {
13
+ return `${separator}${separator}${parts[0]}${separator}${parts[1]}${separator}`;
14
+ }
15
+ return `${separator}${separator}`;
16
+ }
17
+
18
+ if (path.startsWith("/") || path.startsWith("\\")) {
19
+ return separator;
20
+ }
21
+
22
+ return "";
23
+ }
24
+
25
+ function joinPathSegments(prefix: string, separator: "/" | "\\", segments: string[]): string {
26
+ if (segments.length === 0) {
27
+ return prefix || "";
28
+ }
29
+
30
+ const joined = segments.join(separator);
31
+ return prefix ? `${prefix}${joined}` : joined;
32
+ }
33
+
34
+ export function compactPath(path: string, maxLength: number): string {
35
+ if (path.length <= maxLength) {
36
+ return path;
37
+ }
38
+
39
+ if (maxLength < 2) {
40
+ return path.slice(0, maxLength);
41
+ }
42
+
43
+ const separator = detectPathSeparator(path);
44
+ const prefix = detectPathPrefix(path, separator);
45
+ const segments = path
46
+ .slice(prefix.length)
47
+ .split(/[\\/]+/)
48
+ .filter((segment) => segment.length > 0);
49
+
50
+ const lastSegment = segments[segments.length - 1] ?? path.slice(-(maxLength - 1));
51
+ const previousSegment = segments[segments.length - 2];
52
+
53
+ const candidates = [
54
+ joinPathSegments(prefix, separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
55
+ joinPathSegments("", separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
56
+ joinPathSegments("", separator, ["…", lastSegment]),
57
+ `…${path.slice(-(maxLength - 1))}`,
58
+ ];
59
+
60
+ for (const candidate of candidates) {
61
+ if (candidate.length <= maxLength) {
62
+ return candidate;
63
+ }
64
+ }
65
+
66
+ return `…${lastSegment.slice(-(maxLength - 1))}`;
67
+ }
@@ -1,76 +1,67 @@
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
- }
1
+ import { compactPath } from "./path-utils.js";
2
+
3
+ interface SearchResult {
4
+ file: string;
5
+ lineNumber: string;
6
+ content: string;
7
+ }
8
+
9
+ export function groupSearchResults(output: string, maxResults = 50): string | null {
10
+ const results: SearchResult[] = [];
11
+ for (const line of output.split("\n")) {
12
+ if (!line.trim()) {
13
+ continue;
14
+ }
15
+ const match = line.match(/^(.+?):(\d+)?:(.+)$/);
16
+ if (!match) {
17
+ continue;
18
+ }
19
+ results.push({
20
+ file: match[1] ?? "unknown",
21
+ lineNumber: match[2] ?? "?",
22
+ content: match[3] ?? "",
23
+ });
24
+ }
25
+
26
+ if (results.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ const byFile = new Map<string, SearchResult[]>();
31
+ for (const result of results) {
32
+ const existing = byFile.get(result.file) ?? [];
33
+ existing.push(result);
34
+ byFile.set(result.file, existing);
35
+ }
36
+
37
+ let outputText = `${results.length} matches in ${byFile.size} files:\n\n`;
38
+ const sortedFiles = Array.from(byFile.entries()).sort((left, right) =>
39
+ left[0].localeCompare(right[0]),
40
+ );
41
+
42
+ let shown = 0;
43
+ for (const [file, matches] of sortedFiles) {
44
+ if (shown >= maxResults) {
45
+ break;
46
+ }
47
+ outputText += `> ${compactPath(file, 50)} (${matches.length} matches):\n`;
48
+ for (const match of matches.slice(0, 10)) {
49
+ let cleaned = match.content.trim();
50
+ if (cleaned.length > 70) {
51
+ cleaned = `${cleaned.slice(0, 67)}...`;
52
+ }
53
+ outputText += ` ${match.lineNumber}: ${cleaned}\n`;
54
+ shown++;
55
+ }
56
+ if (matches.length > 10) {
57
+ outputText += ` +${matches.length - 10} more\n`;
58
+ }
59
+ outputText += "\n";
60
+ }
61
+
62
+ if (results.length > shown) {
63
+ outputText += `... +${results.length - shown} more\n`;
64
+ }
65
+
66
+ return outputText;
67
+ }