pi-lens 3.8.42 → 3.8.44
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 +64 -7
- package/clients/ast-grep-client.ts +49 -17
- package/clients/dependency-checker.ts +82 -39
- package/clients/dispatch/dispatcher.ts +3 -1
- package/clients/dispatch/integration.ts +2 -0
- package/clients/dispatch/plan.ts +5 -0
- package/clients/dispatch/runners/biome.ts +1 -1
- package/clients/dispatch/runners/cpp-check.ts +20 -11
- package/clients/dispatch/runners/dart-analyze.ts +17 -11
- package/clients/dispatch/runners/detekt.ts +4 -1
- package/clients/dispatch/runners/dotnet-build.ts +93 -8
- package/clients/dispatch/runners/fish-indent.ts +83 -0
- package/clients/dispatch/runners/gleam-check.ts +4 -2
- package/clients/dispatch/runners/go-vet.ts +3 -3
- package/clients/dispatch/runners/hadolint.ts +1 -1
- package/clients/dispatch/runners/htmlhint.ts +1 -1
- package/clients/dispatch/runners/index.ts +2 -0
- package/clients/dispatch/runners/javac.ts +99 -94
- package/clients/dispatch/runners/ktlint.ts +1 -1
- package/clients/dispatch/runners/markdownlint.ts +88 -88
- package/clients/dispatch/runners/mypy.ts +88 -88
- package/clients/dispatch/runners/oxlint.ts +8 -7
- package/clients/dispatch/runners/php-lint.ts +82 -79
- package/clients/dispatch/runners/phpstan.ts +4 -3
- package/clients/dispatch/runners/prisma-validate.ts +8 -5
- package/clients/dispatch/runners/psscriptanalyzer.ts +22 -3
- package/clients/dispatch/runners/pyright.ts +1 -1
- package/clients/dispatch/runners/python-slop.ts +2 -3
- package/clients/dispatch/runners/semgrep.ts +3 -1
- package/clients/dispatch/runners/shellcheck.ts +195 -195
- package/clients/dispatch/runners/shfmt.ts +100 -100
- package/clients/dispatch/runners/spellcheck.ts +145 -143
- package/clients/dispatch/runners/sqlfluff.ts +108 -108
- package/clients/dispatch/runners/stylelint.ts +3 -1
- package/clients/dispatch/runners/taplo.ts +1 -1
- package/clients/dispatch/runners/tflint.ts +1 -1
- package/clients/dispatch/runners/tree-sitter.ts +7 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +112 -37
- package/clients/dispatch/runners/yamllint.ts +92 -92
- package/clients/dispatch/runners/zig-check.ts +1 -1
- package/clients/dispatch/types.ts +6 -0
- package/clients/file-kinds.ts +7 -1
- package/clients/formatters.ts +6 -3
- package/clients/installer/index.ts +124 -40
- package/clients/jscpd-client.ts +59 -60
- package/clients/knip-client.ts +402 -357
- package/clients/language-policy.ts +6 -0
- package/clients/language-profile.ts +1 -0
- package/clients/lsp/client.ts +77 -7
- package/clients/lsp/index.ts +49 -3
- package/clients/lsp/server-strategies.ts +7 -0
- package/clients/lsp/server.ts +46 -7
- package/clients/pipeline.ts +56 -2
- package/clients/review-graph/builder.ts +87 -7
- package/clients/runtime-coordinator.ts +20 -0
- package/clients/runtime-session.ts +15 -13
- package/clients/runtime-tool-result.ts +7 -0
- package/clients/runtime-turn.ts +36 -16
- package/clients/safe-spawn.ts +24 -14
- package/clients/sg-runner.ts +86 -39
- package/clients/tool-availability.ts +1 -1
- package/clients/tool-policy.ts +20 -0
- package/clients/tree-sitter-client.ts +138 -4
- package/clients/tree-sitter-query-loader.ts +19 -4
- package/commands/booboo.ts +36 -10
- package/index.ts +4 -1
- package/package.json +1 -1
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +3 -2
- package/rules/tree-sitter-queries/python/return-in-generator.yml +5 -5
- package/rules/tree-sitter-queries/typescript/incomplete-assertion.yml +50 -0
- package/rules/tree-sitter-queries/typescript/switch-non-case-labels.yml +53 -0
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +71 -0
- package/tools/ast-grep-replace.js +6 -1
- package/tools/ast-grep-replace.ts +6 -1
- package/tools/ast-grep-search.js +57 -3
- package/tools/ast-grep-search.ts +65 -5
- package/tools/lsp-diagnostics.js +330 -0
- package/tools/lsp-diagnostics.ts +388 -0
- package/tools/shared.js +3 -2
- package/tools/shared.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,65 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.44] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`fish` FileKind with `fish_indent` formatter runner** — `.fish` files are now a first-class `"fish"` kind rather than being bucketed under `"shell"`. A new `fish-indent` runner wraps `fish_indent --check` (fish ≥ 3.6), reporting a formatting warning with a `fish_indent -w` fix hint on exit 1 and a blocking parse-error diagnostic when stderr is non-empty. Formatter and linter policy entries added for `.fish` in `tool-policy.ts`; fish dispatch group `[lsp, fish-indent]` wired in `language-policy.ts`. Closes #74.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Linux `sg` command no longer breaks `ast_grep_search` / `ast_grep_replace`** — ast-grep resolution now prefers the canonical `ast-grep` binary and only accepts `sg` when `--version` proves it is ast-grep, avoiding the util-linux `/usr/bin/sg` group-switch command. The installer, probe cache, tool availability, sync runner helpers, and Python slop scan now share the corrected command shape and `npx --no -- ast-grep` fallback. Closes #75.
|
|
16
|
+
- **`return-in-generator` no longer flags normal `async def` coroutine returns** — added a Python tree-sitter post-filter that keeps only synchronous functions containing `yield`, skips `async def`, and rejects non-generator functions. Added regression tests for valued generator returns, coroutine returns, and normal functions. Closes #76.
|
|
17
|
+
- **`python-sql-injection` no longer flags safe SQLAlchemy expression execution** — the rule now captures the call receiver and the post-filter skips likely SQLAlchemy ORM session receivers (`session.execute(stmt)`) plus expression-builder calls such as `conn.execute(select(...).where(...))`, while still flagging raw `cursor.execute(sql)` and composed SQL strings. Closes #77.
|
|
18
|
+
- **Formatter tests no longer depend on a real global Ruff install** — the Ruff global fallback test now uses an isolated PATH shim, making it deterministic on machines without Ruff installed.
|
|
19
|
+
|
|
20
|
+
- **`psscriptanalyzer` runner could hang indefinitely** — `spawnPs` had no timeout; if `pwsh` or `Invoke-ScriptAnalyzer` stalled on a large file the turn would block forever. Added a 30s timeout with SIGTERM → 1s → SIGKILL escalation. `shell: false` means `child.pid` is the actual `pwsh` process so `child.kill()` hits the right target directly (no `taskkill` needed).
|
|
21
|
+
|
|
22
|
+
- **`turn_end` hangs ~40–50s on Windows when knip times out** — `safeSpawnAsync` used `child.kill("SIGTERM/SIGKILL")` to terminate timed-out processes. On Windows with `shell: true`, `child.pid` is the `cmd.exe` wrapper; killing it orphans the actual subprocess (e.g. knip/npx node process) which then runs unsupervised until it naturally exits. Replaced with `taskkill /F /T /PID` on Windows, which kills the full process tree rooted at `cmd.exe`, matching the approach already used in `lsp/client.ts`.
|
|
23
|
+
|
|
24
|
+
- **`fish` missing from `LANGUAGE_CAPABILITY_MATRIX` and `LintRunnerName`** — adding the `"fish"` FileKind required two exhaustiveness fixes: a `fish` entry in `plan.ts`'s `Record<FileKind, CapabilityMatrixEntry>` and `"fish-indent"` in the `LintRunnerName` union in `tool-policy.ts`; both caused build/type-check failures on CI.
|
|
25
|
+
- **shellcheck and shfmt no longer fire on `.fish` files** — `.fish` was classified as `"shell"`, causing both runners (which use `appliesTo: ["shell"]`) to process fish scripts with `--shell bash`, producing false-positive SC1073/SC1064 parse errors. Moving `.fish` to the new `"fish"` kind fixes the routing with no special-case logic in either runner. Closes #74.
|
|
26
|
+
|
|
27
|
+
- **`lsp_diagnostics` tool** — proactive LSP error checking for files and directories. The agent can now run `lsp_diagnostics({ filePath: "src/" })` before builds to catch issues without making edits. Directory mode walks the tree (skipping node_modules/.git/target), auto-detects the language extension, opens each file in the LSP client, and aggregates diagnostics. Supports severity filtering (`error`/`warning`/`information`/`hint`/`all`), caps at 50 files and 200 diagnostics. Returns structured details with `totalDiagnostics`, `truncated`, and per-diagnostic `file`/`line`/`severity`/`message`/`source`/`code`. Adapted from `code-yeongyu/pi-lsp-client`.
|
|
28
|
+
- **LSP process stderr capture and health check** — the LSP client now maintains a rolling 100-line stderr buffer from server startup through shutdown. Three new client methods exposed: `processExited()` (true if the server process died), `recentStderr(n)` (last N lines for diagnostics), and `checkAlive()` (pre-request health check returning error string with exit code + stderr tail if dead). Previously, stderr was only captured during initialization and discarded afterward.
|
|
29
|
+
- **SIGTERM → 1.5s → SIGKILL escalation in `killProcessTree`** — on Unix, process cleanup now sends SIGTERM first, waits 1.5 seconds, then sends SIGKILL if the process is still alive. Prevents zombie server processes that survive a standard kill. Windows already uses `taskkill /F /T` (force kill tree).
|
|
30
|
+
- **LSP force-reinstall when PATH-resolved tool is broken** — when an LSP server's PATH candidate fails to launch (e.g. broken symlink, missing runtime, corrupted binary) AND the managed install returns the same broken PATH entry, pi-lens now clears the probe cache, downloads a managed copy from the registry (npm/GitHub/pip), and retries the launch. Previously, broken PATH tools triggered exponential backoff and were permanently disabled after 5 failures. The retry only fires when the `ensureTool` path is a bare command name (no `/` or `\` separators) — absolute paths from prior managed installs are not force-reinstalled to avoid redundant download loops. `ensureTool` gained an optional `forceReinstall` flag that bypasses both the in-memory `resolvedPathCache` and the persistent probe cache.
|
|
31
|
+
- **`getToolPath` prefers managed installs over PATH for github-strategy tools** — github-strategy tools (`rust-analyzer`, `shellcheck`, `shfmt`, `golangci-lint`) now check `~/.pi-lens/bin/` before falling through to PATH lookup. This ensures force-reinstall flows find the newly downloaded binary, and pi-lens-managed copies take priority over potentially stale or broken PATH entries. Non-github tools (npm, pip) are unaffected.
|
|
32
|
+
- **Pattern hints for `ast_grep_search` zero-match results** — when a search returns no matches, the tool now appends a hint suggesting likely pattern mistakes: regex misuse (`\w`, `\d`, `[a-z]`, `.*`, `.+`, `|` alternation), language-specific mistakes (Python trailing colons, incomplete JS/Go/Rust function patterns). Adapted from `code-yeongyu/pi-ast-grep`.
|
|
33
|
+
- **Truncation metadata in ast-grep tool results** — `SgResult` now carries `totalMatches` and `truncated` fields, threaded through `SgRunner` → `AstGrepClient` → both `ast_grep_search` and `ast_grep_replace` tool `details`. The agent can now distinguish "50 shown of 500 total" from "50 total".
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Runner process execution is async/non-blocking across hook paths** — jscpd scans, Madge dependency checks, formatter execution, and dispatch runners that previously used sync `safeSpawn()` now use `safeSpawnAsync()` in write/session/turn hooks. Added in-flight guards for jscpd and Madge project/file scans, async availability checks in runner helpers, and Knip availability dedupe + project-root bail before install/probe.
|
|
38
|
+
- **`isCommandAvailable` replaced `which`/`where` spawn with PATH walk + `statSync` size validation** — instead of spawning `which`/`where` (~50 ms + timeout risk), the installer now walks `$PATH` entries synchronously and checks `statSync(path).isFile() && stat.size > 0` for each candidate. This catches broken symlinks (stat throws `ENOENT` or returns size 0) at ~μs per candidate with zero process spawns. On Windows, `.exe`, `.cmd`, and `.bat` extensions are probed.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **SonarCloud security hotspots resolved** — replaced the .NET build diagnostic regex with a linear manual parser to avoid ReDoS risk (S5852), and switched jscpd temporary directory creation from a `Math.random()` suffix to `fs.mkdtempSync()` to avoid weak PRNG use (S2245).
|
|
43
|
+
- **ast-grep tool language list aligned with ast-grep CLI** — dropped phantom `dart` and `sql` (not supported by ast-grep binary), added missing `bash`, `nix`, `solidity`. The `LANGUAGES` constant in `tools/shared.ts` now matches ast-grep v0.41's official 25-language list.
|
|
44
|
+
- **Graph-cache test: disk cache leaked across test runs** — `buildOrUpdateGraph` persists to `cwd/.pi-lens/cache/review-graph.json`. All tests used hardcoded `"/cwd"`, causing the first test run's disk cache to contaminate subsequent runs. Switched to `fs.mkdtempSync` temp directories with `afterEach` cleanup.
|
|
45
|
+
- **Disabled tree-sitter rules leaked into production** — `parseQueryFile` uses the YAML's `language:` field over the directory name, so rules in `typescript-disabled/` with `language: typescript` were loaded as active TypeScript rules and appeared in the diagnostics widget. Added `!d.name.endsWith("-disabled")` filter to `loadQueries` directory enumeration.
|
|
46
|
+
|
|
47
|
+
## [3.8.43] - 2026-05-10
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **Unresolved inline blocker re-surfacing at turn_end** — when the agent ignores a blocking diagnostic shown during a write/edit and moves to the next turn without fixing it, the blocker now reappears in the turn_end injection framed as `"Unresolved from this turn — <file>: 🔴 STOP…"`. Previously, unresolved inline blockers were silently lost until cascade happened to re-touch the same file via an importer. `RuntimeCoordinator` tracks the last-seen blocking output per file (`_pendingInlineBlockers`); a subsequent write that produces no blockers clears the entry, so only genuinely unresolved issues resurface. The map is cleared at `beginTurn` to prevent cross-turn contamination.
|
|
52
|
+
- **S1219 (switch non-case labels) and S2970 (incomplete assertions) blocking tree-sitter rules** — S1219 detects labeled statements inside switch cases in TypeScript (SonarCloud S1219); S2970 detects Jest/Vitest `expect()` chains that are never called (e.g. `expect(x).toBe(y)` without `await`), with Chai property assertion exclusion. S2083 (path traversal) moved to disabled — regex heuristics on tree-sitter syntax are the wrong layer; needs taint/data-flow analysis. Adds `parent?` field to `TreeSitterNode` interface.
|
|
53
|
+
- **Inline code snippets in blocker output** — each 🔴 STOP diagnostic now includes the exact source line the agent wrote that caused the violation, so the agent can identify and fix the issue without re-reading the file. `fixSuggestion` is also surfaced inline when present. Snippet capped at 120 chars.
|
|
54
|
+
- **AST node type and matched text in blocker output** — tree-sitter diagnostics now carry `matchedText` (the exact matched node, more precise than the full source line) and `astNodeType` (e.g. `call_expression`, `template_string`). The agent sees: `L12: SQL query built with string interpolation (template_string) → db.query(...)`.
|
|
55
|
+
- **Persist review graph to disk** — `_workspaceGraphCache` is now backed by `.pi-lens/cache/review-graph.json`. On cold start, if source file signatures match the stored cache, the full 2–4 s tree-sitter + import-fact build is skipped (~20 ms JSON parse + `rebuildIndexes` instead). Write is fire-and-forget, never blocks dispatch.
|
|
56
|
+
- **Preserve last known LSP diagnostics when LSP goes inactive** — when no live clients are available (dead client respawning, circuit-breaker cooldown), `getDiagnostics` now returns the last non-empty result for that file instead of `[]`. The widget keeps showing the last known issues rather than going blank mid-session. Live clients returning `[]` clears the stale entry. Stale hits are logged as `failureKind: "no_clients_stale"`.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Read-guard false-positive block on files outside the project root** — edits to files outside `projectRoot` (e.g. `C:/llama/*.bat`, scripts in arbitrary directories) were always blocked with `zero_read` because reads for external files are intentionally not recorded (`isExternalOrVendor` gate in the read handler), but the `checkEdit` call had no matching guard. Added `!isExternalOrVendor` to the `checkEdit` condition so external files bypass the read-guard entirely, consistent with how reads are handled.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **Replace pyright-langserver and pylsp with jedi-language-server for Python LSP** — `PythonServer` (pyright-langserver) and `PythonPylspServer` (pylsp) removed from `LSP_SERVERS`; replaced by `PythonJediServer` which spawns `jedi-language-server`. pyright-langserver was causing 5–14 s cold-start delays on large Python projects (e.g. tinygrad) because it performs full workspace analysis on startup; jedi starts in ~200–500 ms via lazy per-file analysis. pylsp was removed because it consistently returned 0 diagnostics (no venv → jedi can't resolve imports; 1500 ms aggregate timeout hit on warm runs). Deep type checking is unaffected — the standalone `pyright` CLI runner and `mypy` runner continue to run in parallel. Added `"python-jedi"` strategy entry (`seedFirstPush: true`, `aggregateWaitMs: 1000`). Wall-clock gate for Python dispatch shifts from LSP (~5–14 s) to mypy (~3.5 s).
|
|
65
|
+
|
|
7
66
|
## [3.8.42] - 2026-05-08
|
|
8
67
|
|
|
9
68
|
### Added
|
|
@@ -14,11 +73,11 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
14
73
|
- **Severity alignment for 3 existing TS tree-sitter blocking rules** — `ts-command-injection`, `ts-ssrf`, `unsafe-regex` had `inline_tier: blocking` but `severity: warning`, producing `semantic: "warning"` which is never shown inline. Fixed to `severity: error` → `semantic: "blocking"` → actually surfaces to the agent.
|
|
15
74
|
- **Fixed `inline_tier: error` typo** on `ts-hallucinated-react-import` and `python-hallucinated-import` (→ `blocking`).
|
|
16
75
|
- **13 new high-confidence blocking promotions across 5 languages** (all `severity: error`, `inline_tier: blocking`):
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
76
|
+
- _TypeScript:_ `ts-weak-hash` (`createHash("md5"/"sha1")` — confidence: high)
|
|
77
|
+
- _Python:_ `python-command-injection`, `python-sql-injection`, `python-insecure-deserialization`, `python-weak-hash`
|
|
78
|
+
- _Go:_ `go-command-injection`, `go-sql-injection`, `go-shared-map-write-goroutine`, `go-weak-hash`
|
|
79
|
+
- _Ruby:_ `ruby-weak-hash`
|
|
80
|
+
- _Rust:_ `rust-lock-held-across-await`
|
|
22
81
|
- **4 new blocking tree-sitter rules (SonarCloud BLOCKER equivalents)**:
|
|
23
82
|
- `ts-xss-dom-sink` (S5696) — flags dynamic values assigned to `innerHTML`/`outerHTML` or passed to `document.write()` / `document.writeln()`
|
|
24
83
|
- `ts-dynamic-require` (S5335) — flags `require()` called with a non-string-literal argument (arbitrary module loading)
|
|
@@ -41,8 +100,6 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
41
100
|
|
|
42
101
|
- **Startup observability** — `checkProbeCache` now logs the reason for each cache miss (`ttl expired`, `gone`, `mtime changed`); the lsp-config fire-and-forget callback logs how many warm files were configured once the config resolves asynchronously.
|
|
43
102
|
|
|
44
|
-
|
|
45
|
-
|
|
46
103
|
### Added
|
|
47
104
|
|
|
48
105
|
- **Test runner: import-based fallback discovery** — when basename pattern lookup finds no test file for a modified source file (e.g. `cline.test.ts` for `cline-auth.ts`), the runner now scans `tests/`, `__tests__/`, and the source file's own directory for any `*.test.*` file whose content references the source basename in an import path. Fixes the silent `no test file found` for files whose test is named after a module rather than the source file.
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* Rules: ./rules/ directory
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from "node:child_process";
|
|
12
11
|
import * as fs from "node:fs";
|
|
13
12
|
import * as path from "node:path";
|
|
14
13
|
import { AstGrepParser } from "./ast-grep-parser.js";
|
|
@@ -73,7 +72,12 @@ export class AstGrepClient {
|
|
|
73
72
|
lang: string,
|
|
74
73
|
paths: string[],
|
|
75
74
|
options?: { selector?: string; context?: number },
|
|
76
|
-
): Promise<{
|
|
75
|
+
): Promise<{
|
|
76
|
+
matches: AstGrepMatch[];
|
|
77
|
+
totalMatches: number;
|
|
78
|
+
truncated: boolean;
|
|
79
|
+
error?: string;
|
|
80
|
+
}> {
|
|
77
81
|
const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
|
|
78
82
|
if (options?.selector) {
|
|
79
83
|
args.push("--selector", options.selector);
|
|
@@ -82,7 +86,13 @@ export class AstGrepClient {
|
|
|
82
86
|
args.push("--context", String(options.context));
|
|
83
87
|
}
|
|
84
88
|
args.push(...paths);
|
|
85
|
-
|
|
89
|
+
const result = await this.runner.exec(args);
|
|
90
|
+
return {
|
|
91
|
+
matches: result.matches,
|
|
92
|
+
totalMatches: result.totalMatches,
|
|
93
|
+
truncated: result.truncated,
|
|
94
|
+
error: result.error,
|
|
95
|
+
};
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/**
|
|
@@ -94,7 +104,13 @@ export class AstGrepClient {
|
|
|
94
104
|
lang: string,
|
|
95
105
|
paths: string[],
|
|
96
106
|
apply = false,
|
|
97
|
-
): Promise<{
|
|
107
|
+
): Promise<{
|
|
108
|
+
matches: AstGrepMatch[];
|
|
109
|
+
totalMatches: number;
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
applied: boolean;
|
|
112
|
+
error?: string;
|
|
113
|
+
}> {
|
|
98
114
|
const baseArgs = ["run", "-p", pattern, "-r", rewrite, "--lang", lang];
|
|
99
115
|
|
|
100
116
|
if (!apply) {
|
|
@@ -104,7 +120,13 @@ export class AstGrepClient {
|
|
|
104
120
|
"--json=compact",
|
|
105
121
|
...paths,
|
|
106
122
|
]);
|
|
107
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
matches: result.matches,
|
|
125
|
+
totalMatches: result.totalMatches,
|
|
126
|
+
truncated: result.truncated,
|
|
127
|
+
applied: false,
|
|
128
|
+
error: result.error,
|
|
129
|
+
};
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
// Apply: --update-all and --json are MUTUALLY EXCLUSIVE in sg.
|
|
@@ -117,7 +139,13 @@ export class AstGrepClient {
|
|
|
117
139
|
...paths,
|
|
118
140
|
]);
|
|
119
141
|
if (applyResult.error) {
|
|
120
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
matches: [],
|
|
144
|
+
totalMatches: 0,
|
|
145
|
+
truncated: false,
|
|
146
|
+
applied: false,
|
|
147
|
+
error: applyResult.error,
|
|
148
|
+
};
|
|
121
149
|
}
|
|
122
150
|
|
|
123
151
|
// Search for what was changed (pattern no longer matches after rewrite,
|
|
@@ -131,7 +159,13 @@ export class AstGrepClient {
|
|
|
131
159
|
"--json=compact",
|
|
132
160
|
...paths,
|
|
133
161
|
]);
|
|
134
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
matches: searchResult.matches,
|
|
164
|
+
totalMatches: searchResult.totalMatches,
|
|
165
|
+
truncated: searchResult.truncated,
|
|
166
|
+
applied: true,
|
|
167
|
+
error: undefined,
|
|
168
|
+
};
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
/**
|
|
@@ -285,18 +319,16 @@ message: found
|
|
|
285
319
|
const configPath = path.join(this.ruleDir, ".sgconfig.yml");
|
|
286
320
|
|
|
287
321
|
try {
|
|
288
|
-
const result =
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
},
|
|
296
|
-
);
|
|
322
|
+
const result = this.runner.execSync([
|
|
323
|
+
"scan",
|
|
324
|
+
"--config",
|
|
325
|
+
configPath,
|
|
326
|
+
"--json",
|
|
327
|
+
absolutePath,
|
|
328
|
+
]);
|
|
297
329
|
|
|
298
330
|
// ast-grep exits 1 when it finds issues
|
|
299
|
-
const output = result.
|
|
331
|
+
const output = result.output;
|
|
300
332
|
if (!output.trim()) return [];
|
|
301
333
|
|
|
302
334
|
const parser = new AstGrepParser(
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
15
15
|
|
|
16
16
|
// --- Types ---
|
|
17
17
|
|
|
@@ -38,6 +38,12 @@ interface FileImports {
|
|
|
38
38
|
|
|
39
39
|
export class DependencyChecker {
|
|
40
40
|
private available: boolean | null = null;
|
|
41
|
+
private ensureInFlight: Promise<boolean> | null = null;
|
|
42
|
+
private checkInFlight = new Map<string, Promise<DepCheckResult>>();
|
|
43
|
+
private scanInFlight = new Map<
|
|
44
|
+
string,
|
|
45
|
+
Promise<{ circular: CircularDep[]; count: number }>
|
|
46
|
+
>();
|
|
41
47
|
private log: (msg: string) => void;
|
|
42
48
|
|
|
43
49
|
// Cache: file path -> its imports
|
|
@@ -61,7 +67,17 @@ export class DependencyChecker {
|
|
|
61
67
|
async ensureAvailable(): Promise<boolean> {
|
|
62
68
|
// Fast path: already checked
|
|
63
69
|
if (this.available !== null) return this.available;
|
|
70
|
+
if (this.ensureInFlight) return this.ensureInFlight;
|
|
64
71
|
|
|
72
|
+
this.ensureInFlight = this.doEnsureAvailable();
|
|
73
|
+
try {
|
|
74
|
+
return await this.ensureInFlight;
|
|
75
|
+
} finally {
|
|
76
|
+
this.ensureInFlight = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async doEnsureAvailable(): Promise<boolean> {
|
|
65
81
|
// Check if available in PATH
|
|
66
82
|
const result = await safeSpawnAsync("madge", ["--version"], {
|
|
67
83
|
timeout: 5000,
|
|
@@ -84,29 +100,11 @@ export class DependencyChecker {
|
|
|
84
100
|
return true;
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
this.available = false;
|
|
87
104
|
this.log("Madge auto-install failed");
|
|
88
105
|
return false;
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
/**
|
|
92
|
-
* Check if madge is available (legacy sync method)
|
|
93
|
-
* Prefer ensureAvailable() for auto-install behavior
|
|
94
|
-
*/
|
|
95
|
-
isAvailable(): boolean {
|
|
96
|
-
if (this.available !== null) return this.available;
|
|
97
|
-
|
|
98
|
-
const result = safeSpawn("npx", ["madge", "--version"], {
|
|
99
|
-
timeout: 5000,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
this.available = !result.error && result.status === 0;
|
|
103
|
-
if (this.available) {
|
|
104
|
-
this.log("Madge available for dependency checking");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return this.available;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
108
|
/**
|
|
111
109
|
* Check if a file is part of a circular dependency (from cache)
|
|
112
110
|
*/
|
|
@@ -210,7 +208,7 @@ export class DependencyChecker {
|
|
|
210
208
|
* Quick circular dependency check using DFS on cached graph.
|
|
211
209
|
* Only re-runs full madge check when imports change.
|
|
212
210
|
*/
|
|
213
|
-
checkFile(filePath: string, cwd?: string): DepCheckResult {
|
|
211
|
+
async checkFile(filePath: string, cwd?: string): Promise<DepCheckResult> {
|
|
214
212
|
const normalized = path.resolve(filePath);
|
|
215
213
|
|
|
216
214
|
// Return early for non-existent files without running availability check
|
|
@@ -223,18 +221,9 @@ export class DependencyChecker {
|
|
|
223
221
|
};
|
|
224
222
|
}
|
|
225
223
|
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
hasCircular: false,
|
|
229
|
-
circular: [],
|
|
230
|
-
checked: false,
|
|
231
|
-
cacheHit: false,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const projectRoot = cwd || process.cwd();
|
|
224
|
+
const projectRoot = path.resolve(cwd || process.cwd());
|
|
236
225
|
|
|
237
|
-
// Check if imports changed
|
|
226
|
+
// Check if imports changed before probing/installing madge.
|
|
238
227
|
const importsChanged = this.importsChanged(normalized);
|
|
239
228
|
|
|
240
229
|
if (!importsChanged) {
|
|
@@ -249,13 +238,37 @@ export class DependencyChecker {
|
|
|
249
238
|
};
|
|
250
239
|
}
|
|
251
240
|
|
|
241
|
+
if (!(await this.ensureAvailable())) {
|
|
242
|
+
return {
|
|
243
|
+
hasCircular: false,
|
|
244
|
+
circular: [],
|
|
245
|
+
checked: false,
|
|
246
|
+
cacheHit: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const key = `${projectRoot}:${normalized}`;
|
|
251
|
+
const existing = this.checkInFlight.get(key);
|
|
252
|
+
if (existing) return existing;
|
|
253
|
+
|
|
254
|
+
const promise = this.runCheckFile(normalized, projectRoot).finally(() => {
|
|
255
|
+
this.checkInFlight.delete(key);
|
|
256
|
+
});
|
|
257
|
+
this.checkInFlight.set(key, promise);
|
|
258
|
+
return promise;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async runCheckFile(
|
|
262
|
+
normalized: string,
|
|
263
|
+
projectRoot: string,
|
|
264
|
+
): Promise<DepCheckResult> {
|
|
252
265
|
this.log(
|
|
253
|
-
`Imports changed for ${path.basename(
|
|
266
|
+
`Imports changed for ${path.basename(normalized)}, checking dependencies...`,
|
|
254
267
|
);
|
|
255
268
|
|
|
256
269
|
// Run madge on the specific file (fast)
|
|
257
270
|
try {
|
|
258
|
-
const result =
|
|
271
|
+
const result = await safeSpawnAsync(
|
|
259
272
|
"npx",
|
|
260
273
|
[
|
|
261
274
|
"madge",
|
|
@@ -271,6 +284,16 @@ export class DependencyChecker {
|
|
|
271
284
|
},
|
|
272
285
|
);
|
|
273
286
|
|
|
287
|
+
if (result.error) {
|
|
288
|
+
this.log(`Check error: ${result.error.message}`);
|
|
289
|
+
return {
|
|
290
|
+
hasCircular: false,
|
|
291
|
+
circular: [],
|
|
292
|
+
checked: false,
|
|
293
|
+
cacheHit: false,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
274
297
|
const output = result.stdout || "[]";
|
|
275
298
|
const parsed = JSON.parse(output);
|
|
276
299
|
|
|
@@ -333,10 +356,12 @@ export class DependencyChecker {
|
|
|
333
356
|
/**
|
|
334
357
|
* Full project scan (for /check-deps command)
|
|
335
358
|
*/
|
|
336
|
-
scanProject(
|
|
337
|
-
|
|
359
|
+
async scanProject(
|
|
360
|
+
cwd?: string,
|
|
361
|
+
): Promise<{ circular: CircularDep[]; count: number }> {
|
|
362
|
+
const projectRoot = path.resolve(cwd || process.cwd());
|
|
338
363
|
|
|
339
|
-
// Return early for non-existent or empty directories
|
|
364
|
+
// Return early for non-existent or empty directories before probing/installing.
|
|
340
365
|
if (!fs.existsSync(projectRoot)) {
|
|
341
366
|
return { circular: [], count: 0 };
|
|
342
367
|
}
|
|
@@ -348,12 +373,25 @@ export class DependencyChecker {
|
|
|
348
373
|
return { circular: [], count: 0 };
|
|
349
374
|
}
|
|
350
375
|
|
|
351
|
-
if (!this.
|
|
376
|
+
if (!(await this.ensureAvailable())) {
|
|
352
377
|
return { circular: [], count: 0 };
|
|
353
378
|
}
|
|
354
379
|
|
|
380
|
+
const existing = this.scanInFlight.get(projectRoot);
|
|
381
|
+
if (existing) return existing;
|
|
382
|
+
|
|
383
|
+
const promise = this.runScanProject(projectRoot).finally(() => {
|
|
384
|
+
this.scanInFlight.delete(projectRoot);
|
|
385
|
+
});
|
|
386
|
+
this.scanInFlight.set(projectRoot, promise);
|
|
387
|
+
return promise;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async runScanProject(
|
|
391
|
+
projectRoot: string,
|
|
392
|
+
): Promise<{ circular: CircularDep[]; count: number }> {
|
|
355
393
|
try {
|
|
356
|
-
const result =
|
|
394
|
+
const result = await safeSpawnAsync(
|
|
357
395
|
"npx",
|
|
358
396
|
[
|
|
359
397
|
"madge",
|
|
@@ -369,6 +407,11 @@ export class DependencyChecker {
|
|
|
369
407
|
},
|
|
370
408
|
);
|
|
371
409
|
|
|
410
|
+
if (result.error) {
|
|
411
|
+
this.log(`Scan error: ${result.error.message}`);
|
|
412
|
+
return { circular: [], count: 0 };
|
|
413
|
+
}
|
|
414
|
+
|
|
372
415
|
const output = result.stdout || "{}";
|
|
373
416
|
const data = JSON.parse(output);
|
|
374
417
|
|
|
@@ -742,7 +742,8 @@ export async function dispatchForFile(
|
|
|
742
742
|
|
|
743
743
|
// Format output — only blocking issues shown inline
|
|
744
744
|
// Warnings tracked but not shown (noise) — surfaced via /lens-booboo
|
|
745
|
-
|
|
745
|
+
const blockerOutput = formatDiagnostics(inlineBlockers, "blocking");
|
|
746
|
+
let output = blockerOutput;
|
|
746
747
|
output += formatDiagnostics(inlineFixed, "fixed");
|
|
747
748
|
if (coverageNotice) {
|
|
748
749
|
output += formatDiagnostics([coverageNotice], "warning", 1);
|
|
@@ -807,6 +808,7 @@ export async function dispatchForFile(
|
|
|
807
808
|
fixed: fixedItems,
|
|
808
809
|
resolvedCount,
|
|
809
810
|
output,
|
|
811
|
+
blockerOutput,
|
|
810
812
|
hasBlockers: blockers.length > 0,
|
|
811
813
|
};
|
|
812
814
|
}
|
|
@@ -1127,6 +1127,7 @@ export async function dispatchLintWithResult(
|
|
|
1127
1127
|
fixed: [],
|
|
1128
1128
|
resolvedCount: 0,
|
|
1129
1129
|
output: "",
|
|
1130
|
+
blockerOutput: "",
|
|
1130
1131
|
hasBlockers: false,
|
|
1131
1132
|
};
|
|
1132
1133
|
}
|
|
@@ -1145,6 +1146,7 @@ export async function dispatchLintWithResult(
|
|
|
1145
1146
|
fixed: [],
|
|
1146
1147
|
resolvedCount: 0,
|
|
1147
1148
|
output: "",
|
|
1149
|
+
blockerOutput: "",
|
|
1148
1150
|
hasBlockers: false,
|
|
1149
1151
|
};
|
|
1150
1152
|
}
|
package/clients/dispatch/plan.ts
CHANGED
|
@@ -110,6 +110,11 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<
|
|
|
110
110
|
{ mode: "all", runnerIds: ["fact-rules"], filterKinds: ["cmake"] },
|
|
111
111
|
],
|
|
112
112
|
},
|
|
113
|
+
fish: {
|
|
114
|
+
name: "Fish Script Formatting",
|
|
115
|
+
capabilities: ["format"],
|
|
116
|
+
writeGroups: [primary("fish")],
|
|
117
|
+
},
|
|
113
118
|
shell: {
|
|
114
119
|
name: "Shell Script Linting",
|
|
115
120
|
capabilities: ["lint", "security"],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
3
3
|
import { PRIORITY } from "../priorities.js";
|
|
4
4
|
import type {
|
|
5
5
|
Diagnostic,
|
|
@@ -12,20 +12,22 @@ type CompilerSpec =
|
|
|
12
12
|
| { command: string; args: string[]; flavor: "gcc" | "msvc" }
|
|
13
13
|
| undefined;
|
|
14
14
|
|
|
15
|
-
function resolveCompiler(absPath: string): CompilerSpec {
|
|
15
|
+
async function resolveCompiler(absPath: string): Promise<CompilerSpec> {
|
|
16
16
|
const gccLike: Array<{ command: string; args: string[] }> = [
|
|
17
17
|
{ command: "clang++", args: ["-fsyntax-only", absPath] },
|
|
18
18
|
{ command: "g++", args: ["-fsyntax-only", absPath] },
|
|
19
19
|
{ command: "c++", args: ["-fsyntax-only", absPath] },
|
|
20
20
|
];
|
|
21
21
|
for (const candidate of gccLike) {
|
|
22
|
-
const probe =
|
|
22
|
+
const probe = await safeSpawnAsync(candidate.command, ["--version"], {
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
});
|
|
23
25
|
if (!probe.error && probe.status === 0) {
|
|
24
26
|
return { ...candidate, flavor: "gcc" };
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const clProbe =
|
|
30
|
+
const clProbe = await safeSpawnAsync("cl", [], { timeout: 5000 });
|
|
29
31
|
if (!clProbe.error && clProbe.status !== null) {
|
|
30
32
|
return {
|
|
31
33
|
command: "cl",
|
|
@@ -49,8 +51,9 @@ function parseGccLikeOutput(raw: string, filePath: string): Diagnostic[] {
|
|
|
49
51
|
const resolvedTarget = path.resolve(filePath);
|
|
50
52
|
if (resolvedSource !== resolvedTarget) continue;
|
|
51
53
|
|
|
52
|
-
const severity =
|
|
53
|
-
|
|
54
|
+
const severity = severityLabel.toLowerCase().includes("error")
|
|
55
|
+
? "error"
|
|
56
|
+
: "warning";
|
|
54
57
|
diagnostics.push({
|
|
55
58
|
id: `cpp-check-${severityLabel}-${lineStr}-${colStr || "1"}`,
|
|
56
59
|
message: message.trim(),
|
|
@@ -79,8 +82,9 @@ function parseMsvcOutput(raw: string, filePath: string): Diagnostic[] {
|
|
|
79
82
|
const resolvedTarget = path.resolve(filePath);
|
|
80
83
|
if (resolvedSource !== resolvedTarget) continue;
|
|
81
84
|
|
|
82
|
-
const severity =
|
|
83
|
-
|
|
85
|
+
const severity = severityLabel.toLowerCase().includes("error")
|
|
86
|
+
? "error"
|
|
87
|
+
: "warning";
|
|
84
88
|
diagnostics.push({
|
|
85
89
|
id: `cpp-check-${rule}-${lineStr}-${colStr || "1"}`,
|
|
86
90
|
message: `[${rule}] ${message.trim()}`,
|
|
@@ -111,12 +115,12 @@ const cppCheckRunner: RunnerDefinition = {
|
|
|
111
115
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
112
116
|
const cwd = ctx.cwd || process.cwd();
|
|
113
117
|
const absPath = path.resolve(cwd, ctx.filePath);
|
|
114
|
-
const compiler = resolveCompiler(absPath);
|
|
118
|
+
const compiler = await resolveCompiler(absPath);
|
|
115
119
|
if (!compiler) {
|
|
116
120
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
const result =
|
|
123
|
+
const result = await safeSpawnAsync(compiler.command, compiler.args, {
|
|
120
124
|
cwd,
|
|
121
125
|
timeout: 30000,
|
|
122
126
|
});
|
|
@@ -148,7 +152,12 @@ const cppCheckRunner: RunnerDefinition = {
|
|
|
148
152
|
rawOutput: raw,
|
|
149
153
|
};
|
|
150
154
|
}
|
|
151
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
status: "succeeded",
|
|
157
|
+
diagnostics: [],
|
|
158
|
+
semantic: "none",
|
|
159
|
+
rawOutput: raw,
|
|
160
|
+
};
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
@@ -21,19 +21,24 @@ function parseDartMachineOutput(raw: string, filePath: string): Diagnostic[] {
|
|
|
21
21
|
const parts = line.split("|");
|
|
22
22
|
if (parts.length < 8) continue;
|
|
23
23
|
|
|
24
|
-
const [severityStr, , code, file, lineStr, colStr, , ...messageParts] =
|
|
24
|
+
const [severityStr, , code, file, lineStr, colStr, , ...messageParts] =
|
|
25
|
+
parts;
|
|
25
26
|
const message = messageParts.join("|").trim();
|
|
26
27
|
const lineNum = parseInt(lineStr, 10);
|
|
27
28
|
const colNum = parseInt(colStr, 10);
|
|
28
29
|
|
|
29
30
|
// Only include diagnostics for the target file
|
|
30
|
-
if (
|
|
31
|
+
if (
|
|
32
|
+
file &&
|
|
33
|
+
!path.resolve(file).endsWith(path.resolve(filePath).replace(/\\/g, "/"))
|
|
34
|
+
) {
|
|
31
35
|
const resolvedFile = path.resolve(file.trim());
|
|
32
36
|
const resolvedTarget = path.resolve(filePath);
|
|
33
37
|
if (resolvedFile !== resolvedTarget) continue;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
const severity =
|
|
40
|
+
const severity =
|
|
41
|
+
severityStr?.trim().toLowerCase() === "error" ? "error" : "warning";
|
|
37
42
|
diagnostics.push({
|
|
38
43
|
id: `dart-${code?.trim()}-${lineNum}-${colNum}`,
|
|
39
44
|
message: `[${code?.trim()}] ${message}`,
|
|
@@ -67,21 +72,22 @@ const dartAnalyzeRunner: RunnerDefinition = {
|
|
|
67
72
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
68
73
|
const cwd = ctx.cwd || process.cwd();
|
|
69
74
|
const absPath = path.resolve(cwd, ctx.filePath);
|
|
70
|
-
const dartAvailable = dart.
|
|
71
|
-
|
|
75
|
+
const dartAvailable = await (dart.isAvailableAsync?.(cwd) ??
|
|
76
|
+
dart.isAvailable(cwd));
|
|
77
|
+
const flutterAvailable =
|
|
78
|
+
!dartAvailable &&
|
|
79
|
+
(await (flutter.isAvailableAsync?.(cwd) ?? flutter.isAvailable(cwd)));
|
|
72
80
|
if (!dartAvailable && !flutterAvailable) {
|
|
73
81
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
74
82
|
}
|
|
75
|
-
const cmd = dartAvailable
|
|
83
|
+
const cmd = dartAvailable
|
|
84
|
+
? dart.getCommand(cwd)!
|
|
85
|
+
: flutter.getCommand(cwd)!;
|
|
76
86
|
const args = dartAvailable
|
|
77
87
|
? ["analyze", "--format=machine", absPath]
|
|
78
88
|
: ["analyze", "--machine", absPath];
|
|
79
89
|
|
|
80
|
-
const result = await safeSpawnAsync(
|
|
81
|
-
cmd,
|
|
82
|
-
args,
|
|
83
|
-
{ cwd, timeout: 30000 },
|
|
84
|
-
);
|
|
90
|
+
const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 30000 });
|
|
85
91
|
|
|
86
92
|
if (result.error && !result.stdout && !result.stderr) {
|
|
87
93
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -78,7 +78,10 @@ const detektRunner: RunnerDefinition = {
|
|
|
78
78
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const cmd =
|
|
81
|
+
const cmd = (await (detekt.isAvailableAsync?.(cwd) ??
|
|
82
|
+
detekt.isAvailable(cwd)))
|
|
83
|
+
? detekt.getCommand(cwd)
|
|
84
|
+
: null;
|
|
82
85
|
if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
83
86
|
|
|
84
87
|
const absPath = path.resolve(cwd, ctx.filePath);
|