pi-rtk-optimizer 0.3.3 → 0.5.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 +102 -67
- package/README.md +292 -290
- package/config/config.example.json +36 -35
- package/package.json +4 -4
- package/src/additional-coverage-test.ts +278 -0
- package/src/boolean-format.ts +3 -0
- package/src/command-rewriter-test.ts +160 -120
- package/src/command-rewriter.ts +594 -585
- package/src/config-modal-test.ts +168 -0
- package/src/config-modal.ts +613 -600
- package/src/config-store.ts +224 -217
- package/src/index-test.ts +54 -0
- package/src/index.ts +410 -289
- package/src/output-compactor-test.ts +500 -158
- package/src/output-compactor.ts +432 -349
- package/src/record-utils.ts +6 -0
- package/src/rewrite-bypass.ts +332 -173
- package/src/rewrite-pipeline-safety.ts +154 -0
- package/src/rewrite-rules.ts +255 -255
- package/src/rtk-command-environment.ts +64 -0
- package/src/runtime-guard-test.ts +42 -50
- package/src/runtime-guard.ts +14 -14
- package/src/techniques/build.ts +155 -155
- package/src/techniques/emoji.ts +91 -0
- package/src/techniques/git.ts +231 -229
- package/src/techniques/index.ts +10 -16
- package/src/techniques/linter.ts +151 -161
- package/src/techniques/path-utils.ts +67 -0
- package/src/techniques/rtk.ts +136 -0
- package/src/techniques/search.ts +67 -76
- package/src/techniques/source.ts +253 -253
- package/src/techniques/test-output.ts +172 -172
- package/src/test-helpers.ts +10 -0
- package/src/tool-execution-sanitizer.ts +69 -0
- package/src/types-shims.d.ts +192 -183
- package/src/types.ts +103 -114
- package/src/zellij-modal.ts +1001 -1001
- package/src/compat-commands.ts +0 -207
package/src/techniques/linter.ts
CHANGED
|
@@ -1,161 +1,151 @@
|
|
|
1
|
-
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/^(?:npx\s+)?
|
|
6
|
-
/^
|
|
7
|
-
/^
|
|
8
|
-
/^
|
|
9
|
-
/^
|
|
10
|
-
/^
|
|
11
|
-
/^
|
|
12
|
-
/^
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (/^
|
|
84
|
-
if (/^
|
|
85
|
-
if (/^
|
|
86
|
-
if (
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
89
|
-
return "
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
result += "
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const RTK_HOOK_WARNING_MESSAGES = [
|
|
2
|
+
"No hook installed — run `rtk init -g` for automatic token savings",
|
|
3
|
+
"Hook outdated — run `rtk init -g` to update",
|
|
4
|
+
] as const;
|
|
5
|
+
|
|
6
|
+
const RTK_HOOK_WARNING_PREFIX_MARKERS = ["[rtk] /!\\", "⚠", "[WARN]"] as const;
|
|
7
|
+
|
|
8
|
+
type HookWarningLineStripResult =
|
|
9
|
+
| {
|
|
10
|
+
removed: false;
|
|
11
|
+
removedLine: false;
|
|
12
|
+
line: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
removed: true;
|
|
16
|
+
removedLine: boolean;
|
|
17
|
+
line: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function outputContainsKnownHookWarning(output: string): boolean {
|
|
21
|
+
return RTK_HOOK_WARNING_MESSAGES.some((message) => output.includes(message));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isQuotedPrefixBoundary(line: string, prefixIndex: number): boolean {
|
|
25
|
+
if (prefixIndex <= 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const charBefore = line[prefixIndex - 1];
|
|
30
|
+
return charBefore === "\"" || charBefore === "'";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findClosestWarningPrefixIndex(line: string, beforeIndex: number): number {
|
|
34
|
+
let closestIndex = -1;
|
|
35
|
+
for (const marker of RTK_HOOK_WARNING_PREFIX_MARKERS) {
|
|
36
|
+
const index = line.lastIndexOf(marker, beforeIndex);
|
|
37
|
+
if (index > closestIndex) {
|
|
38
|
+
closestIndex = index;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return closestIndex;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripHookWarningFromLine(line: string): HookWarningLineStripResult {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
return { removed: false, removedLine: false, line };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (RTK_HOOK_WARNING_MESSAGES.some((message) => trimmed === message)) {
|
|
52
|
+
return { removed: true, removedLine: true, line: "" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const message of RTK_HOOK_WARNING_MESSAGES) {
|
|
56
|
+
const messageIndex = line.indexOf(message);
|
|
57
|
+
if (messageIndex === -1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prefixIndex = findClosestWarningPrefixIndex(line, messageIndex);
|
|
62
|
+
if (prefixIndex === -1) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isQuotedPrefixBoundary(line, prefixIndex)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let removalStart = prefixIndex;
|
|
71
|
+
while (removalStart > 0 && /\s/.test(line[removalStart - 1] ?? "")) {
|
|
72
|
+
removalStart -= 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const removalEnd = messageIndex + message.length;
|
|
76
|
+
const before = line.slice(0, removalStart);
|
|
77
|
+
const after = line.slice(removalEnd);
|
|
78
|
+
|
|
79
|
+
let nextLine = `${before}${after}`;
|
|
80
|
+
if (before.trim() !== "" && after.trim() !== "") {
|
|
81
|
+
nextLine = `${before.trimEnd()}\n${after}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!nextLine.trim()) {
|
|
85
|
+
return { removed: true, removedLine: true, line: "" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { removed: true, removedLine: false, line: nextLine };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { removed: false, removedLine: false, line };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Removes only RTK hook status notices that are not actionable inside Pi.
|
|
96
|
+
* Other RTK warnings should remain visible so the agent can inspect them.
|
|
97
|
+
*/
|
|
98
|
+
export function stripRtkHookWarnings(output: string, _command: string | undefined | null): string | null {
|
|
99
|
+
if (!outputContainsKnownHookWarning(output)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const filteredLines: string[] = [];
|
|
104
|
+
let removedWarning = false;
|
|
105
|
+
let skipImmediateBlankLine = false;
|
|
106
|
+
|
|
107
|
+
for (const line of output.split("\n")) {
|
|
108
|
+
if (skipImmediateBlankLine && line.trim() === "") {
|
|
109
|
+
skipImmediateBlankLine = false;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stripped = stripHookWarningFromLine(line);
|
|
114
|
+
if (stripped.removed) {
|
|
115
|
+
removedWarning = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (stripped.removedLine) {
|
|
119
|
+
skipImmediateBlankLine = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
skipImmediateBlankLine = false;
|
|
124
|
+
filteredLines.push(stripped.line);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!removedWarning) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (filteredLines.length > 0 && filteredLines[0]?.trim() === "") {
|
|
132
|
+
filteredLines.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return filteredLines.join("\n");
|
|
136
|
+
}
|
package/src/techniques/search.ts
CHANGED
|
@@ -1,76 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|