pi-lens 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Handles cross-platform path normalization, particularly
|
|
5
|
+
* Windows case-insensitivity issues when using paths as Map keys.
|
|
6
|
+
*
|
|
7
|
+
* Approach (inspired by OpenCode's Filesystem.normalizePath):
|
|
8
|
+
* - On Windows: try realpathSync.native() for canonical casing
|
|
9
|
+
* - Falls back to lowercase for files that don't exist yet
|
|
10
|
+
* - On non-Windows: return path as-is (case-sensitive filesystem)
|
|
11
|
+
* - Always convert backslashes to forward slashes for Map key consistency
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
14
|
+
import { dirname, win32 } from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
/**
|
|
17
|
+
* Detect if a path is a Windows path (has drive letter or UNC prefix).
|
|
18
|
+
*/
|
|
19
|
+
function isWindowsPath(filePath) {
|
|
20
|
+
return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\\\");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a file path for consistent Map key usage.
|
|
24
|
+
*
|
|
25
|
+
* On Windows:
|
|
26
|
+
* - If the file exists: uses realpathSync.native() to get the canonical
|
|
27
|
+
* filesystem path (actual casing, resolved symlinks)
|
|
28
|
+
* - If the file doesn't exist: resolves the path and lowercases
|
|
29
|
+
* (needed for new files where we haven't written yet)
|
|
30
|
+
*
|
|
31
|
+
* On non-Windows: returns path as-is (case-sensitive filesystem).
|
|
32
|
+
*
|
|
33
|
+
* Always converts backslashes to forward slashes for consistent Map keys.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeFilePath(filePath) {
|
|
36
|
+
// Convert backslashes to forward slashes first
|
|
37
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
38
|
+
if (process.platform !== "win32" && !isWindowsPath(normalized)) {
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
// Windows: try realpathSync.native() for canonical casing
|
|
42
|
+
// This resolves symlinks and returns the actual filesystem casing
|
|
43
|
+
try {
|
|
44
|
+
const canonical = realpathSync.native(filePath);
|
|
45
|
+
return canonical.replace(/\\/g, "/");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// File doesn't exist yet (new file) — resolve path and lowercase
|
|
49
|
+
// We need to walk up the directory tree to find the nearest existing
|
|
50
|
+
// parent, resolve its casing, then append the non-existent parts
|
|
51
|
+
try {
|
|
52
|
+
return resolveNonExisting(filePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Last resort: just lowercase the resolved path
|
|
56
|
+
const resolved = win32.normalize(win32.resolve(filePath));
|
|
57
|
+
return resolved.replace(/\\/g, "/").toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a non-existing path by finding the nearest existing parent,
|
|
63
|
+
* getting its canonical casing, then appending the non-existent parts lowercased.
|
|
64
|
+
*
|
|
65
|
+
* Example: C:\Users\Foo\newdir\file.ts
|
|
66
|
+
* - C:\Users\Foo exists → realpathSync gives C:\Users\Foo
|
|
67
|
+
* - newdir\file.ts doesn't exist → lowercased
|
|
68
|
+
* - Result: C:/Users/Foo/newdir/file.ts
|
|
69
|
+
*/
|
|
70
|
+
function resolveNonExisting(filePath) {
|
|
71
|
+
const resolved = win32.resolve(filePath);
|
|
72
|
+
let current = resolved;
|
|
73
|
+
const nonExistentParts = [];
|
|
74
|
+
// Walk up until we find an existing directory
|
|
75
|
+
while (true) {
|
|
76
|
+
if (existsSync(current)) {
|
|
77
|
+
// Found existing ancestor — get its canonical casing
|
|
78
|
+
const canonical = realpathSync.native(current);
|
|
79
|
+
if (nonExistentParts.length === 0) {
|
|
80
|
+
return canonical.replace(/\\/g, "/");
|
|
81
|
+
}
|
|
82
|
+
// Append non-existent parts (lowercased for consistency)
|
|
83
|
+
const tail = nonExistentParts.reverse().join("/").toLowerCase();
|
|
84
|
+
const base = canonical.replace(/\\/g, "/");
|
|
85
|
+
return base.endsWith("/") ? base + tail : `${base}/${tail}`;
|
|
86
|
+
}
|
|
87
|
+
const parent = dirname(current);
|
|
88
|
+
if (parent === current) {
|
|
89
|
+
// Reached filesystem root without finding existing dir
|
|
90
|
+
// Fall back to full lowercase
|
|
91
|
+
throw new Error("No existing parent found");
|
|
92
|
+
}
|
|
93
|
+
nonExistentParts.push(win32.basename(current));
|
|
94
|
+
current = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Convert a file:// URI to a normalized path.
|
|
99
|
+
* Handles URL decoding and Windows drive letter normalization.
|
|
100
|
+
*/
|
|
101
|
+
export function uriToPath(uri) {
|
|
102
|
+
try {
|
|
103
|
+
const filePath = fileURLToPath(uri);
|
|
104
|
+
return normalizeFilePath(filePath);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Not a valid file:// URI, treat as plain path
|
|
108
|
+
return normalizeFilePath(uri);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convert a path to a file:// URI.
|
|
113
|
+
* Does NOT normalize the path - URIs preserve original casing.
|
|
114
|
+
*/
|
|
115
|
+
export function pathToUri(filePath) {
|
|
116
|
+
return pathToFileURL(filePath).href;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Normalize a Map key lookup for file paths.
|
|
120
|
+
* Use this when getting/setting values in Maps that use file paths as keys.
|
|
121
|
+
*/
|
|
122
|
+
export function normalizeMapKey(filePath) {
|
|
123
|
+
return normalizeFilePath(filePath);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compare two file paths for equality, handling Windows case-insensitivity
|
|
127
|
+
* and mixed separators (backslash vs forward slash).
|
|
128
|
+
*/
|
|
129
|
+
export function pathsEqual(a, b) {
|
|
130
|
+
return normalizeFilePath(a) === normalizeFilePath(b);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if `child` is under `parent` directory.
|
|
134
|
+
* Separator-agnostic and case-insensitive on Windows.
|
|
135
|
+
*/
|
|
136
|
+
export function isUnderDir(child, parent) {
|
|
137
|
+
const normChild = normalizeFilePath(child);
|
|
138
|
+
const normParent = normalizeFilePath(parent);
|
|
139
|
+
// Ensure parent ends with / for prefix matching
|
|
140
|
+
const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
|
|
141
|
+
return normChild === normParent || normChild.startsWith(parentPrefix);
|
|
142
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Handles cross-platform path normalization, particularly
|
|
5
|
+
* Windows case-insensitivity issues when using paths as Map keys.
|
|
6
|
+
*
|
|
7
|
+
* Approach (inspired by OpenCode's Filesystem.normalizePath):
|
|
8
|
+
* - On Windows: try realpathSync.native() for canonical casing
|
|
9
|
+
* - Falls back to lowercase for files that don't exist yet
|
|
10
|
+
* - On non-Windows: return path as-is (case-sensitive filesystem)
|
|
11
|
+
* - Always convert backslashes to forward slashes for Map key consistency
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
15
|
+
import { dirname, win32 } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect if a path is a Windows path (has drive letter or UNC prefix).
|
|
20
|
+
*/
|
|
21
|
+
function isWindowsPath(filePath: string): boolean {
|
|
22
|
+
return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\\\");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a file path for consistent Map key usage.
|
|
27
|
+
*
|
|
28
|
+
* On Windows:
|
|
29
|
+
* - If the file exists: uses realpathSync.native() to get the canonical
|
|
30
|
+
* filesystem path (actual casing, resolved symlinks)
|
|
31
|
+
* - If the file doesn't exist: resolves the path and lowercases
|
|
32
|
+
* (needed for new files where we haven't written yet)
|
|
33
|
+
*
|
|
34
|
+
* On non-Windows: returns path as-is (case-sensitive filesystem).
|
|
35
|
+
*
|
|
36
|
+
* Always converts backslashes to forward slashes for consistent Map keys.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeFilePath(filePath: string): string {
|
|
39
|
+
// Convert backslashes to forward slashes first
|
|
40
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
41
|
+
|
|
42
|
+
if (process.platform !== "win32" && !isWindowsPath(normalized)) {
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Windows: try realpathSync.native() for canonical casing
|
|
47
|
+
// This resolves symlinks and returns the actual filesystem casing
|
|
48
|
+
try {
|
|
49
|
+
const canonical = realpathSync.native(filePath);
|
|
50
|
+
return canonical.replace(/\\/g, "/");
|
|
51
|
+
} catch {
|
|
52
|
+
// File doesn't exist yet (new file) — resolve path and lowercase
|
|
53
|
+
// We need to walk up the directory tree to find the nearest existing
|
|
54
|
+
// parent, resolve its casing, then append the non-existent parts
|
|
55
|
+
try {
|
|
56
|
+
return resolveNonExisting(filePath);
|
|
57
|
+
} catch {
|
|
58
|
+
// Last resort: just lowercase the resolved path
|
|
59
|
+
const resolved = win32.normalize(win32.resolve(filePath));
|
|
60
|
+
return resolved.replace(/\\/g, "/").toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a non-existing path by finding the nearest existing parent,
|
|
67
|
+
* getting its canonical casing, then appending the non-existent parts lowercased.
|
|
68
|
+
*
|
|
69
|
+
* Example: C:\Users\Foo\newdir\file.ts
|
|
70
|
+
* - C:\Users\Foo exists → realpathSync gives C:\Users\Foo
|
|
71
|
+
* - newdir\file.ts doesn't exist → lowercased
|
|
72
|
+
* - Result: C:/Users/Foo/newdir/file.ts
|
|
73
|
+
*/
|
|
74
|
+
function resolveNonExisting(filePath: string): string {
|
|
75
|
+
const resolved = win32.resolve(filePath);
|
|
76
|
+
let current = resolved;
|
|
77
|
+
const nonExistentParts: string[] = [];
|
|
78
|
+
|
|
79
|
+
// Walk up until we find an existing directory
|
|
80
|
+
while (true) {
|
|
81
|
+
if (existsSync(current)) {
|
|
82
|
+
// Found existing ancestor — get its canonical casing
|
|
83
|
+
const canonical = realpathSync.native(current);
|
|
84
|
+
if (nonExistentParts.length === 0) {
|
|
85
|
+
return canonical.replace(/\\/g, "/");
|
|
86
|
+
}
|
|
87
|
+
// Append non-existent parts (lowercased for consistency)
|
|
88
|
+
const tail = nonExistentParts.reverse().join("/").toLowerCase();
|
|
89
|
+
const base = canonical.replace(/\\/g, "/");
|
|
90
|
+
return base.endsWith("/") ? base + tail : `${base}/${tail}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parent = dirname(current);
|
|
94
|
+
if (parent === current) {
|
|
95
|
+
// Reached filesystem root without finding existing dir
|
|
96
|
+
// Fall back to full lowercase
|
|
97
|
+
throw new Error("No existing parent found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
nonExistentParts.push(win32.basename(current));
|
|
101
|
+
current = parent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convert a file:// URI to a normalized path.
|
|
107
|
+
* Handles URL decoding and Windows drive letter normalization.
|
|
108
|
+
*/
|
|
109
|
+
export function uriToPath(uri: string): string {
|
|
110
|
+
try {
|
|
111
|
+
const filePath = fileURLToPath(uri);
|
|
112
|
+
return normalizeFilePath(filePath);
|
|
113
|
+
} catch {
|
|
114
|
+
// Not a valid file:// URI, treat as plain path
|
|
115
|
+
return normalizeFilePath(uri);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Convert a path to a file:// URI.
|
|
121
|
+
* Does NOT normalize the path - URIs preserve original casing.
|
|
122
|
+
*/
|
|
123
|
+
export function pathToUri(filePath: string): string {
|
|
124
|
+
return pathToFileURL(filePath).href;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalize a Map key lookup for file paths.
|
|
129
|
+
* Use this when getting/setting values in Maps that use file paths as keys.
|
|
130
|
+
*/
|
|
131
|
+
export function normalizeMapKey(filePath: string): string {
|
|
132
|
+
return normalizeFilePath(filePath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compare two file paths for equality, handling Windows case-insensitivity
|
|
137
|
+
* and mixed separators (backslash vs forward slash).
|
|
138
|
+
*/
|
|
139
|
+
export function pathsEqual(a: string, b: string): boolean {
|
|
140
|
+
return normalizeFilePath(a) === normalizeFilePath(b);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if `child` is under `parent` directory.
|
|
145
|
+
* Separator-agnostic and case-insensitive on Windows.
|
|
146
|
+
*/
|
|
147
|
+
export function isUnderDir(child: string, parent: string): boolean {
|
|
148
|
+
const normChild = normalizeFilePath(child);
|
|
149
|
+
const normParent = normalizeFilePath(parent);
|
|
150
|
+
// Ensure parent ends with / for prefix matching
|
|
151
|
+
const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
|
|
152
|
+
return normChild === normParent || normChild.startsWith(parentPrefix);
|
|
153
|
+
}
|
package/clients/ruff-client.js
CHANGED
|
@@ -20,7 +20,36 @@ export class RuffClient {
|
|
|
20
20
|
: () => { };
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Check if ruff CLI is available
|
|
23
|
+
* Check if ruff CLI is available, auto-install if not
|
|
24
|
+
*/
|
|
25
|
+
async ensureAvailable() {
|
|
26
|
+
// Fast path: already checked
|
|
27
|
+
if (this.ruffAvailable !== null)
|
|
28
|
+
return this.ruffAvailable;
|
|
29
|
+
// Check if available in PATH
|
|
30
|
+
const result = safeSpawn("ruff", ["--version"], {
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
});
|
|
33
|
+
this.ruffAvailable = !result.error && result.status === 0;
|
|
34
|
+
if (this.ruffAvailable) {
|
|
35
|
+
this.log(`Ruff found: ${result.stdout.trim()}`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Auto-install via pi-lens installer
|
|
39
|
+
this.log("Ruff not found, attempting auto-install...");
|
|
40
|
+
const { ensureTool } = await import("./installer/index.js");
|
|
41
|
+
const installedPath = await ensureTool("ruff");
|
|
42
|
+
if (installedPath) {
|
|
43
|
+
this.log(`Ruff auto-installed: ${installedPath}`);
|
|
44
|
+
this.ruffAvailable = true;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
this.log("Ruff auto-install failed");
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if ruff CLI is available (legacy sync method)
|
|
52
|
+
* Prefer ensureAvailable() for auto-install behavior
|
|
24
53
|
*/
|
|
25
54
|
isAvailable() {
|
|
26
55
|
if (this.ruffAvailable !== null)
|
|
@@ -166,8 +195,8 @@ export class RuffClient {
|
|
|
166
195
|
}
|
|
167
196
|
// Filter to existing Python files
|
|
168
197
|
const validFiles = filePaths
|
|
169
|
-
.map(f => path.resolve(f))
|
|
170
|
-
.filter(f => fs.existsSync(f) && f.endsWith(".py"));
|
|
198
|
+
.map((f) => path.resolve(f))
|
|
199
|
+
.filter((f) => fs.existsSync(f) && f.endsWith(".py"));
|
|
171
200
|
if (validFiles.length === 0) {
|
|
172
201
|
return { success: true, fixed: 0, changed: 0 };
|
|
173
202
|
}
|
|
@@ -176,7 +205,7 @@ export class RuffClient {
|
|
|
176
205
|
let totalFixable = 0;
|
|
177
206
|
for (const file of validFiles) {
|
|
178
207
|
const diags = this.checkFile(file);
|
|
179
|
-
totalFixable += diags.filter(d => d.fixable).length;
|
|
208
|
+
totalFixable += diags.filter((d) => d.fixable).length;
|
|
180
209
|
}
|
|
181
210
|
// Run ruff once on all files - much faster than per file
|
|
182
211
|
const result = safeSpawn("ruff", ["check", "--fix", ...validFiles], {
|
package/clients/ruff-client.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* Docs: https://docs.astral.sh/ruff/
|
|
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 { isFileKind } from "./file-kinds.js";
|
|
@@ -51,7 +50,41 @@ export class RuffClient {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
|
-
* Check if ruff CLI is available
|
|
53
|
+
* Check if ruff CLI is available, auto-install if not
|
|
54
|
+
*/
|
|
55
|
+
async ensureAvailable(): Promise<boolean> {
|
|
56
|
+
// Fast path: already checked
|
|
57
|
+
if (this.ruffAvailable !== null) return this.ruffAvailable;
|
|
58
|
+
|
|
59
|
+
// Check if available in PATH
|
|
60
|
+
const result = safeSpawn("ruff", ["--version"], {
|
|
61
|
+
timeout: 5000,
|
|
62
|
+
});
|
|
63
|
+
this.ruffAvailable = !result.error && result.status === 0;
|
|
64
|
+
|
|
65
|
+
if (this.ruffAvailable) {
|
|
66
|
+
this.log(`Ruff found: ${result.stdout.trim()}`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Auto-install via pi-lens installer
|
|
71
|
+
this.log("Ruff not found, attempting auto-install...");
|
|
72
|
+
const { ensureTool } = await import("./installer/index.js");
|
|
73
|
+
const installedPath = await ensureTool("ruff");
|
|
74
|
+
|
|
75
|
+
if (installedPath) {
|
|
76
|
+
this.log(`Ruff auto-installed: ${installedPath}`);
|
|
77
|
+
this.ruffAvailable = true;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.log("Ruff auto-install failed");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if ruff CLI is available (legacy sync method)
|
|
87
|
+
* Prefer ensureAvailable() for auto-install behavior
|
|
55
88
|
*/
|
|
56
89
|
isAvailable(): boolean {
|
|
57
90
|
if (this.ruffAvailable !== null) return this.ruffAvailable;
|
|
@@ -230,8 +263,8 @@ export class RuffClient {
|
|
|
230
263
|
|
|
231
264
|
// Filter to existing Python files
|
|
232
265
|
const validFiles = filePaths
|
|
233
|
-
.map(f => path.resolve(f))
|
|
234
|
-
.filter(f => fs.existsSync(f) && f.endsWith(".py"));
|
|
266
|
+
.map((f) => path.resolve(f))
|
|
267
|
+
.filter((f) => fs.existsSync(f) && f.endsWith(".py"));
|
|
235
268
|
|
|
236
269
|
if (validFiles.length === 0) {
|
|
237
270
|
return { success: true, fixed: 0, changed: 0 };
|
|
@@ -242,17 +275,13 @@ export class RuffClient {
|
|
|
242
275
|
let totalFixable = 0;
|
|
243
276
|
for (const file of validFiles) {
|
|
244
277
|
const diags = this.checkFile(file);
|
|
245
|
-
totalFixable += diags.filter(d => d.fixable).length;
|
|
278
|
+
totalFixable += diags.filter((d) => d.fixable).length;
|
|
246
279
|
}
|
|
247
280
|
|
|
248
281
|
// Run ruff once on all files - much faster than per file
|
|
249
|
-
const result = safeSpawn(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
{
|
|
253
|
-
timeout: 60000, // Longer timeout for batch
|
|
254
|
-
},
|
|
255
|
-
);
|
|
282
|
+
const result = safeSpawn("ruff", ["check", "--fix", ...validFiles], {
|
|
283
|
+
timeout: 60000, // Longer timeout for batch
|
|
284
|
+
});
|
|
256
285
|
|
|
257
286
|
if (result.error) {
|
|
258
287
|
return {
|
|
@@ -263,7 +292,9 @@ export class RuffClient {
|
|
|
263
292
|
};
|
|
264
293
|
}
|
|
265
294
|
|
|
266
|
-
this.log(
|
|
295
|
+
this.log(
|
|
296
|
+
`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`,
|
|
297
|
+
);
|
|
267
298
|
|
|
268
299
|
return { success: true, fixed: totalFixable, changed: validFiles.length };
|
|
269
300
|
} catch (err: any) {
|
package/clients/safe-spawn.js
CHANGED
|
@@ -48,11 +48,13 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
48
48
|
let timedOut = false;
|
|
49
49
|
let killed = false;
|
|
50
50
|
// Spawn the process (non-blocking)
|
|
51
|
+
// On Windows, use shell mode for .cmd files (like pyright, biome)
|
|
52
|
+
const isWindows = process.platform === "win32";
|
|
51
53
|
const child = spawn(command, args, {
|
|
52
54
|
cwd: options?.cwd,
|
|
53
55
|
env: { ...process.env, ...options?.env },
|
|
54
56
|
windowsHide: true,
|
|
55
|
-
shell:
|
|
57
|
+
shell: isWindows,
|
|
56
58
|
});
|
|
57
59
|
// Handle abort signal
|
|
58
60
|
const onAbort = () => {
|
package/clients/safe-spawn.ts
CHANGED
|
@@ -72,11 +72,13 @@ export async function safeSpawnAsync(
|
|
|
72
72
|
let killed = false;
|
|
73
73
|
|
|
74
74
|
// Spawn the process (non-blocking)
|
|
75
|
+
// On Windows, use shell mode for .cmd files (like pyright, biome)
|
|
76
|
+
const isWindows = process.platform === "win32";
|
|
75
77
|
const child = spawn(command, args, {
|
|
76
78
|
cwd: options?.cwd,
|
|
77
79
|
env: { ...process.env, ...options?.env },
|
|
78
80
|
windowsHide: true,
|
|
79
|
-
shell:
|
|
81
|
+
shell: isWindows,
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
// Handle abort signal
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - Graceful error recovery
|
|
10
10
|
* - Bus event integration
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { DiagnosticFound, RunnerStarted, RunnerCompleted, FileModified, } from "../bus/events.js";
|
|
12
|
+
import { DiagnosticFound, FileModified, RunnerCompleted, RunnerStarted, } from "../bus/events.js";
|
|
14
13
|
import { formatDiagnostics } from "../dispatch/utils/format-utils.js";
|
|
14
|
+
import { executeEffect, runRunnersConcurrent, } from "./runner-service.js";
|
|
15
15
|
// Import runners to register them in the dispatcher
|
|
16
16
|
import "../dispatch/runners/index.js";
|
|
17
17
|
// --- Core Functions ---
|
|
@@ -20,11 +20,10 @@ import "../dispatch/runners/index.js";
|
|
|
20
20
|
*/
|
|
21
21
|
async function runGroupConcurrent(ctx, group) {
|
|
22
22
|
const { getRunner, getRunnersForKind } = await import("../dispatch/dispatcher.js");
|
|
23
|
-
const
|
|
23
|
+
const _startTime = Date.now();
|
|
24
24
|
// Get runner definitions
|
|
25
25
|
const runnerDefs = group.filterKinds
|
|
26
|
-
? group.runnerIds
|
|
27
|
-
.filter((id) => {
|
|
26
|
+
? group.runnerIds.filter((id) => {
|
|
28
27
|
const runner = getRunner(id);
|
|
29
28
|
return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
|
|
30
29
|
})
|
|
@@ -59,7 +58,11 @@ async function runGroupConcurrent(ctx, group) {
|
|
|
59
58
|
filePath: ctx.filePath,
|
|
60
59
|
line: d.line,
|
|
61
60
|
column: d.column,
|
|
62
|
-
severity: d.severity === "error"
|
|
61
|
+
severity: d.severity === "error"
|
|
62
|
+
? "error"
|
|
63
|
+
: d.severity === "warning"
|
|
64
|
+
? "warning"
|
|
65
|
+
: "info",
|
|
63
66
|
semantic: d.semantic ?? result.semantic ?? "warning",
|
|
64
67
|
tool: runnerId,
|
|
65
68
|
rule: d.rule,
|
|
@@ -182,7 +185,8 @@ export async function dispatchWithEffect(ctx, groups) {
|
|
|
182
185
|
export async function dispatchLintWithEffect(filePath, cwd, pi) {
|
|
183
186
|
const { createDispatchContext } = await import("../dispatch/dispatcher.js");
|
|
184
187
|
const { TOOL_PLANS } = await import("../dispatch/plan.js");
|
|
185
|
-
|
|
188
|
+
// blockingOnly=true: post-write dispatch only reports blocking errors (same as standard dispatchLint)
|
|
189
|
+
const ctx = createDispatchContext(filePath, cwd, pi, undefined, true);
|
|
186
190
|
const kind = ctx.kind;
|
|
187
191
|
if (!kind)
|
|
188
192
|
return "";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Effect-TS Integration for pi-lens Dispatch
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Bridges the Effect service layer with the existing dispatch system.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* This provides:
|
|
7
7
|
* - Concurrent runner execution with Effect.all
|
|
8
8
|
* - Timeout handling for slow runners
|
|
@@ -11,21 +11,19 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
executeEffect,
|
|
16
|
-
formatError,
|
|
17
|
-
type RunnerResult,
|
|
18
|
-
type ConcurrentRunnerResult,
|
|
19
|
-
} from "./runner-service.js";
|
|
20
|
-
|
|
21
|
-
import {
|
|
14
|
+
type Diagnostic,
|
|
22
15
|
DiagnosticFound,
|
|
23
|
-
RunnerStarted,
|
|
24
|
-
RunnerCompleted,
|
|
25
16
|
FileModified,
|
|
26
|
-
|
|
17
|
+
RunnerCompleted,
|
|
18
|
+
RunnerStarted,
|
|
27
19
|
} from "../bus/events.js";
|
|
28
|
-
import {
|
|
20
|
+
import { formatDiagnostics } from "../dispatch/utils/format-utils.js";
|
|
21
|
+
import {
|
|
22
|
+
type ConcurrentRunnerResult,
|
|
23
|
+
executeEffect,
|
|
24
|
+
type RunnerResult,
|
|
25
|
+
runRunnersConcurrent,
|
|
26
|
+
} from "./runner-service.js";
|
|
29
27
|
// Import runners to register them in the dispatcher
|
|
30
28
|
import "../dispatch/runners/index.js";
|
|
31
29
|
|
|
@@ -54,16 +52,17 @@ async function runGroupConcurrent(
|
|
|
54
52
|
ctx: DispatchContext,
|
|
55
53
|
group: RunnerGroup,
|
|
56
54
|
): Promise<{ results: ConcurrentRunnerResult[]; diagnostics: Diagnostic[] }> {
|
|
57
|
-
const { getRunner, getRunnersForKind } = await import(
|
|
58
|
-
|
|
55
|
+
const { getRunner, getRunnersForKind } = await import(
|
|
56
|
+
"../dispatch/dispatcher.js"
|
|
57
|
+
);
|
|
58
|
+
const _startTime = Date.now();
|
|
59
59
|
|
|
60
60
|
// Get runner definitions
|
|
61
61
|
const runnerDefs = group.filterKinds
|
|
62
|
-
? group.runnerIds
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
62
|
+
? group.runnerIds.filter((id) => {
|
|
63
|
+
const runner = getRunner(id);
|
|
64
|
+
return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
|
|
65
|
+
})
|
|
67
66
|
: group.runnerIds;
|
|
68
67
|
|
|
69
68
|
const runners = runnerDefs
|
|
@@ -76,7 +75,10 @@ async function runGroupConcurrent(
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
// Build the single runner execution function
|
|
79
|
-
const runSingle = async (
|
|
78
|
+
const runSingle = async (
|
|
79
|
+
filePath: string,
|
|
80
|
+
runnerId: string,
|
|
81
|
+
): Promise<RunnerResult> => {
|
|
80
82
|
const runner = getRunner(runnerId);
|
|
81
83
|
if (!runner) {
|
|
82
84
|
return { diagnostics: [], durationMs: 0 };
|
|
@@ -101,7 +103,12 @@ async function runGroupConcurrent(
|
|
|
101
103
|
filePath: ctx.filePath,
|
|
102
104
|
line: d.line,
|
|
103
105
|
column: d.column,
|
|
104
|
-
severity:
|
|
106
|
+
severity:
|
|
107
|
+
d.severity === "error"
|
|
108
|
+
? "error"
|
|
109
|
+
: d.severity === "warning"
|
|
110
|
+
? "warning"
|
|
111
|
+
: "info",
|
|
105
112
|
semantic: d.semantic ?? result.semantic ?? "warning",
|
|
106
113
|
tool: runnerId,
|
|
107
114
|
rule: d.rule,
|
|
@@ -150,7 +157,7 @@ async function runGroupConcurrent(
|
|
|
150
157
|
// Run all runners concurrently using Effect
|
|
151
158
|
const runnerIds = runners.map((r) => r.id);
|
|
152
159
|
const concurrentResults = await executeEffect(
|
|
153
|
-
runRunnersConcurrent(ctx.filePath, runnerIds, runSingle, 30_000)
|
|
160
|
+
runRunnersConcurrent(ctx.filePath, runnerIds, runSingle, 30_000),
|
|
154
161
|
);
|
|
155
162
|
|
|
156
163
|
// Collect all diagnostics
|
|
@@ -165,7 +172,7 @@ async function runGroupConcurrent(
|
|
|
165
172
|
severity: d.severity,
|
|
166
173
|
semantic: d.semantic ?? group.semantic ?? "warning",
|
|
167
174
|
tool: result.runnerId,
|
|
168
|
-
}))
|
|
175
|
+
})),
|
|
169
176
|
);
|
|
170
177
|
}
|
|
171
178
|
}
|
|
@@ -255,7 +262,8 @@ export async function dispatchLintWithEffect(
|
|
|
255
262
|
const { createDispatchContext } = await import("../dispatch/dispatcher.js");
|
|
256
263
|
const { TOOL_PLANS } = await import("../dispatch/plan.js");
|
|
257
264
|
|
|
258
|
-
|
|
265
|
+
// blockingOnly=true: post-write dispatch only reports blocking errors (same as standard dispatchLint)
|
|
266
|
+
const ctx = createDispatchContext(filePath, cwd, pi, undefined, true);
|
|
259
267
|
|
|
260
268
|
const kind = ctx.kind;
|
|
261
269
|
if (!kind) return "";
|