pi-lens 2.2.9 → 3.0.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 +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruff Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Fast Python linting and formatting via Ruff CLI.
|
|
5
|
+
* Replaces flake8, pylint, isort, black, pyupgrade.
|
|
6
|
+
*
|
|
7
|
+
* Requires: pip install ruff
|
|
8
|
+
* Docs: https://docs.astral.sh/ruff/
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { isFileKind } from "./file-kinds.js";
|
|
13
|
+
import { safeSpawn } from "./safe-spawn.js";
|
|
14
|
+
// --- Client ---
|
|
15
|
+
export class RuffClient {
|
|
16
|
+
ruffAvailable = null;
|
|
17
|
+
log;
|
|
18
|
+
constructor(verbose = false) {
|
|
19
|
+
this.log = verbose
|
|
20
|
+
? (msg) => console.error(`[ruff] ${msg}`)
|
|
21
|
+
: () => { };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if ruff CLI is available
|
|
25
|
+
*/
|
|
26
|
+
isAvailable() {
|
|
27
|
+
if (this.ruffAvailable !== null)
|
|
28
|
+
return this.ruffAvailable;
|
|
29
|
+
try {
|
|
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
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
void err;
|
|
40
|
+
this.ruffAvailable = false;
|
|
41
|
+
}
|
|
42
|
+
return this.ruffAvailable;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a file is a Python file
|
|
46
|
+
*/
|
|
47
|
+
isPythonFile(filePath) {
|
|
48
|
+
return isFileKind(filePath, "python");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Lint a Python file
|
|
52
|
+
*/
|
|
53
|
+
checkFile(filePath) {
|
|
54
|
+
if (!this.isAvailable())
|
|
55
|
+
return [];
|
|
56
|
+
const absolutePath = path.resolve(filePath);
|
|
57
|
+
if (!fs.existsSync(absolutePath))
|
|
58
|
+
return [];
|
|
59
|
+
try {
|
|
60
|
+
const result = safeSpawn("ruff", [
|
|
61
|
+
"check",
|
|
62
|
+
"--output-format",
|
|
63
|
+
"json",
|
|
64
|
+
"--target-version",
|
|
65
|
+
"py310",
|
|
66
|
+
absolutePath,
|
|
67
|
+
], {
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
});
|
|
70
|
+
// ruff exits 1 when it finds issues (normal)
|
|
71
|
+
const output = result.stdout || "";
|
|
72
|
+
if (!output.trim())
|
|
73
|
+
return [];
|
|
74
|
+
return this.parseOutput(output, absolutePath);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
this.log(`Check error: ${err.message}`);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if file has formatting issues (ruff format --check)
|
|
83
|
+
*/
|
|
84
|
+
checkFormatting(filePath) {
|
|
85
|
+
if (!this.isAvailable())
|
|
86
|
+
return "";
|
|
87
|
+
const absolutePath = path.resolve(filePath);
|
|
88
|
+
if (!fs.existsSync(absolutePath))
|
|
89
|
+
return "";
|
|
90
|
+
try {
|
|
91
|
+
const result = safeSpawn("ruff", ["format", "--check", "--diff", absolutePath], {
|
|
92
|
+
timeout: 10000,
|
|
93
|
+
});
|
|
94
|
+
// ruff format --check exits 1 when changes needed
|
|
95
|
+
if (result.status === 0)
|
|
96
|
+
return "";
|
|
97
|
+
const diff = result.stdout || "";
|
|
98
|
+
if (!diff.trim())
|
|
99
|
+
return "";
|
|
100
|
+
// Count lines that would change
|
|
101
|
+
const diffLines = diff
|
|
102
|
+
.split("\n")
|
|
103
|
+
.filter((l) => l.startsWith("+") || l.startsWith("-")).length;
|
|
104
|
+
return `[Ruff Format] ${diffLines} line(s) would change — run 'ruff format ${path.basename(filePath)}' to fix`;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
void err;
|
|
108
|
+
return "";
|
|
109
|
+
} // Intentionally return empty string on diff failure
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Auto-fix linting issues (writes to disk)
|
|
113
|
+
*/
|
|
114
|
+
fixFile(filePath) {
|
|
115
|
+
if (!this.isAvailable())
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
changed: false,
|
|
119
|
+
fixed: 0,
|
|
120
|
+
error: "Ruff not available",
|
|
121
|
+
};
|
|
122
|
+
const absolutePath = path.resolve(filePath);
|
|
123
|
+
if (!fs.existsSync(absolutePath))
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
changed: false,
|
|
127
|
+
fixed: 0,
|
|
128
|
+
error: "File not found",
|
|
129
|
+
};
|
|
130
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
131
|
+
try {
|
|
132
|
+
const beforeDiags = this.checkFile(filePath);
|
|
133
|
+
const fixableCount = beforeDiags.filter((d) => d.fixable).length;
|
|
134
|
+
const result = safeSpawn("ruff", ["check", "--fix", absolutePath], {
|
|
135
|
+
timeout: 15000,
|
|
136
|
+
});
|
|
137
|
+
if (result.error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
changed: false,
|
|
141
|
+
fixed: 0,
|
|
142
|
+
error: result.error.message,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const fixed = fs.readFileSync(absolutePath, "utf-8");
|
|
146
|
+
const changed = content !== fixed;
|
|
147
|
+
if (changed) {
|
|
148
|
+
this.log(`Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`);
|
|
149
|
+
}
|
|
150
|
+
return { success: true, changed, fixed: fixableCount };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return { success: false, changed: false, fixed: 0, error: err.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fix multiple Python files at once (much faster than file-by-file)
|
|
158
|
+
*/
|
|
159
|
+
fixFiles(filePaths) {
|
|
160
|
+
if (!this.isAvailable()) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
fixed: 0,
|
|
164
|
+
changed: 0,
|
|
165
|
+
error: "Ruff not available",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Filter to existing Python files
|
|
169
|
+
const validFiles = filePaths
|
|
170
|
+
.map(f => path.resolve(f))
|
|
171
|
+
.filter(f => fs.existsSync(f) && f.endsWith(".py"));
|
|
172
|
+
if (validFiles.length === 0) {
|
|
173
|
+
return { success: true, fixed: 0, changed: 0 };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Count fixable issues before fixing
|
|
177
|
+
let totalFixable = 0;
|
|
178
|
+
for (const file of validFiles) {
|
|
179
|
+
const diags = this.checkFile(file);
|
|
180
|
+
totalFixable += diags.filter(d => d.fixable).length;
|
|
181
|
+
}
|
|
182
|
+
// Run ruff once on all files - much faster than per file
|
|
183
|
+
const result = safeSpawn("ruff", ["check", "--fix", ...validFiles], {
|
|
184
|
+
timeout: 60000, // Longer timeout for batch
|
|
185
|
+
});
|
|
186
|
+
if (result.error) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
fixed: 0,
|
|
190
|
+
changed: 0,
|
|
191
|
+
error: result.error.message,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
|
|
195
|
+
return { success: true, fixed: totalFixable, changed: validFiles.length };
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
fixed: 0,
|
|
201
|
+
changed: 0,
|
|
202
|
+
error: err.message,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Format a Python file (writes to disk)
|
|
208
|
+
*/
|
|
209
|
+
formatFile(filePath) {
|
|
210
|
+
if (!this.isAvailable())
|
|
211
|
+
return { success: false, changed: false, error: "Ruff not available" };
|
|
212
|
+
const absolutePath = path.resolve(filePath);
|
|
213
|
+
if (!fs.existsSync(absolutePath))
|
|
214
|
+
return { success: false, changed: false, error: "File not found" };
|
|
215
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
216
|
+
try {
|
|
217
|
+
const result = safeSpawn("ruff", ["format", absolutePath], {
|
|
218
|
+
timeout: 10000,
|
|
219
|
+
});
|
|
220
|
+
if (result.error) {
|
|
221
|
+
return { success: false, changed: false, error: result.error.message };
|
|
222
|
+
}
|
|
223
|
+
const formatted = fs.readFileSync(absolutePath, "utf-8");
|
|
224
|
+
const changed = content !== formatted;
|
|
225
|
+
if (changed) {
|
|
226
|
+
this.log(`Formatted ${path.basename(filePath)}`);
|
|
227
|
+
}
|
|
228
|
+
return { success: true, changed };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return { success: false, changed: false, error: err.message };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Format diagnostics for LLM consumption
|
|
236
|
+
*/
|
|
237
|
+
formatDiagnostics(diags) {
|
|
238
|
+
if (diags.length === 0)
|
|
239
|
+
return "";
|
|
240
|
+
const errors = diags.filter((d) => d.severity === "error");
|
|
241
|
+
const warnings = diags.filter((d) => d.severity === "warning");
|
|
242
|
+
const fixable = diags.filter((d) => d.fixable);
|
|
243
|
+
let result = `[Ruff] ${diags.length} issue(s)`;
|
|
244
|
+
if (errors.length)
|
|
245
|
+
result += ` — ${errors.length} error(s)`;
|
|
246
|
+
if (warnings.length)
|
|
247
|
+
result += ` — ${warnings.length} warning(s)`;
|
|
248
|
+
if (fixable.length)
|
|
249
|
+
result += ` — ${fixable.length} auto-fixable`;
|
|
250
|
+
result += ":\n";
|
|
251
|
+
for (const d of diags.slice(0, 15)) {
|
|
252
|
+
const loc = d.line === d.endLine
|
|
253
|
+
? `L${d.line}:${d.column}-${d.endColumn}`
|
|
254
|
+
: `L${d.line}:${d.column}-L${d.endLine}:${d.endColumn}`;
|
|
255
|
+
const fix = d.fixable ? " [fixable]" : "";
|
|
256
|
+
result += ` [${d.rule}] ${loc} ${d.message}${fix}\n`;
|
|
257
|
+
}
|
|
258
|
+
if (diags.length > 15) {
|
|
259
|
+
result += ` ... and ${diags.length - 15} more\n`;
|
|
260
|
+
}
|
|
261
|
+
if (fixable.length > 0) {
|
|
262
|
+
result += `\n Run 'ruff check --fix ${path.basename(diags[0].file)}' to auto-fix ${fixable.length} issue(s)\n`;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
// --- Internal ---
|
|
267
|
+
parseOutput(output, filterFile) {
|
|
268
|
+
if (!output.trim())
|
|
269
|
+
return [];
|
|
270
|
+
try {
|
|
271
|
+
const items = JSON.parse(output);
|
|
272
|
+
const diagnostics = [];
|
|
273
|
+
for (const item of items) {
|
|
274
|
+
// Filter to single file if requested
|
|
275
|
+
if (filterFile && path.resolve(item.filename) !== filterFile)
|
|
276
|
+
continue;
|
|
277
|
+
diagnostics.push({
|
|
278
|
+
line: item.location.row - 1, // ruff is 1-indexed
|
|
279
|
+
column: item.location.column - 1,
|
|
280
|
+
endLine: item.end_location.row - 1,
|
|
281
|
+
endColumn: item.end_location.column - 1,
|
|
282
|
+
severity: item.code?.startsWith("E") ? "error" : "warning",
|
|
283
|
+
message: item.message,
|
|
284
|
+
rule: item.code || "unknown",
|
|
285
|
+
file: item.filename,
|
|
286
|
+
fixable: item.fix !== null,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return diagnostics;
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
void err;
|
|
293
|
+
this.log("Failed to parse ruff JSON output");
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe cross-platform spawn utilities
|
|
3
|
+
*
|
|
4
|
+
* Wraps child_process.spawnSync to handle Windows execution safely
|
|
5
|
+
* without triggering deprecation warnings.
|
|
6
|
+
*
|
|
7
|
+
* Strategy:
|
|
8
|
+
* - Unix: Use shell: false with normal args
|
|
9
|
+
* - Windows: Manually construct command string to avoid deprecation warning,
|
|
10
|
+
* then use shell: true with no args array
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
/**
|
|
14
|
+
* Escape an argument for Windows shell execution.
|
|
15
|
+
* Handles spaces, quotes, and special characters.
|
|
16
|
+
*/
|
|
17
|
+
function escapeWindowsArg(arg) {
|
|
18
|
+
// If no special characters, return as-is
|
|
19
|
+
if (!/[\s\"]/.test(arg))
|
|
20
|
+
return arg;
|
|
21
|
+
// Escape quotes by doubling them
|
|
22
|
+
return `"${arg.replace(/"/g, "\"\"")}"`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Construct a command string for Windows shell execution.
|
|
26
|
+
* This avoids the deprecation warning by not passing an args array.
|
|
27
|
+
*/
|
|
28
|
+
function buildWindowsCommand(command, args) {
|
|
29
|
+
const escapedArgs = args.map(escapeWindowsArg).join(" ");
|
|
30
|
+
return `${command} ${escapedArgs}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Safely spawn a process cross-platform without shell deprecation warnings.
|
|
34
|
+
*
|
|
35
|
+
* On Windows: Uses shell: true but constructs the command string manually
|
|
36
|
+
* to avoid the deprecation warning about unescaped args.
|
|
37
|
+
* On Unix: Uses shell: false for normal execution.
|
|
38
|
+
*/
|
|
39
|
+
export function safeSpawn(command, args, options) {
|
|
40
|
+
if (process.platform === "win32") {
|
|
41
|
+
// On Windows, construct the full command string and use shell: true
|
|
42
|
+
// without an args array. This avoids the deprecation warning.
|
|
43
|
+
const fullCommand = buildWindowsCommand(command, args);
|
|
44
|
+
const result = spawnSync(fullCommand, {
|
|
45
|
+
...options,
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
shell: true,
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
stdout: result.stdout?.toString() || "",
|
|
52
|
+
stderr: result.stderr?.toString() || "",
|
|
53
|
+
status: result.status,
|
|
54
|
+
error: result.error,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// On Unix, use shell: false (the default) with normal args
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
|
+
...options,
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
shell: false,
|
|
62
|
+
windowsHide: true,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
stdout: result.stdout?.toString() || "",
|
|
66
|
+
stderr: result.stderr?.toString() || "",
|
|
67
|
+
status: result.status,
|
|
68
|
+
error: result.error,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if a command is available in PATH
|
|
73
|
+
*/
|
|
74
|
+
export function isCommandAvailable(command) {
|
|
75
|
+
const result = safeSpawn(process.platform === "win32" ? "where" : "which", [command], { timeout: 5000 });
|
|
76
|
+
return result.status === 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Find the full path to a command (npx, node, etc.)
|
|
80
|
+
*/
|
|
81
|
+
export function findCommand(command) {
|
|
82
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
83
|
+
const result = safeSpawn(finder, [command], { timeout: 5000 });
|
|
84
|
+
if (result.status !== 0)
|
|
85
|
+
return null;
|
|
86
|
+
// Take first line (first match)
|
|
87
|
+
return result.stdout.trim().split("\n")[0] || null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { EXCLUDED_DIRS, isTestFile } from "./file-utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Common parsing logic for ast-grep JSON output (handles both array and NDJSON).
|
|
6
|
+
*/
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
|
|
8
|
+
export function parseAstGrepJson(raw) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return [];
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
if (trimmed.startsWith("[")) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(trimmed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return trimmed.split("\n").flatMap((l) => {
|
|
21
|
+
try {
|
|
22
|
+
return [JSON.parse(l)];
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a file should be ignored based on project type and common patterns.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldIgnoreFile(filePath, isTsProject) {
|
|
33
|
+
const relPath = filePath.replace(/\\/g, "/");
|
|
34
|
+
const _basename = path.basename(relPath);
|
|
35
|
+
// Ignore compiled JS in TS projects
|
|
36
|
+
const isJs = relPath.endsWith(".js") ||
|
|
37
|
+
relPath.endsWith(".mjs") ||
|
|
38
|
+
relPath.endsWith(".cjs");
|
|
39
|
+
if (isTsProject && isJs)
|
|
40
|
+
return true;
|
|
41
|
+
// Ignore test scripts and common test patterns
|
|
42
|
+
if (isTestFile(filePath))
|
|
43
|
+
return true;
|
|
44
|
+
// Ignore hidden directories and common build outputs
|
|
45
|
+
if (EXCLUDED_DIRS.some((d) => relPath.includes(`/${d}/`)))
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Recursively find source files in a directory, respecting common excludes.
|
|
51
|
+
*/
|
|
52
|
+
export function getSourceFiles(dir, isTsProject) {
|
|
53
|
+
const files = [];
|
|
54
|
+
if (!fs.existsSync(dir))
|
|
55
|
+
return files;
|
|
56
|
+
const scan = (d) => {
|
|
57
|
+
let entries = [];
|
|
58
|
+
try {
|
|
59
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const full = path.join(d, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
if (EXCLUDED_DIRS.includes(entry.name))
|
|
68
|
+
continue;
|
|
69
|
+
scan(full);
|
|
70
|
+
}
|
|
71
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
|
|
72
|
+
// Skip compiled JS if it's a TS project
|
|
73
|
+
if (isTsProject &&
|
|
74
|
+
entry.name.endsWith(".js") &&
|
|
75
|
+
fs.existsSync(full.replace(/\.js$/, ".ts")))
|
|
76
|
+
continue;
|
|
77
|
+
files.push(full);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
scan(dir);
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SgRunner - encapsulates ast-grep subprocess management
|
|
3
|
+
*
|
|
4
|
+
* Extracted from AstGrepClient to simplify the main client.
|
|
5
|
+
* Handles: spawn, spawnSync, temp dir management, JSON parsing.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { safeSpawn } from "./safe-spawn.js";
|
|
12
|
+
export class SgRunner {
|
|
13
|
+
log;
|
|
14
|
+
constructor(verbose = false) {
|
|
15
|
+
this.log = verbose
|
|
16
|
+
? (msg) => console.error(`[sg-runner] ${msg}`)
|
|
17
|
+
: () => { };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if ast-grep CLI is available
|
|
21
|
+
*/
|
|
22
|
+
isAvailable() {
|
|
23
|
+
const result = safeSpawn("npx", ["sg", "--version"], {
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
});
|
|
26
|
+
return !result.error && result.status === 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run ast-grep asynchronously, return parsed matches
|
|
30
|
+
*/
|
|
31
|
+
async exec(args) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
// On Windows, construct full command string to avoid deprecation warning
|
|
34
|
+
const useShell = process.platform === "win32";
|
|
35
|
+
const fullCommand = useShell
|
|
36
|
+
? `npx sg ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`
|
|
37
|
+
: undefined;
|
|
38
|
+
const proc = fullCommand
|
|
39
|
+
? spawn(fullCommand, { stdio: ["ignore", "pipe", "pipe"], shell: true })
|
|
40
|
+
: spawn("npx", ["sg", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
proc.stdout.on("data", (data) => (stdout += data.toString()));
|
|
44
|
+
proc.stderr.on("data", (data) => (stderr += data.toString()));
|
|
45
|
+
proc.on("error", (err) => {
|
|
46
|
+
if (err.message.includes("ENOENT")) {
|
|
47
|
+
resolve({
|
|
48
|
+
matches: [],
|
|
49
|
+
error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
resolve({ matches: [], error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
proc.on("close", (code) => {
|
|
57
|
+
if (code !== 0 && !stdout.trim()) {
|
|
58
|
+
resolve({
|
|
59
|
+
matches: [],
|
|
60
|
+
error: stderr.includes("No files found")
|
|
61
|
+
? undefined
|
|
62
|
+
: stderr.trim() || `Exit code ${code}`,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!stdout.trim()) {
|
|
67
|
+
resolve({ matches: [] });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(stdout);
|
|
72
|
+
const matches = Array.isArray(parsed) ? parsed : [parsed];
|
|
73
|
+
resolve({ matches });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
resolve({ matches: [], error: "Failed to parse output" });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run ast-grep synchronously (for simple scans)
|
|
83
|
+
*/
|
|
84
|
+
execSync(args) {
|
|
85
|
+
const result = safeSpawn("npx", ["sg", ...args], {
|
|
86
|
+
timeout: 30000,
|
|
87
|
+
});
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return { output: "", error: result.error.message };
|
|
90
|
+
}
|
|
91
|
+
const output = result.stdout || result.stderr || "";
|
|
92
|
+
return { output };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run a temporary rule scan (creates temp dir with rule file)
|
|
96
|
+
*/
|
|
97
|
+
tempScan(dir, ruleId, ruleYaml, timeout = 30000) {
|
|
98
|
+
const tmpDir = os.tmpdir();
|
|
99
|
+
const ts = Date.now();
|
|
100
|
+
const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
|
|
101
|
+
const rulesSubdir = path.join(sessionDir, "rules");
|
|
102
|
+
const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
|
|
103
|
+
const configFile = path.join(sessionDir, ".sgconfig.yml");
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(rulesSubdir, { recursive: true });
|
|
106
|
+
fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
|
|
107
|
+
fs.writeFileSync(ruleFile, ruleYaml);
|
|
108
|
+
const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
|
|
109
|
+
const output = result.stdout || result.stderr || "";
|
|
110
|
+
if (!output.trim())
|
|
111
|
+
return [];
|
|
112
|
+
const items = JSON.parse(output);
|
|
113
|
+
return Array.isArray(items) ? items : [items];
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
try {
|
|
120
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
this.log(`Cleanup failed: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Run a rule file scan (temporary config approach) - alias for tempScan
|
|
129
|
+
*/
|
|
130
|
+
scanWithRule(ruleYaml, dir, timeout = 30000) {
|
|
131
|
+
const sessionDir = path.join(os.tmpdir(), `sg-scan-${Date.now()}`);
|
|
132
|
+
const rulesSubdir = path.join(sessionDir, "rules");
|
|
133
|
+
const configFile = path.join(sessionDir, ".sgconfig.yml");
|
|
134
|
+
const ruleFile = path.join(rulesSubdir, "rule.yml");
|
|
135
|
+
try {
|
|
136
|
+
fs.mkdirSync(rulesSubdir, { recursive: true });
|
|
137
|
+
fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
|
|
138
|
+
fs.writeFileSync(ruleFile, ruleYaml);
|
|
139
|
+
const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
|
|
140
|
+
const output = result.stdout || result.stderr || "";
|
|
141
|
+
if (!output.trim())
|
|
142
|
+
return [];
|
|
143
|
+
const items = JSON.parse(output);
|
|
144
|
+
return Array.isArray(items) ? items : [items];
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
try {
|
|
151
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
this.log(`Cleanup failed: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Format matches for display
|
|
160
|
+
*/
|
|
161
|
+
formatMatches(matches, isDryRun = false, maxItems = 50, showModeIndicator = false) {
|
|
162
|
+
if (matches.length === 0) {
|
|
163
|
+
if (showModeIndicator) {
|
|
164
|
+
return isDryRun
|
|
165
|
+
? "[DRY-RUN] No matches found."
|
|
166
|
+
: "[APPLIED] No changes made (no matches found).";
|
|
167
|
+
}
|
|
168
|
+
return "No matches found";
|
|
169
|
+
}
|
|
170
|
+
const shown = matches.slice(0, maxItems);
|
|
171
|
+
const lines = shown.map((m) => {
|
|
172
|
+
const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
|
|
173
|
+
const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
|
|
174
|
+
return isDryRun && m.replacement
|
|
175
|
+
? `${loc}\n - ${text}\n + ${m.replacement}`
|
|
176
|
+
: `${loc}: ${text}`;
|
|
177
|
+
});
|
|
178
|
+
if (matches.length > maxItems) {
|
|
179
|
+
lines.unshift(`Found ${matches.length} matches (showing first ${maxItems}):`);
|
|
180
|
+
}
|
|
181
|
+
if (showModeIndicator) {
|
|
182
|
+
const prefix = isDryRun ? "[DRY-RUN]" : "[APPLIED]";
|
|
183
|
+
const suffix = isDryRun
|
|
184
|
+
? "\n\n(Dry run — use apply=true to apply changes)"
|
|
185
|
+
: "";
|
|
186
|
+
return `${prefix} ${matches.length} replacement(s):\n\n${lines.join("\n")}${suffix}`;
|
|
187
|
+
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Types - Core types for the pi-local LSP client
|
|
3
|
+
* Simplified from the full LSP spec to what we actually need
|
|
4
|
+
*/
|
|
5
|
+
export var DiagnosticSeverity;
|
|
6
|
+
(function (DiagnosticSeverity) {
|
|
7
|
+
DiagnosticSeverity[DiagnosticSeverity["Error"] = 1] = "Error";
|
|
8
|
+
DiagnosticSeverity[DiagnosticSeverity["Warning"] = 2] = "Warning";
|
|
9
|
+
DiagnosticSeverity[DiagnosticSeverity["Information"] = 3] = "Information";
|
|
10
|
+
DiagnosticSeverity[DiagnosticSeverity["Hint"] = 4] = "Hint";
|
|
11
|
+
})(DiagnosticSeverity || (DiagnosticSeverity = {}));
|