pi-lens 3.7.1 → 3.8.2
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 +217 -0
- package/README.md +706 -619
- package/clients/architect-client.ts +7 -2
- package/clients/ast-grep-client.ts +7 -1
- package/clients/dispatch/plan.ts +10 -4
- package/clients/dispatch/runners/architect.ts +20 -7
- package/clients/dispatch/runners/ast-grep-napi.ts +5 -2
- package/clients/dispatch/runners/ast-grep.ts +29 -18
- package/clients/dispatch/runners/biome.ts +4 -4
- package/clients/dispatch/runners/python-slop.ts +17 -7
- package/clients/dispatch/runners/ruff.ts +4 -4
- package/clients/dispatch/runners/tree-sitter.ts +30 -19
- package/clients/dispatch/runners/ts-slop.ts +17 -7
- package/clients/dispatch/runners/utils/runner-helpers.ts +76 -8
- package/clients/dispatch/utils/format-utils.ts +2 -1
- package/clients/fix-scanners.ts +8 -8
- package/clients/installer/index.ts +19 -1
- package/clients/lsp/index.ts +0 -40
- package/clients/lsp/launch.ts +5 -2
- package/clients/package-root.ts +44 -0
- package/clients/pipeline.ts +179 -8
- package/clients/scan-utils.ts +20 -32
- package/clients/sg-runner.ts +7 -5
- package/clients/source-filter.ts +222 -0
- package/clients/startup-scan.ts +142 -0
- package/clients/todo-scanner.ts +44 -55
- package/clients/tree-sitter-cache.ts +315 -0
- package/clients/tree-sitter-client.ts +208 -52
- package/clients/tree-sitter-fixer.ts +217 -0
- package/clients/tree-sitter-navigator.ts +329 -0
- package/clients/tree-sitter-query-loader.ts +55 -32
- package/commands/booboo.ts +47 -35
- package/default-architect.yaml +76 -87
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/AST_GREP_RULES.md +266 -0
- package/docs/COMPLEXITY_METRICS.md +120 -0
- package/docs/EXCLUSIONS.md +83 -0
- package/docs/LSP_CONFIG.md +240 -0
- package/docs/TREE_SITTER_RULES.md +340 -0
- package/docs/WRITING_NEW_AST_GREP_RULES.md +200 -0
- package/index.ts +209 -86
- package/package.json +13 -4
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +33 -0
- package/rules/ast-grep-rules/rules/array-callback-return.yml +1 -1
- package/rules/ast-grep-rules/rules/constructor-super-js.yml +22 -0
- package/rules/ast-grep-rules/rules/empty-catch-js.yml +45 -0
- package/rules/ast-grep-rules/rules/empty-catch.yml +1 -1
- package/rules/ast-grep-rules/rules/getter-return-js.yml +59 -0
- package/rules/ast-grep-rules/rules/getter-return.yml +1 -1
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +12 -0
- package/rules/ast-grep-rules/rules/jsx-boolean-short-circuit.yml +1 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +14 -0
- package/rules/ast-grep-rules/rules/missed-concurrency-js.yml +25 -0
- package/rules/ast-grep-rules/rules/nested-ternary-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-alert-js.yml +6 -0
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +21 -18
- package/rules/ast-grep-rules/rules/no-array-constructor-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-async-promise-executor-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +1 -1
- package/rules/ast-grep-rules/rules/no-await-in-loop-js.yml +30 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all-js.yml +20 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +1 -1
- package/rules/ast-grep-rules/rules/no-bare-except.yml +1 -1
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +16 -0
- package/rules/ast-grep-rules/rules/no-compare-neg-zero-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +1 -1
- package/rules/ast-grep-rules/rules/no-comparison-to-none.yml +1 -1
- package/rules/ast-grep-rules/rules/no-cond-assign-js.yml +36 -0
- package/rules/ast-grep-rules/rules/no-cond-assign.yml +1 -1
- package/rules/ast-grep-rules/rules/no-constant-condition-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-constant-condition.yml +1 -1
- package/rules/ast-grep-rules/rules/no-constructor-return-js.yml +28 -0
- package/rules/ast-grep-rules/rules/no-constructor-return.yml +1 -1
- package/rules/ast-grep-rules/rules/no-discarded-error-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-discarded-error.yml +25 -0
- package/rules/ast-grep-rules/rules/no-dupe-args-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-dupe-keys-js.yml +73 -0
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets-js.yml +17 -0
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-inner-html-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-insecure-randomness-js.yml +20 -0
- package/rules/ast-grep-rules/rules/no-insecure-randomness.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +11 -0
- package/rules/ast-grep-rules/rules/no-nan-comparison-js.yml +22 -0
- package/rules/ast-grep-rules/rules/no-nan-comparison.yml +22 -0
- package/rules/ast-grep-rules/rules/no-new-symbol-js.yml +8 -0
- package/rules/ast-grep-rules/rules/no-new-wrappers-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-open-redirect-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins.yml +1 -1
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code.yml +1 -1
- package/rules/ast-grep-rules/rules/no-throw-string-js.yml +12 -0
- package/rules/ast-grep-rules/rules/no-throw-string.yml +1 -1
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +10 -0
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +10 -0
- package/rules/ast-grep-rules/rules/toctou-js.yml +112 -0
- package/rules/ast-grep-rules/rules/toctou.yml +1 -1
- package/rules/ast-grep-rules/rules/unchecked-sync-fs-js.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-sync-fs.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-js.yml +31 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +48 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-ruby.yml +47 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call.yml +31 -0
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +15 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +47 -0
- package/rules/tree-sitter-queries/go/go-defer-in-loop.yml +47 -0
- package/rules/tree-sitter-queries/go/go-hardcoded-secrets.yml +54 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +1 -1
- package/rules/tree-sitter-queries/python/python-debugger.yml +46 -0
- package/rules/tree-sitter-queries/python/python-empty-except.yml +48 -0
- package/rules/tree-sitter-queries/python/python-hardcoded-secrets.yml +44 -0
- package/rules/tree-sitter-queries/python/python-mutable-class-attr.yml +57 -0
- package/rules/tree-sitter-queries/python/python-print-statement.yml +53 -0
- package/rules/tree-sitter-queries/python/python-raise-string.yml +38 -0
- package/rules/tree-sitter-queries/python/python-unsafe-regex.yml +58 -0
- package/rules/tree-sitter-queries/ruby/ruby-debugger.yml +44 -0
- package/rules/tree-sitter-queries/ruby/ruby-empty-rescue.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +43 -0
- package/rules/tree-sitter-queries/ruby/ruby-hardcoded-secrets.yml +40 -0
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +48 -0
- package/rules/tree-sitter-queries/ruby/ruby-puts-statement.yml +52 -0
- package/rules/tree-sitter-queries/ruby/ruby-rescue-exception.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-unsafe-regex.yml +49 -0
- package/rules/tree-sitter-queries/rust/rust-clone-in-loop.yml +49 -0
- package/rules/tree-sitter-queries/rust/rust-unwrap.yml +45 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +3 -3
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +13 -27
- package/rules/tree-sitter-queries/typescript/injections.scm +40 -0
- package/rules/tree-sitter-queries/typescript/no-console-in-tests.yml +52 -0
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +71 -0
- package/rules/tree-sitter-queries/typescript/variable-shadowing.yml +51 -0
- package/scripts/download-grammars.ts +78 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source File Filter — Deduplicates source files by detecting build artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Problem: When scanning a codebase, we encounter both source files and their
|
|
5
|
+
* compiled/transpiled outputs (TypeScript → JavaScript, Vue → JavaScript, etc.).
|
|
6
|
+
* Scanning both wastes time and produces duplicate findings.
|
|
7
|
+
*
|
|
8
|
+
* Solution: For each file, check if a "higher precedence" source sibling exists.
|
|
9
|
+
* If yes, skip the file as a build artifact. If no, keep it as hand-written source.
|
|
10
|
+
*
|
|
11
|
+
* Supported ecosystems:
|
|
12
|
+
* - TypeScript: .ts shadows .js, .tsx shadows .jsx
|
|
13
|
+
* - Vue/Svelte: .vue/.svelte shadows .js
|
|
14
|
+
* - CoffeeScript: .coffee shadows .js
|
|
15
|
+
*
|
|
16
|
+
* Files without higher-precedence siblings are always kept (hand-written JS, Python,
|
|
17
|
+
* Go, Rust, etc.).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mapping of file extension to the extensions it shadows (build artifacts).
|
|
25
|
+
* Order matters: first entry has highest precedence.
|
|
26
|
+
*/
|
|
27
|
+
export const SOURCE_PRECEDENCE: Record<string, string[]> = {
|
|
28
|
+
".ts": [".js", ".mjs", ".cjs"],
|
|
29
|
+
".tsx": [".jsx", ".js", ".mjs", ".cjs"],
|
|
30
|
+
".vue": [".js", ".mjs"],
|
|
31
|
+
".svelte": [".js", ".mjs"],
|
|
32
|
+
".coffee": [".js"],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* All extensions that could be source or artifacts, in precedence order.
|
|
37
|
+
*/
|
|
38
|
+
export const ALL_SCANNABLE_EXTENSIONS = [
|
|
39
|
+
".ts",
|
|
40
|
+
".tsx",
|
|
41
|
+
".js",
|
|
42
|
+
".jsx",
|
|
43
|
+
".mjs",
|
|
44
|
+
".cjs",
|
|
45
|
+
".vue",
|
|
46
|
+
".svelte",
|
|
47
|
+
".coffee",
|
|
48
|
+
".py",
|
|
49
|
+
".go",
|
|
50
|
+
".rs",
|
|
51
|
+
".rb",
|
|
52
|
+
".rake",
|
|
53
|
+
".gemspec",
|
|
54
|
+
".ru",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract the basename (filename without extension) from a path.
|
|
59
|
+
*/
|
|
60
|
+
function getBasename(filePath: string): string {
|
|
61
|
+
const ext = path.extname(filePath);
|
|
62
|
+
return path.basename(filePath, ext);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the directory of a file path.
|
|
67
|
+
*/
|
|
68
|
+
function getDir(filePath: string): string {
|
|
69
|
+
return path.dirname(filePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a file has a higher-precedence source sibling.
|
|
74
|
+
* Returns the shadowing source file path if found, null otherwise.
|
|
75
|
+
*/
|
|
76
|
+
export function findSourceSibling(filePath: string): string | null {
|
|
77
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
78
|
+
const dir = getDir(filePath);
|
|
79
|
+
const base = getBasename(filePath);
|
|
80
|
+
|
|
81
|
+
// Find which precedence group this extension belongs to
|
|
82
|
+
for (const [sourceExt, shadowedExts] of Object.entries(SOURCE_PRECEDENCE)) {
|
|
83
|
+
if (shadowedExts.includes(ext)) {
|
|
84
|
+
// This file could be shadowed by a source file with sourceExt
|
|
85
|
+
const siblingPath = path.join(dir, base + sourceExt);
|
|
86
|
+
if (fs.existsSync(siblingPath)) {
|
|
87
|
+
return siblingPath;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a file is a build artifact (has a source sibling).
|
|
97
|
+
*/
|
|
98
|
+
export function isBuildArtifact(filePath: string): boolean {
|
|
99
|
+
return findSourceSibling(filePath) !== null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Filter a list of files, removing build artifacts that have source siblings.
|
|
104
|
+
* Returns de-duplicated list keeping only highest-precedence sources.
|
|
105
|
+
*/
|
|
106
|
+
export function filterSourceFiles(filePaths: string[]): string[] {
|
|
107
|
+
// Track which files we're keeping and why we're skipping others
|
|
108
|
+
const keep: string[] = [];
|
|
109
|
+
const skipReasons = new Map<string, string>(); // skipped file -> kept source
|
|
110
|
+
|
|
111
|
+
for (const filePath of filePaths) {
|
|
112
|
+
const sourceSibling = findSourceSibling(filePath);
|
|
113
|
+
if (sourceSibling) {
|
|
114
|
+
// This is a build artifact, skip it
|
|
115
|
+
skipReasons.set(filePath, sourceSibling);
|
|
116
|
+
} else {
|
|
117
|
+
// No higher-precedence source, keep it
|
|
118
|
+
keep.push(filePath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return keep;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recursively collect all source files in a directory, excluding build artifacts.
|
|
127
|
+
*
|
|
128
|
+
* @param dir - Directory to scan
|
|
129
|
+
* @param options - Optional configuration
|
|
130
|
+
* @returns Array of absolute file paths that are source files (not build artifacts)
|
|
131
|
+
*/
|
|
132
|
+
export function collectSourceFiles(
|
|
133
|
+
dir: string,
|
|
134
|
+
options?: {
|
|
135
|
+
/** Additional directory names to exclude (merged with defaults) */
|
|
136
|
+
excludeDirs?: string[];
|
|
137
|
+
/** File extensions to consider (defaults to ALL_SCANNABLE_EXTENSIONS) */
|
|
138
|
+
extensions?: string[];
|
|
139
|
+
/** Whether to follow symlinks (default: false) */
|
|
140
|
+
followSymlinks?: boolean;
|
|
141
|
+
},
|
|
142
|
+
): string[] {
|
|
143
|
+
const excludeDirs = new Set([
|
|
144
|
+
"node_modules",
|
|
145
|
+
".git",
|
|
146
|
+
"dist",
|
|
147
|
+
"build",
|
|
148
|
+
".next",
|
|
149
|
+
"coverage",
|
|
150
|
+
"__pycache__",
|
|
151
|
+
".cache",
|
|
152
|
+
"target", // Rust
|
|
153
|
+
"out",
|
|
154
|
+
"*.dSYM", // macOS debug symbols
|
|
155
|
+
...(options?.excludeDirs || []),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const extensions = new Set(options?.extensions || ALL_SCANNABLE_EXTENSIONS);
|
|
159
|
+
|
|
160
|
+
const files: string[] = [];
|
|
161
|
+
|
|
162
|
+
function scan(currentDir: string) {
|
|
163
|
+
let entries: fs.Dirent[] = [];
|
|
164
|
+
try {
|
|
165
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
166
|
+
} catch {
|
|
167
|
+
return; // Permission denied or directory doesn't exist
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
172
|
+
|
|
173
|
+
if (entry.isDirectory()) {
|
|
174
|
+
if (excludeDirs.has(entry.name)) continue;
|
|
175
|
+
if (!options?.followSymlinks && entry.isSymbolicLink()) continue;
|
|
176
|
+
scan(fullPath);
|
|
177
|
+
} else if (entry.isFile()) {
|
|
178
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
179
|
+
if (!extensions.has(ext)) continue;
|
|
180
|
+
|
|
181
|
+
// Skip if this is a build artifact
|
|
182
|
+
if (isBuildArtifact(fullPath)) continue;
|
|
183
|
+
|
|
184
|
+
files.push(fullPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
scan(path.resolve(dir));
|
|
190
|
+
return files;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get statistics about source file filtering for debugging/monitoring.
|
|
195
|
+
*/
|
|
196
|
+
export function getFilterStats(
|
|
197
|
+
allFiles: string[],
|
|
198
|
+
filteredFiles: string[],
|
|
199
|
+
): {
|
|
200
|
+
total: number;
|
|
201
|
+
kept: number;
|
|
202
|
+
skipped: number;
|
|
203
|
+
byType: Record<string, number>;
|
|
204
|
+
} {
|
|
205
|
+
const skipped = allFiles.length - filteredFiles.length;
|
|
206
|
+
const byType: Record<string, number> = {};
|
|
207
|
+
|
|
208
|
+
// Count what we skipped
|
|
209
|
+
for (const file of allFiles) {
|
|
210
|
+
if (!filteredFiles.includes(file)) {
|
|
211
|
+
const ext = path.extname(file).toLowerCase();
|
|
212
|
+
byType[ext] = (byType[ext] || 0) + 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
total: allFiles.length,
|
|
218
|
+
kept: filteredFiles.length,
|
|
219
|
+
skipped,
|
|
220
|
+
byType,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup scan safety — gates eager cache warmups to real project roots.
|
|
3
|
+
*
|
|
4
|
+
* Prevents pi-lens from scanning $HOME or generic directories at session
|
|
5
|
+
* start, which would hang or produce meaningless results.
|
|
6
|
+
*
|
|
7
|
+
* Credit: alexx-ftw (PR #1)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { EXCLUDED_DIRS } from "./file-utils.js";
|
|
14
|
+
|
|
15
|
+
export const PROJECT_ROOT_MARKERS = [
|
|
16
|
+
".git",
|
|
17
|
+
"package.json",
|
|
18
|
+
"pyproject.toml",
|
|
19
|
+
"Cargo.toml",
|
|
20
|
+
"go.mod",
|
|
21
|
+
"composer.json",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const MAX_STARTUP_SOURCE_FILES = 2000;
|
|
25
|
+
|
|
26
|
+
const SOURCE_FILE_PATTERN = /\.(ts|tsx|js|jsx|py|go|rs|rb)$/;
|
|
27
|
+
|
|
28
|
+
export interface StartupScanContext {
|
|
29
|
+
cwd: string;
|
|
30
|
+
scanRoot: string;
|
|
31
|
+
projectRoot: string | null;
|
|
32
|
+
canWarmCaches: boolean;
|
|
33
|
+
reason?: "home-dir" | "no-project-root" | "too-many-source-files";
|
|
34
|
+
sourceFileCount?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StartupScanOptions {
|
|
38
|
+
homeDir?: string;
|
|
39
|
+
maxSourceFiles?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function findNearestProjectRoot(startDir: string): string | null {
|
|
43
|
+
let current = path.resolve(startDir);
|
|
44
|
+
while (true) {
|
|
45
|
+
if (
|
|
46
|
+
PROJECT_ROOT_MARKERS.some((marker) =>
|
|
47
|
+
fs.existsSync(path.join(current, marker)),
|
|
48
|
+
)
|
|
49
|
+
) {
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current) return null;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function countSourceFilesWithinLimit(
|
|
59
|
+
dir: string,
|
|
60
|
+
limit: number,
|
|
61
|
+
): number {
|
|
62
|
+
let count = 0;
|
|
63
|
+
const stack = [path.resolve(dir)];
|
|
64
|
+
|
|
65
|
+
while (stack.length > 0) {
|
|
66
|
+
const current = stack.pop();
|
|
67
|
+
if (!current) continue;
|
|
68
|
+
|
|
69
|
+
let entries: fs.Dirent[] = [];
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
79
|
+
stack.push(path.join(current, entry.name));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) {
|
|
83
|
+
count += 1;
|
|
84
|
+
if (count > limit) return count;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveStartupScanContext(
|
|
92
|
+
cwd: string,
|
|
93
|
+
options: StartupScanOptions = {},
|
|
94
|
+
): StartupScanContext {
|
|
95
|
+
const resolvedCwd = path.resolve(cwd);
|
|
96
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
97
|
+
const maxSourceFiles = options.maxSourceFiles ?? MAX_STARTUP_SOURCE_FILES;
|
|
98
|
+
const projectRoot = findNearestProjectRoot(resolvedCwd);
|
|
99
|
+
|
|
100
|
+
if (!projectRoot) {
|
|
101
|
+
return {
|
|
102
|
+
cwd: resolvedCwd,
|
|
103
|
+
scanRoot: resolvedCwd,
|
|
104
|
+
projectRoot: null,
|
|
105
|
+
canWarmCaches: false,
|
|
106
|
+
reason: resolvedCwd === homeDir ? "home-dir" : "no-project-root",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (path.resolve(projectRoot) === homeDir) {
|
|
111
|
+
return {
|
|
112
|
+
cwd: resolvedCwd,
|
|
113
|
+
scanRoot: projectRoot,
|
|
114
|
+
projectRoot,
|
|
115
|
+
canWarmCaches: false,
|
|
116
|
+
reason: "home-dir",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sourceFileCount = countSourceFilesWithinLimit(
|
|
121
|
+
projectRoot,
|
|
122
|
+
maxSourceFiles,
|
|
123
|
+
);
|
|
124
|
+
if (sourceFileCount > maxSourceFiles) {
|
|
125
|
+
return {
|
|
126
|
+
cwd: resolvedCwd,
|
|
127
|
+
scanRoot: projectRoot,
|
|
128
|
+
projectRoot,
|
|
129
|
+
canWarmCaches: false,
|
|
130
|
+
reason: "too-many-source-files",
|
|
131
|
+
sourceFileCount,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
cwd: resolvedCwd,
|
|
137
|
+
scanRoot: projectRoot,
|
|
138
|
+
projectRoot,
|
|
139
|
+
canWarmCaches: true,
|
|
140
|
+
sourceFileCount,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/clients/todo-scanner.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TODO Scanner for pi-local.
|
|
3
3
|
*
|
|
4
|
-
* Scans codebase for TODO, FIXME, HACK, XXX, and
|
|
5
|
-
* Helps
|
|
4
|
+
* Scans codebase for TODO, FIXME, HACK, XXX, and BUG annotations.
|
|
5
|
+
* Helps understand what's already flagged as problematic or incomplete.
|
|
6
6
|
*
|
|
7
7
|
* No dependencies required — uses regex scanning.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import { collectSourceFiles } from "./source-filter.js";
|
|
12
13
|
|
|
13
14
|
// --- Types ---
|
|
14
15
|
|
|
15
16
|
export interface TodoItem {
|
|
16
|
-
type: "TODO" | "FIXME" | "HACK" | "XXX" | "
|
|
17
|
+
type: "TODO" | "FIXME" | "HACK" | "XXX" | "BUG";
|
|
17
18
|
message: string;
|
|
18
19
|
file: string;
|
|
19
20
|
line: number;
|
|
@@ -29,8 +30,12 @@ export interface TodoScanResult {
|
|
|
29
30
|
// --- Scanner ---
|
|
30
31
|
|
|
31
32
|
export class TodoScanner {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Pattern matches actionable annotations only.
|
|
35
|
+
* Excludes NOTE and DEPRECATED — these are documentation, not work items.
|
|
36
|
+
* Case-sensitive to avoid matching "Note:" in prose.
|
|
37
|
+
*/
|
|
38
|
+
private readonly pattern = /\b(TODO|FIXME|HACK|XXX|BUG)\b\s*[(:]?\s*(.+)/g;
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* Check if a match position is inside a comment context.
|
|
@@ -80,7 +85,7 @@ export class TodoScanner {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
|
-
* Scan a single file for TODOs
|
|
88
|
+
* Scan a single file for TODOs.
|
|
84
89
|
*/
|
|
85
90
|
scanFile(filePath: string): TodoItem[] {
|
|
86
91
|
const absolutePath = path.resolve(filePath);
|
|
@@ -98,7 +103,7 @@ export class TodoScanner {
|
|
|
98
103
|
// Skip matches that aren't inside comments
|
|
99
104
|
if (!this.isInComment(line, match.index ?? 0)) continue;
|
|
100
105
|
|
|
101
|
-
const type = match[1]
|
|
106
|
+
const type = match[1] as TodoItem["type"];
|
|
102
107
|
const message = (match[2] || "").trim().replace(/\s*\*\/\s*$/, ""); // Strip closing comment
|
|
103
108
|
|
|
104
109
|
items.push({
|
|
@@ -115,52 +120,42 @@ export class TodoScanner {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
118
|
-
* Scan a
|
|
123
|
+
* Scan a list of pre-filtered files (recommended — uses source-filter module).
|
|
124
|
+
* Callers should use collectSourceFiles() to get deduplicated source files.
|
|
119
125
|
*/
|
|
120
|
-
|
|
121
|
-
dirPath: string,
|
|
122
|
-
extensions = [".ts", ".tsx", ".js", ".jsx", ".py"],
|
|
123
|
-
): TodoScanResult {
|
|
126
|
+
scanFiles(filePaths: string[]): TodoScanResult {
|
|
124
127
|
const items: TodoItem[] = [];
|
|
125
128
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"build",
|
|
142
|
-
".next",
|
|
143
|
-
"coverage",
|
|
144
|
-
].includes(entry.name)
|
|
145
|
-
)
|
|
146
|
-
continue;
|
|
147
|
-
scan(fullPath);
|
|
148
|
-
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
149
|
-
// Skip this scanner file — its own type literals and regex cause false positives
|
|
150
|
-
if (
|
|
151
|
-
entry.name === "todo-scanner.ts" ||
|
|
152
|
-
entry.name === "todo-scanner.js"
|
|
153
|
-
)
|
|
154
|
-
continue;
|
|
155
|
-
// Skip test files — intentional annotations are test fixtures, not work items
|
|
156
|
-
if (/\.(test|spec)\.[jt]sx?$/.test(entry.name)) continue;
|
|
157
|
-
items.push(...this.scanFile(fullPath));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
};
|
|
129
|
+
for (const filePath of filePaths) {
|
|
130
|
+
// Skip this scanner file — its own type literals and regex cause false positives
|
|
131
|
+
if (
|
|
132
|
+
filePath.endsWith("todo-scanner.ts") ||
|
|
133
|
+
filePath.endsWith("todo-scanner.js")
|
|
134
|
+
)
|
|
135
|
+
continue;
|
|
136
|
+
// Skip test files — intentional annotations are test fixtures, not work items
|
|
137
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(filePath)) continue;
|
|
138
|
+
|
|
139
|
+
items.push(...this.scanFile(filePath));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.groupResults(items);
|
|
143
|
+
}
|
|
161
144
|
|
|
162
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Scan a directory recursively using the source-filter module to exclude build artifacts.
|
|
147
|
+
* This is the preferred entry point for new callers.
|
|
148
|
+
*/
|
|
149
|
+
scanDirectory(dirPath: string): TodoScanResult {
|
|
150
|
+
// Use source-filter to collect only source files (no build artifacts)
|
|
151
|
+
const sourceFiles = collectSourceFiles(dirPath);
|
|
152
|
+
return this.scanFiles(sourceFiles);
|
|
153
|
+
}
|
|
163
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Group scan results by type and file.
|
|
157
|
+
*/
|
|
158
|
+
private groupResults(items: TodoItem[]): TodoScanResult {
|
|
164
159
|
// Group by type
|
|
165
160
|
const byType = new Map<string, TodoItem[]>();
|
|
166
161
|
for (const item of items) {
|
|
@@ -181,7 +176,7 @@ export class TodoScanner {
|
|
|
181
176
|
}
|
|
182
177
|
|
|
183
178
|
/**
|
|
184
|
-
* Format scan results for LLM consumption
|
|
179
|
+
* Format scan results for LLM consumption.
|
|
185
180
|
*/
|
|
186
181
|
formatResult(result: TodoScanResult, maxItems = 30): string {
|
|
187
182
|
if (result.items.length === 0) return "";
|
|
@@ -203,10 +198,8 @@ export class TodoScanner {
|
|
|
203
198
|
"FIXME",
|
|
204
199
|
"HACK",
|
|
205
200
|
"BUG",
|
|
206
|
-
"DEPRECATED",
|
|
207
201
|
"TODO",
|
|
208
202
|
"XXX",
|
|
209
|
-
"NOTE",
|
|
210
203
|
];
|
|
211
204
|
const sorted = [...result.items].sort((a, b) => {
|
|
212
205
|
const aIdx = priorityOrder.indexOf(a.type);
|
|
@@ -234,14 +227,10 @@ export class TodoScanner {
|
|
|
234
227
|
return "🟠";
|
|
235
228
|
case "BUG":
|
|
236
229
|
return "🐛";
|
|
237
|
-
case "DEPRECATED":
|
|
238
|
-
return "⚠️";
|
|
239
230
|
case "TODO":
|
|
240
231
|
return "📝";
|
|
241
232
|
case "XXX":
|
|
242
233
|
return "❌";
|
|
243
|
-
case "NOTE":
|
|
244
|
-
return "ℹ️";
|
|
245
234
|
default:
|
|
246
235
|
return "•";
|
|
247
236
|
}
|