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
|
@@ -281,6 +281,16 @@ async function verifyToolBinary(binPath: string): Promise<boolean> {
|
|
|
281
281
|
/**
|
|
282
282
|
* Install an npm package tool
|
|
283
283
|
*/
|
|
284
|
+
/**
|
|
285
|
+
* Packages that require postinstall scripts to download native binaries.
|
|
286
|
+
* All others get --ignore-scripts to prevent arbitrary code execution during install.
|
|
287
|
+
*/
|
|
288
|
+
const NEEDS_POSTINSTALL = new Set([
|
|
289
|
+
"@biomejs/biome",
|
|
290
|
+
"@ast-grep/napi",
|
|
291
|
+
"esbuild",
|
|
292
|
+
]);
|
|
293
|
+
|
|
284
294
|
async function installNpmTool(
|
|
285
295
|
packageName: string,
|
|
286
296
|
binaryName: string,
|
|
@@ -311,7 +321,15 @@ async function installNpmTool(
|
|
|
311
321
|
: isWindows
|
|
312
322
|
? "npm.cmd"
|
|
313
323
|
: "npm";
|
|
314
|
-
|
|
324
|
+
// Use --ignore-scripts unless the package explicitly needs postinstall
|
|
325
|
+
// (e.g. biome downloads a platform-specific native binary via postinstall).
|
|
326
|
+
const needsScripts = NEEDS_POSTINSTALL.has(
|
|
327
|
+
packageName.split("@")[0] ?? packageName,
|
|
328
|
+
);
|
|
329
|
+
const installArgs = needsScripts
|
|
330
|
+
? ["install", packageName]
|
|
331
|
+
: ["install", "--ignore-scripts", packageName];
|
|
332
|
+
const proc = spawn(pm, installArgs, {
|
|
315
333
|
cwd: TOOLS_DIR,
|
|
316
334
|
stdio: ["ignore", "pipe", "pipe"],
|
|
317
335
|
shell: isWindows, // Required for .cmd files on Windows
|
package/clients/lsp/index.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - Resource cleanup
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Effect } from "effect";
|
|
12
11
|
import type { LSPClientInfo } from "./client.js";
|
|
13
12
|
import { createLSPClient } from "./client.js";
|
|
14
13
|
import { getServersForFileWithConfig } from "./config.js";
|
|
@@ -339,45 +338,6 @@ export class LSPService {
|
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
340
|
|
|
342
|
-
// --- Effect Integration ---
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Effect wrapper for LSP operations
|
|
346
|
-
*/
|
|
347
|
-
export function lspEffect(service: LSPService) {
|
|
348
|
-
return {
|
|
349
|
-
openFile: (filePath: string, content: string) =>
|
|
350
|
-
Effect.tryPromise({
|
|
351
|
-
try: () => service.openFile(filePath, content),
|
|
352
|
-
catch: (err) => err as Error,
|
|
353
|
-
}),
|
|
354
|
-
|
|
355
|
-
updateFile: (filePath: string, content: string) =>
|
|
356
|
-
Effect.tryPromise({
|
|
357
|
-
try: () => service.updateFile(filePath, content),
|
|
358
|
-
catch: (err) => err as Error,
|
|
359
|
-
}),
|
|
360
|
-
|
|
361
|
-
getDiagnostics: (filePath: string) =>
|
|
362
|
-
Effect.tryPromise({
|
|
363
|
-
try: () => service.getDiagnostics(filePath),
|
|
364
|
-
catch: (err) => err as Error,
|
|
365
|
-
}),
|
|
366
|
-
|
|
367
|
-
hasLSP: (filePath: string) =>
|
|
368
|
-
Effect.tryPromise({
|
|
369
|
-
try: () => service.hasLSP(filePath),
|
|
370
|
-
catch: (err) => err as Error,
|
|
371
|
-
}),
|
|
372
|
-
|
|
373
|
-
shutdown: () =>
|
|
374
|
-
Effect.tryPromise({
|
|
375
|
-
try: () => service.shutdown(),
|
|
376
|
-
catch: (err) => err as Error,
|
|
377
|
-
}),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
341
|
// --- Singleton Instance ---
|
|
382
342
|
|
|
383
343
|
let globalLSPService: LSPService | null = null;
|
package/clients/lsp/launch.ts
CHANGED
|
@@ -328,7 +328,8 @@ export async function launchViaPackageManager(
|
|
|
328
328
|
// For npx on Windows, use shell mode with the full command string
|
|
329
329
|
if (isWin) {
|
|
330
330
|
const argsStr = args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ");
|
|
331
|
-
|
|
331
|
+
// --no prevents silent download of uncached packages
|
|
332
|
+
const shellCommand = `npx --no ${packageName}${argsStr ? ` ${argsStr}` : ""}`;
|
|
332
333
|
|
|
333
334
|
const cwd = String(options.cwd ?? process.cwd());
|
|
334
335
|
const env = { ...process.env, ...options.env };
|
|
@@ -393,7 +394,9 @@ export async function launchViaPackageManager(
|
|
|
393
394
|
};
|
|
394
395
|
}
|
|
395
396
|
|
|
396
|
-
|
|
397
|
+
// --no prevents silent download of uncached packages; user must have
|
|
398
|
+
// already installed the LSP server via the interactive-install flow.
|
|
399
|
+
return launchLSP("npx", ["--no", packageName, ...args], options);
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const packageRootCache = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the installed package root for the current module.
|
|
9
|
+
* Walks upward from the caller until it finds the nearest package.json.
|
|
10
|
+
*
|
|
11
|
+
* This is the correct alternative to process.cwd() for resolving
|
|
12
|
+
* pi-lens's own assets (rules, grammars, configs) when installed
|
|
13
|
+
* globally — cwd is the user's project, not the extension root.
|
|
14
|
+
*
|
|
15
|
+
* Credit: alexx-ftw (PR #1)
|
|
16
|
+
*/
|
|
17
|
+
export function getPackageRoot(importMetaUrl: string): string {
|
|
18
|
+
const cached = packageRootCache.get(importMetaUrl);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
|
|
21
|
+
let current = path.dirname(fileURLToPath(importMetaUrl));
|
|
22
|
+
while (true) {
|
|
23
|
+
if (fs.existsSync(path.join(current, "package.json"))) {
|
|
24
|
+
packageRootCache.set(importMetaUrl, current);
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(current);
|
|
28
|
+
if (parent === current) {
|
|
29
|
+
packageRootCache.set(importMetaUrl, current);
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a path relative to the installed package root.
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePackagePath(
|
|
40
|
+
importMetaUrl: string,
|
|
41
|
+
...segments: string[]
|
|
42
|
+
): string {
|
|
43
|
+
return path.join(getPackageRoot(importMetaUrl), ...segments);
|
|
44
|
+
}
|
package/clients/pipeline.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Runs sequentially on every file write/edit:
|
|
6
6
|
* 1. Secrets scan (blocking — early exit)
|
|
7
7
|
* 2. Auto-format (Biome, Prettier, Ruff, gofmt, etc.)
|
|
8
|
-
* 3. Auto-fix (Biome --write, Ruff --fix)
|
|
8
|
+
* 3. Auto-fix (Biome --write, Ruff --fix, ESLint --fix)
|
|
9
9
|
* 4. LSP file sync (open/update in LSP servers)
|
|
10
10
|
* 5. Dispatch lint (type errors, security rules)
|
|
11
11
|
* 6. Test runner (run corresponding test file)
|
|
@@ -17,11 +17,13 @@ import * as path from "node:path";
|
|
|
17
17
|
import type { BiomeClient } from "./biome-client.js";
|
|
18
18
|
import { dispatchLintWithResult } from "./dispatch/integration.js";
|
|
19
19
|
import type { PiAgentAPI } from "./dispatch/types.js";
|
|
20
|
+
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
20
21
|
import type { FormatService } from "./format-service.js";
|
|
21
22
|
import { logLatency } from "./latency-logger.js";
|
|
22
23
|
import { getLSPService } from "./lsp/index.js";
|
|
23
24
|
import type { MetricsClient } from "./metrics-client.js";
|
|
24
25
|
import type { RuffClient } from "./ruff-client.js";
|
|
26
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
25
27
|
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
26
28
|
import type { TestRunnerClient } from "./test-runner-client.js";
|
|
27
29
|
|
|
@@ -49,6 +51,12 @@ export interface PipelineDeps {
|
|
|
49
51
|
export interface PipelineResult {
|
|
50
52
|
/** Text to append to tool_result content */
|
|
51
53
|
output: string;
|
|
54
|
+
/**
|
|
55
|
+
* Cascade diagnostics (errors in OTHER files caused by this edit).
|
|
56
|
+
* Intentionally NOT included in output — surfaced at turn_end instead
|
|
57
|
+
* so mid-refactor intermediate errors don't derail the agent.
|
|
58
|
+
*/
|
|
59
|
+
cascadeOutput?: string;
|
|
52
60
|
/** True if secrets found — block the agent */
|
|
53
61
|
isError: boolean;
|
|
54
62
|
/** True if file was modified by format/autofix */
|
|
@@ -90,6 +98,109 @@ function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
|
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
// --- ESLint autofix helpers ---
|
|
102
|
+
|
|
103
|
+
const ESLINT_CONFIGS = [
|
|
104
|
+
".eslintrc",
|
|
105
|
+
".eslintrc.js",
|
|
106
|
+
".eslintrc.cjs",
|
|
107
|
+
".eslintrc.json",
|
|
108
|
+
".eslintrc.yaml",
|
|
109
|
+
".eslintrc.yml",
|
|
110
|
+
"eslint.config.js",
|
|
111
|
+
"eslint.config.mjs",
|
|
112
|
+
"eslint.config.cjs",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const JSTS_EXTS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
|
|
116
|
+
|
|
117
|
+
function isJsTs(filePath: string): boolean {
|
|
118
|
+
return JSTS_EXTS.has(path.extname(filePath).toLowerCase());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasEslintConfig(cwd: string): boolean {
|
|
122
|
+
for (const cfg of ESLINT_CONFIGS) {
|
|
123
|
+
if (nodeFs.existsSync(path.join(cwd, cfg))) return true;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const pkg = JSON.parse(
|
|
127
|
+
nodeFs.readFileSync(path.join(cwd, "package.json"), "utf-8"),
|
|
128
|
+
);
|
|
129
|
+
if (pkg.eslintConfig) return true;
|
|
130
|
+
} catch {}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let _eslintAvailable: boolean | null = null;
|
|
135
|
+
let _eslintBin: string | null = null;
|
|
136
|
+
|
|
137
|
+
function findEslintBin(cwd: string): string {
|
|
138
|
+
const isWin = process.platform === "win32";
|
|
139
|
+
const local = path.join(
|
|
140
|
+
cwd,
|
|
141
|
+
"node_modules",
|
|
142
|
+
".bin",
|
|
143
|
+
isWin ? "eslint.cmd" : "eslint",
|
|
144
|
+
);
|
|
145
|
+
if (nodeFs.existsSync(local)) return local;
|
|
146
|
+
return "eslint";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run eslint --fix on a file. Returns number of fixable issues resolved,
|
|
151
|
+
* or 0 if ESLint is not configured / not available.
|
|
152
|
+
*/
|
|
153
|
+
async function tryEslintFix(filePath: string, cwd: string): Promise<number> {
|
|
154
|
+
if (!hasEslintConfig(cwd)) return 0;
|
|
155
|
+
if (_eslintAvailable === false) return 0;
|
|
156
|
+
if (_eslintAvailable === null) {
|
|
157
|
+
const candidate = findEslintBin(cwd);
|
|
158
|
+
const check = await safeSpawnAsync(candidate, ["--version"], {
|
|
159
|
+
timeout: 5000,
|
|
160
|
+
cwd,
|
|
161
|
+
});
|
|
162
|
+
_eslintAvailable = !check.error && check.status === 0;
|
|
163
|
+
if (_eslintAvailable) _eslintBin = candidate;
|
|
164
|
+
}
|
|
165
|
+
if (!_eslintAvailable || !_eslintBin) return 0;
|
|
166
|
+
const cmd = _eslintBin;
|
|
167
|
+
// --fix-dry-run returns JSON with fixable counts without writing to disk.
|
|
168
|
+
// Use it to get the real count, then apply with --fix only if needed.
|
|
169
|
+
const dry = await safeSpawnAsync(
|
|
170
|
+
cmd,
|
|
171
|
+
[
|
|
172
|
+
"--fix-dry-run",
|
|
173
|
+
"--format",
|
|
174
|
+
"json",
|
|
175
|
+
"--no-error-on-unmatched-pattern",
|
|
176
|
+
filePath,
|
|
177
|
+
],
|
|
178
|
+
{ timeout: 30000, cwd },
|
|
179
|
+
);
|
|
180
|
+
if (dry.status === 2) return 0;
|
|
181
|
+
let fixableCount = 0;
|
|
182
|
+
try {
|
|
183
|
+
const results: Array<{
|
|
184
|
+
fixableErrorCount?: number;
|
|
185
|
+
fixableWarningCount?: number;
|
|
186
|
+
}> = JSON.parse(dry.stdout);
|
|
187
|
+
fixableCount = results.reduce(
|
|
188
|
+
(sum, r) =>
|
|
189
|
+
sum + (r.fixableErrorCount ?? 0) + (r.fixableWarningCount ?? 0),
|
|
190
|
+
0,
|
|
191
|
+
);
|
|
192
|
+
} catch {}
|
|
193
|
+
if (fixableCount === 0) return 0;
|
|
194
|
+
// Apply the fixes
|
|
195
|
+
const fix = await safeSpawnAsync(
|
|
196
|
+
cmd,
|
|
197
|
+
["--fix", "--no-error-on-unmatched-pattern", filePath],
|
|
198
|
+
{ timeout: 30000, cwd },
|
|
199
|
+
);
|
|
200
|
+
if (fix.status === 2) return 0;
|
|
201
|
+
return fixableCount;
|
|
202
|
+
}
|
|
203
|
+
|
|
93
204
|
// --- Main Pipeline ---
|
|
94
205
|
|
|
95
206
|
export async function runPipeline(
|
|
@@ -184,6 +295,9 @@ export async function runPipeline(
|
|
|
184
295
|
}
|
|
185
296
|
|
|
186
297
|
let output = "";
|
|
298
|
+
const autofixTools: string[] = []; // track which tools fixed something
|
|
299
|
+
let testSummary: { passed: number; total: number; failed: number } | null =
|
|
300
|
+
null;
|
|
187
301
|
|
|
188
302
|
// --- 4. Auto-fix ---
|
|
189
303
|
// Biome (TS/JS) and Ruff (Python) never touch the same file, so their
|
|
@@ -208,6 +322,7 @@ export async function runPipeline(
|
|
|
208
322
|
const result = ruffClient.fixFile(filePath);
|
|
209
323
|
if (result.success && result.fixed > 0) {
|
|
210
324
|
fixedCount += result.fixed;
|
|
325
|
+
autofixTools.push(`ruff:${result.fixed}`);
|
|
211
326
|
fixedThisTurn.add(filePath);
|
|
212
327
|
dbg(`autofix: ruff fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
213
328
|
}
|
|
@@ -217,12 +332,24 @@ export async function runPipeline(
|
|
|
217
332
|
const result = biomeClient.fixFile(filePath);
|
|
218
333
|
if (result.success && result.fixed > 0) {
|
|
219
334
|
fixedCount += result.fixed;
|
|
335
|
+
autofixTools.push(`biome:${result.fixed}`);
|
|
220
336
|
fixedThisTurn.add(filePath);
|
|
221
337
|
dbg(`autofix: biome fixed ${result.fixed} issue(s) in ${filePath}`);
|
|
222
338
|
}
|
|
223
339
|
}
|
|
224
340
|
}
|
|
225
|
-
|
|
341
|
+
// ESLint --fix: only for jsts files in projects that use ESLint
|
|
342
|
+
if (!noAutofix && isJsTs(filePath)) {
|
|
343
|
+
const eslintFixed = await tryEslintFix(filePath, cwd);
|
|
344
|
+
if (eslintFixed > 0) {
|
|
345
|
+
fixedCount += eslintFixed;
|
|
346
|
+
autofixTools.push(`eslint:${eslintFixed}`);
|
|
347
|
+
fixedThisTurn.add(filePath);
|
|
348
|
+
dbg(`autofix: eslint fixed ${eslintFixed} issue(s) in ${filePath}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
phase.end("autofix", { fixedCount, tools: ["ruff", "biome", "eslint"] });
|
|
226
353
|
|
|
227
354
|
// --- 5. Dispatch lint ---
|
|
228
355
|
phase.start("dispatch_lint");
|
|
@@ -239,7 +366,9 @@ export async function runPipeline(
|
|
|
239
366
|
}
|
|
240
367
|
|
|
241
368
|
if (fixedCount > 0) {
|
|
242
|
-
|
|
369
|
+
const detail =
|
|
370
|
+
autofixTools.length > 0 ? ` (${autofixTools.join(", ")})` : "";
|
|
371
|
+
output += `\n\n✅ Auto-fixed ${fixedCount} issue(s)${detail}`;
|
|
243
372
|
}
|
|
244
373
|
|
|
245
374
|
if (formatChanged || fixedCount > 0) {
|
|
@@ -285,6 +414,11 @@ export async function runPipeline(
|
|
|
285
414
|
},
|
|
286
415
|
});
|
|
287
416
|
if (testResult && !testResult.error) {
|
|
417
|
+
testSummary = {
|
|
418
|
+
passed: testResult.passed,
|
|
419
|
+
total: testResult.passed + testResult.failed + testResult.skipped,
|
|
420
|
+
failed: testResult.failed,
|
|
421
|
+
};
|
|
288
422
|
const testOutput = testRunnerClient.formatResult(testResult);
|
|
289
423
|
if (testOutput) {
|
|
290
424
|
output += `\n\n${testOutput}`;
|
|
@@ -296,6 +430,11 @@ export async function runPipeline(
|
|
|
296
430
|
phase.end("test_runner", { found: testInfoFound, ran: testRunnerRan });
|
|
297
431
|
|
|
298
432
|
// --- 7. Cascade diagnostics (LSP only) ---
|
|
433
|
+
// Deferred: cascade errors are errors in OTHER files caused by this edit.
|
|
434
|
+
// They are NOT shown inline (mid-refactor they are always noisy — agent is
|
|
435
|
+
// still editing the other files). Returned in cascadeOutput so index.ts can
|
|
436
|
+
// surface the LAST snapshot at turn_end once all edits in the turn are done.
|
|
437
|
+
let cascadeOutput: string | undefined;
|
|
299
438
|
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
300
439
|
const MAX_CASCADE_FILES = 5;
|
|
301
440
|
const MAX_DIAGNOSTICS_PER_FILE = 20;
|
|
@@ -319,7 +458,7 @@ export async function runPipeline(
|
|
|
319
458
|
}
|
|
320
459
|
|
|
321
460
|
if (otherFileErrors.length > 0) {
|
|
322
|
-
|
|
461
|
+
let c = `📐 Cascade errors in ${otherFileErrors.length} other file(s) — fix before finishing turn:`;
|
|
323
462
|
for (const { file, errors } of otherFileErrors.slice(
|
|
324
463
|
0,
|
|
325
464
|
MAX_CASCADE_FILES,
|
|
@@ -329,18 +468,19 @@ export async function runPipeline(
|
|
|
329
468
|
errors.length > MAX_DIAGNOSTICS_PER_FILE
|
|
330
469
|
? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more`
|
|
331
470
|
: "";
|
|
332
|
-
|
|
471
|
+
c += `\n<diagnostics file="${file}">`;
|
|
333
472
|
for (const e of limited) {
|
|
334
473
|
const line = (e.range?.start?.line ?? 0) + 1;
|
|
335
474
|
const col = (e.range?.start?.character ?? 0) + 1;
|
|
336
475
|
const code = e.code ? ` [${e.code}]` : "";
|
|
337
|
-
|
|
476
|
+
c += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
|
|
338
477
|
}
|
|
339
|
-
|
|
478
|
+
c += `${suffix}\n</diagnostics>`;
|
|
340
479
|
}
|
|
341
480
|
if (otherFileErrors.length > MAX_CASCADE_FILES) {
|
|
342
|
-
|
|
481
|
+
c += `\n... and ${otherFileErrors.length - MAX_CASCADE_FILES} more files with errors`;
|
|
343
482
|
}
|
|
483
|
+
cascadeOutput = c;
|
|
344
484
|
}
|
|
345
485
|
|
|
346
486
|
logLatency({
|
|
@@ -358,6 +498,36 @@ export async function runPipeline(
|
|
|
358
498
|
|
|
359
499
|
// --- Final timing ---
|
|
360
500
|
const elapsed = Date.now() - pipelineStart;
|
|
501
|
+
|
|
502
|
+
// --- All-clear / warnings notice ---
|
|
503
|
+
// When no blocking output exists, emit a one-liner so the agent knows
|
|
504
|
+
// checks actually ran and what the result was.
|
|
505
|
+
if (!output) {
|
|
506
|
+
const kind = detectFileKind(filePath);
|
|
507
|
+
const langLabel = kind ? getFileKindLabel(kind) : path.extname(filePath);
|
|
508
|
+
const parts: string[] = [];
|
|
509
|
+
|
|
510
|
+
if (dispatchResult.warnings.length > 0) {
|
|
511
|
+
// Has non-blocking warnings — tell agent to run booboo
|
|
512
|
+
parts.push(`no blockers`);
|
|
513
|
+
parts.push(
|
|
514
|
+
`${dispatchResult.warnings.length} warning(s) -> /lens-booboo`,
|
|
515
|
+
);
|
|
516
|
+
} else if (kind) {
|
|
517
|
+
parts.push(`${langLabel} clean`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (testSummary) {
|
|
521
|
+
if (testSummary.failed === 0) {
|
|
522
|
+
parts.push(`${testSummary.passed}/${testSummary.total} tests`);
|
|
523
|
+
}
|
|
524
|
+
// failing tests already have their own output above — skip here
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
parts.push(`${elapsed}ms`);
|
|
528
|
+
output = `checkmark ${parts.join(" · ")}`.replace("checkmark", "\u2713");
|
|
529
|
+
}
|
|
530
|
+
|
|
361
531
|
phase.end("total", { hasOutput: !!output });
|
|
362
532
|
|
|
363
533
|
logLatency({
|
|
@@ -370,6 +540,7 @@ export async function runPipeline(
|
|
|
370
540
|
|
|
371
541
|
return {
|
|
372
542
|
output,
|
|
543
|
+
cascadeOutput,
|
|
373
544
|
isError: false,
|
|
374
545
|
fileModified: formatChanged || fixedCount > 0,
|
|
375
546
|
};
|
package/clients/scan-utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { EXCLUDED_DIRS, isTestFile } from "./file-utils.js";
|
|
4
|
+
import { collectSourceFiles, isBuildArtifact } from "./source-filter.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Common parsing logic for ast-grep JSON output (handles both array and NDJSON).
|
|
@@ -27,6 +28,10 @@ export function parseAstGrepJson(raw: string): any[] {
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Check if a file should be ignored based on project type and common patterns.
|
|
31
|
+
*
|
|
32
|
+
* @deprecated Use `isBuildArtifact()` from `source-filter.js` instead for artifact
|
|
33
|
+
* detection, or compose your own filter using `collectSourceFiles()`. This function
|
|
34
|
+
* is kept for backward compatibility.
|
|
30
35
|
*/
|
|
31
36
|
export function shouldIgnoreFile(
|
|
32
37
|
filePath: string,
|
|
@@ -35,7 +40,10 @@ export function shouldIgnoreFile(
|
|
|
35
40
|
const relPath = filePath.replace(/\\/g, "/");
|
|
36
41
|
const _basename = path.basename(relPath);
|
|
37
42
|
|
|
38
|
-
//
|
|
43
|
+
// Use new source-filter module for artifact detection
|
|
44
|
+
if (isTsProject && isBuildArtifact(filePath)) return true;
|
|
45
|
+
|
|
46
|
+
// Legacy: simple JS check for non-TS projects (hand-written JS)
|
|
39
47
|
const isJs =
|
|
40
48
|
relPath.endsWith(".js") ||
|
|
41
49
|
relPath.endsWith(".mjs") ||
|
|
@@ -53,36 +61,16 @@ export function shouldIgnoreFile(
|
|
|
53
61
|
|
|
54
62
|
/**
|
|
55
63
|
* Recursively find source files in a directory, respecting common excludes.
|
|
64
|
+
*
|
|
65
|
+
* This function now delegates to `collectSourceFiles()` from the `source-filter`
|
|
66
|
+
* module for unified artifact detection across all scanners.
|
|
67
|
+
*
|
|
68
|
+
* @param dir - Directory to scan
|
|
69
|
+
* @param isTsProject - Deprecated parameter (kept for backward compatibility, not used)
|
|
70
|
+
* @returns Array of absolute file paths that are source files (not build artifacts)
|
|
56
71
|
*/
|
|
57
|
-
export function getSourceFiles(dir: string,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const scan = (d: string) => {
|
|
62
|
-
let entries: fs.Dirent[] = [];
|
|
63
|
-
try {
|
|
64
|
-
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
65
|
-
} catch {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const entry of entries) {
|
|
70
|
-
const full = path.join(d, entry.name);
|
|
71
|
-
if (entry.isDirectory()) {
|
|
72
|
-
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
73
|
-
scan(full);
|
|
74
|
-
} else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
|
|
75
|
-
// Skip compiled JS if it's a TS project
|
|
76
|
-
if (
|
|
77
|
-
isTsProject &&
|
|
78
|
-
entry.name.endsWith(".js") &&
|
|
79
|
-
fs.existsSync(full.replace(/\.js$/, ".ts"))
|
|
80
|
-
)
|
|
81
|
-
continue;
|
|
82
|
-
files.push(full);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
scan(dir);
|
|
87
|
-
return files;
|
|
72
|
+
export function getSourceFiles(dir: string, _isTsProject?: boolean): string[] {
|
|
73
|
+
// Delegate to the unified source-filter module
|
|
74
|
+
// isTsProject parameter is no longer needed — artifact detection is automatic
|
|
75
|
+
return collectSourceFiles(dir);
|
|
88
76
|
}
|
package/clients/sg-runner.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
getSgCommand,
|
|
14
|
+
isSgAvailable,
|
|
15
|
+
} from "./dispatch/runners/utils/runner-helpers.js";
|
|
12
16
|
import { safeSpawn } from "./safe-spawn.js";
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -90,10 +94,7 @@ export class SgRunner {
|
|
|
90
94
|
isAvailable(): boolean {
|
|
91
95
|
if (this.available !== null) return this.available;
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
timeout: 10000,
|
|
95
|
-
});
|
|
96
|
-
this.available = !result.error && result.status === 0;
|
|
97
|
+
this.available = isSgAvailable();
|
|
97
98
|
return this.available;
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -214,7 +215,8 @@ export class SgRunner {
|
|
|
214
215
|
* Run ast-grep synchronously (for simple scans)
|
|
215
216
|
*/
|
|
216
217
|
execSync(args: string[]): { output: string; error?: string } {
|
|
217
|
-
const
|
|
218
|
+
const { cmd: sgCmd, args: sgPre } = getSgCommand();
|
|
219
|
+
const result = safeSpawn(sgCmd, [...sgPre, "sg", ...args], {
|
|
218
220
|
timeout: 30000,
|
|
219
221
|
});
|
|
220
222
|
|