pi-lens 3.8.43 → 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 +45 -7
- package/clients/ast-grep-client.ts +49 -17
- package/clients/dependency-checker.ts +82 -39
- 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/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/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/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 +61 -6
- package/clients/lsp/server.ts +40 -0
- package/clients/review-graph/builder.ts +9 -2
- package/clients/runtime-session.ts +15 -13
- package/clients/runtime-turn.ts +29 -17
- 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 +79 -4
- package/clients/tree-sitter-query-loader.ts +19 -4
- package/commands/booboo.ts +36 -10
- package/index.ts +3 -0
- 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/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,46 @@ 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
|
+
|
|
7
47
|
## [3.8.43] - 2026-05-10
|
|
8
48
|
|
|
9
49
|
### Added
|
|
@@ -33,11 +73,11 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
33
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.
|
|
34
74
|
- **Fixed `inline_tier: error` typo** on `ts-hallucinated-react-import` and `python-hallucinated-import` (→ `blocking`).
|
|
35
75
|
- **13 new high-confidence blocking promotions across 5 languages** (all `severity: error`, `inline_tier: blocking`):
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
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`
|
|
41
81
|
- **4 new blocking tree-sitter rules (SonarCloud BLOCKER equivalents)**:
|
|
42
82
|
- `ts-xss-dom-sink` (S5696) — flags dynamic values assigned to `innerHTML`/`outerHTML` or passed to `document.write()` / `document.writeln()`
|
|
43
83
|
- `ts-dynamic-require` (S5335) — flags `require()` called with a non-string-literal argument (arbitrary module loading)
|
|
@@ -60,8 +100,6 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
60
100
|
|
|
61
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.
|
|
62
102
|
|
|
63
|
-
|
|
64
|
-
|
|
65
103
|
### Added
|
|
66
104
|
|
|
67
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
|
|
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);
|