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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Kind Detection for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Centralized file type detection to avoid duplication across clients.
|
|
5
|
+
* Maps file extensions and paths to semantic file kinds.
|
|
6
|
+
*/
|
|
7
|
+
import { basename, extname } from "node:path";
|
|
8
|
+
// --- Extension Maps ---
|
|
9
|
+
const KIND_EXTENSIONS = {
|
|
10
|
+
jsts: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".svelte"],
|
|
11
|
+
python: [".py"],
|
|
12
|
+
go: [".go"],
|
|
13
|
+
rust: [".rs"],
|
|
14
|
+
cxx: [
|
|
15
|
+
".c",
|
|
16
|
+
".cc",
|
|
17
|
+
".cpp",
|
|
18
|
+
".cxx",
|
|
19
|
+
".h",
|
|
20
|
+
".hh",
|
|
21
|
+
".hpp",
|
|
22
|
+
".hxx",
|
|
23
|
+
".ixx",
|
|
24
|
+
".ipp",
|
|
25
|
+
".inl",
|
|
26
|
+
".tpp",
|
|
27
|
+
".txx",
|
|
28
|
+
],
|
|
29
|
+
cmake: [".cmake"],
|
|
30
|
+
shell: [".sh", ".bash", ".zsh", ".fish"],
|
|
31
|
+
json: [".json", ".jsonc", ".json5"],
|
|
32
|
+
markdown: [".md", ".mdx"],
|
|
33
|
+
css: [".css", ".scss", ".sass", ".less"],
|
|
34
|
+
yaml: [".yaml", ".yml"],
|
|
35
|
+
};
|
|
36
|
+
// Reverse map: extension → file kind (for fast lookup)
|
|
37
|
+
const EXT_TO_KIND = new Map();
|
|
38
|
+
for (const [kind, exts] of Object.entries(KIND_EXTENSIONS)) {
|
|
39
|
+
for (const ext of exts) {
|
|
40
|
+
EXT_TO_KIND.set(ext.toLowerCase(), kind);
|
|
41
|
+
}
|
|
42
|
+
// Also register without leading dot
|
|
43
|
+
for (const ext of exts) {
|
|
44
|
+
if (ext.startsWith(".")) {
|
|
45
|
+
EXT_TO_KIND.set(ext.slice(1).toLowerCase(), kind);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Special filenames that indicate a file kind
|
|
50
|
+
const SPECIAL_FILENAMES = [
|
|
51
|
+
{ pattern: /^CMakeLists\.txt$/i, kind: "cmake" },
|
|
52
|
+
{ pattern: /^Makefile$/i, kind: "shell" },
|
|
53
|
+
{ pattern: /^ Dockerfile(\.\w+)?$/i, kind: "shell" },
|
|
54
|
+
];
|
|
55
|
+
// --- Detection Functions ---
|
|
56
|
+
/**
|
|
57
|
+
* Detect the file kind from a file path.
|
|
58
|
+
* Returns the semantic file kind or undefined if unknown.
|
|
59
|
+
*/
|
|
60
|
+
export function detectFileKind(filePath) {
|
|
61
|
+
if (!filePath || typeof filePath !== "string") {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
// Check special filenames first
|
|
65
|
+
const base = basename(filePath);
|
|
66
|
+
for (const { pattern, kind } of SPECIAL_FILENAMES) {
|
|
67
|
+
if (pattern.test(base)) {
|
|
68
|
+
return kind;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check by extension
|
|
72
|
+
const ext = extname(filePath).toLowerCase();
|
|
73
|
+
return EXT_TO_KIND.get(ext);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a file kind is supported by a specific tool or capability.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Check if TypeScript file
|
|
80
|
+
* if (isFileKind(filePath, "jsts")) { ... }
|
|
81
|
+
*
|
|
82
|
+
* // Check for multiple kinds
|
|
83
|
+
* if (isFileKind(filePath, ["jsts", "python"])) { ... }
|
|
84
|
+
*/
|
|
85
|
+
export function isFileKind(filePath, kind) {
|
|
86
|
+
const detected = detectFileKind(filePath);
|
|
87
|
+
if (!detected)
|
|
88
|
+
return false;
|
|
89
|
+
if (Array.isArray(kind)) {
|
|
90
|
+
return kind.includes(detected);
|
|
91
|
+
}
|
|
92
|
+
return detected === kind;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get all file kinds that match a given file extension.
|
|
96
|
+
* Useful for listing which tools might handle a file.
|
|
97
|
+
*/
|
|
98
|
+
export function getFileKindsForExtension(ext) {
|
|
99
|
+
const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
|
|
100
|
+
const kind = EXT_TO_KIND.get(normalizedExt.toLowerCase());
|
|
101
|
+
return kind ? [kind] : [];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if a file kind represents a code file (not config/markdown).
|
|
105
|
+
*/
|
|
106
|
+
export function isCodeKind(kind) {
|
|
107
|
+
return ["jsts", "python", "go", "rust", "cxx", "shell"].includes(kind);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a file kind represents a text/config file.
|
|
111
|
+
*/
|
|
112
|
+
export function isConfigKind(kind) {
|
|
113
|
+
return ["json", "yaml", "markdown", "css"].includes(kind);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get human-readable description of a file kind.
|
|
117
|
+
*/
|
|
118
|
+
export function getFileKindLabel(kind) {
|
|
119
|
+
const labels = {
|
|
120
|
+
jsts: "JavaScript/TypeScript",
|
|
121
|
+
python: "Python",
|
|
122
|
+
go: "Go",
|
|
123
|
+
rust: "Rust",
|
|
124
|
+
cxx: "C/C++",
|
|
125
|
+
cmake: "CMake",
|
|
126
|
+
shell: "Shell",
|
|
127
|
+
json: "JSON",
|
|
128
|
+
markdown: "Markdown",
|
|
129
|
+
css: "CSS",
|
|
130
|
+
yaml: "YAML",
|
|
131
|
+
};
|
|
132
|
+
return labels[kind] ?? kind;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get file extensions for a file kind.
|
|
136
|
+
*/
|
|
137
|
+
export function getExtensionsForKind(kind) {
|
|
138
|
+
return [...(KIND_EXTENSIONS[kind] ?? [])];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check if a file should be scanned for linting/formatting.
|
|
142
|
+
* Excludes test files, generated files, etc.
|
|
143
|
+
*/
|
|
144
|
+
export function isScannableFile(filePath) {
|
|
145
|
+
const kind = detectFileKind(filePath);
|
|
146
|
+
if (!kind)
|
|
147
|
+
return false;
|
|
148
|
+
// Exclude test files for most kinds
|
|
149
|
+
const base = basename(filePath);
|
|
150
|
+
if (base.includes(".test.") ||
|
|
151
|
+
base.includes(".spec.") ||
|
|
152
|
+
base.startsWith("test-") ||
|
|
153
|
+
base.startsWith("spec-")) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// Only scan code and config files
|
|
157
|
+
return isCodeKind(kind) || isConfigKind(kind);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the language identifier for LSP/tools that use language IDs.
|
|
161
|
+
*/
|
|
162
|
+
export function getLanguageId(kind) {
|
|
163
|
+
const languageIds = {
|
|
164
|
+
jsts: "typescript",
|
|
165
|
+
python: "python",
|
|
166
|
+
go: "go",
|
|
167
|
+
rust: "rust",
|
|
168
|
+
cxx: "cpp",
|
|
169
|
+
cmake: "cmake",
|
|
170
|
+
shell: "shell",
|
|
171
|
+
json: "json",
|
|
172
|
+
markdown: "markdown",
|
|
173
|
+
css: "css",
|
|
174
|
+
yaml: "yaml",
|
|
175
|
+
};
|
|
176
|
+
return languageIds[kind] ?? "plaintext";
|
|
177
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared file path utilities for pi-lens
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Directories to exclude from all scans (build outputs, dependencies, caches).
|
|
6
|
+
* Used consistently across all scanners to avoid noise from generated files.
|
|
7
|
+
*/
|
|
8
|
+
export const EXCLUDED_DIRS = [
|
|
9
|
+
"node_modules",
|
|
10
|
+
".git",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".next",
|
|
14
|
+
".pi-lens",
|
|
15
|
+
".pi", // pi agent directory
|
|
16
|
+
".ruff_cache", // Python linter cache
|
|
17
|
+
"venv",
|
|
18
|
+
".venv",
|
|
19
|
+
"coverage",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".tox",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Check if file path is a test/fixture/mock file.
|
|
26
|
+
* Used by secrets scanner, rate command, and dispatch runners
|
|
27
|
+
* to skip these files (false positives on fake credentials, etc).
|
|
28
|
+
*/
|
|
29
|
+
export function isTestFile(filePath) {
|
|
30
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
31
|
+
return (normalized.includes(".test.") ||
|
|
32
|
+
normalized.includes(".spec.") ||
|
|
33
|
+
normalized.includes("/test/") ||
|
|
34
|
+
normalized.includes("/tests/") ||
|
|
35
|
+
normalized.includes("__tests__/") ||
|
|
36
|
+
normalized.includes("test-utils") ||
|
|
37
|
+
normalized.startsWith("test-") ||
|
|
38
|
+
normalized.includes(".fixture.") ||
|
|
39
|
+
normalized.includes(".mock."));
|
|
40
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jscpd Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Detects copy-paste / duplicate code blocks across the project.
|
|
5
|
+
* Helps the agent avoid unknowingly duplicating logic that already exists.
|
|
6
|
+
*
|
|
7
|
+
* Requires: npm install -D jscpd
|
|
8
|
+
* Docs: https://github.com/kucherenko/jscpd
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { safeSpawn } from "./safe-spawn.js";
|
|
14
|
+
// --- Client ---
|
|
15
|
+
export class JscpdClient {
|
|
16
|
+
available = null;
|
|
17
|
+
log;
|
|
18
|
+
constructor(verbose = false) {
|
|
19
|
+
this.log = verbose ? (msg) => console.error(`[jscpd] ${msg}`) : () => { };
|
|
20
|
+
}
|
|
21
|
+
isAvailable() {
|
|
22
|
+
if (this.available !== null)
|
|
23
|
+
return this.available;
|
|
24
|
+
const result = safeSpawn("npx", ["jscpd", "--version"], {
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
});
|
|
27
|
+
this.available = !result.error && result.status === 0;
|
|
28
|
+
return this.available;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Scan a directory for duplicate code blocks.
|
|
32
|
+
* Uses a temp output dir to capture JSON report.
|
|
33
|
+
* @param isTsProject - If true, excludes .js files (they're compiled artifacts in TS projects)
|
|
34
|
+
*/
|
|
35
|
+
scan(cwd, minLines = 5, minTokens = 50, isTsProject = false) {
|
|
36
|
+
// Return early for non-existent or empty directories
|
|
37
|
+
if (!fs.existsSync(cwd)) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
clones: [],
|
|
41
|
+
duplicatedLines: 0,
|
|
42
|
+
totalLines: 0,
|
|
43
|
+
percentage: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const entries = fs.readdirSync(cwd);
|
|
47
|
+
const hasSourceFiles = entries.some((e) => /\.(ts|tsx|js|jsx)$/.test(e) && !e.endsWith(".d.ts"));
|
|
48
|
+
if (!hasSourceFiles) {
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
clones: [],
|
|
52
|
+
duplicatedLines: 0,
|
|
53
|
+
totalLines: 0,
|
|
54
|
+
percentage: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!this.isAvailable()) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
clones: [],
|
|
61
|
+
duplicatedLines: 0,
|
|
62
|
+
totalLines: 0,
|
|
63
|
+
percentage: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const outDir = path.join(os.tmpdir(), `pi-lens-jscpd-${Date.now()}`);
|
|
67
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
68
|
+
// Build ignore pattern - exclude .js in TS projects (compiled artifacts)
|
|
69
|
+
const baseIgnores = "**/node_modules/**,**/dist/**,**/build/**,**/.git/**,**/.pi-lens/**,**/*.md,**/*.txt,**/*.json,**/*.yaml,**/*.yml,**/*.toml,**/*.lock,**/*.test.*,**/*.spec.*,**/*.poc.test.*,**/__tests__/**,**/tests/**";
|
|
70
|
+
const ignorePattern = isTsProject
|
|
71
|
+
? `${baseIgnores},**/*.js,**/*.jsx`
|
|
72
|
+
: baseIgnores;
|
|
73
|
+
try {
|
|
74
|
+
safeSpawn("npx", [
|
|
75
|
+
"jscpd",
|
|
76
|
+
".",
|
|
77
|
+
"--min-lines",
|
|
78
|
+
String(minLines),
|
|
79
|
+
"--min-tokens",
|
|
80
|
+
String(minTokens),
|
|
81
|
+
"--reporters",
|
|
82
|
+
"json",
|
|
83
|
+
"--output",
|
|
84
|
+
outDir,
|
|
85
|
+
"--ignore",
|
|
86
|
+
ignorePattern,
|
|
87
|
+
], {
|
|
88
|
+
timeout: 30000,
|
|
89
|
+
cwd,
|
|
90
|
+
});
|
|
91
|
+
const reportPath = path.join(outDir, "jscpd-report.json");
|
|
92
|
+
if (!fs.existsSync(reportPath)) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
clones: [],
|
|
96
|
+
duplicatedLines: 0,
|
|
97
|
+
totalLines: 0,
|
|
98
|
+
percentage: 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return this.parseReport(reportPath);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.log(`Scan error: ${err.message}`);
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
clones: [],
|
|
108
|
+
duplicatedLines: 0,
|
|
109
|
+
totalLines: 0,
|
|
110
|
+
percentage: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
try {
|
|
115
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
void err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
formatResult(result, maxClones = 8) {
|
|
123
|
+
if (!result.success || result.clones.length === 0)
|
|
124
|
+
return "";
|
|
125
|
+
const pct = result.percentage.toFixed(1);
|
|
126
|
+
let output = `[jscpd] ${result.clones.length} duplicate block(s) — ${pct}% of codebase (${result.duplicatedLines}/${result.totalLines} lines):\n`;
|
|
127
|
+
for (const clone of result.clones.slice(0, maxClones)) {
|
|
128
|
+
const a = `${path.basename(clone.fileA)}:${clone.startA}`;
|
|
129
|
+
const b = `${path.basename(clone.fileB)}:${clone.startB}`;
|
|
130
|
+
output += ` ${clone.lines} lines — ${a} ↔ ${b}\n`;
|
|
131
|
+
}
|
|
132
|
+
if (result.clones.length > maxClones) {
|
|
133
|
+
output += ` ... and ${result.clones.length - maxClones} more\n`;
|
|
134
|
+
}
|
|
135
|
+
return output;
|
|
136
|
+
}
|
|
137
|
+
// --- Internal ---
|
|
138
|
+
parseReport(reportPath) {
|
|
139
|
+
try {
|
|
140
|
+
const data = JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
141
|
+
// Stats live in statistics.total, not statistics.clones
|
|
142
|
+
const total = data.statistics?.total ?? {};
|
|
143
|
+
const duplicatedLines = total.duplicatedLines ?? 0;
|
|
144
|
+
const totalLines = total.lines ?? 0;
|
|
145
|
+
const percentage = total.percentage ??
|
|
146
|
+
(totalLines > 0 ? (duplicatedLines / totalLines) * 100 : 0);
|
|
147
|
+
const rawClones = data.duplicates ?? [];
|
|
148
|
+
const clones = rawClones.map((c) => ({
|
|
149
|
+
fileA: c.firstFile?.name ?? "",
|
|
150
|
+
startA: c.firstFile?.start ?? 0,
|
|
151
|
+
fileB: c.secondFile?.name ?? "",
|
|
152
|
+
startB: c.secondFile?.start ?? 0,
|
|
153
|
+
lines: c.lines ?? 0,
|
|
154
|
+
tokens: c.tokens ?? 0,
|
|
155
|
+
}));
|
|
156
|
+
return { success: true, clones, duplicatedLines, totalLines, percentage };
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
void err;
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
clones: [],
|
|
163
|
+
duplicatedLines: 0,
|
|
164
|
+
totalLines: 0,
|
|
165
|
+
percentage: 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knip Client for pi-local
|
|
3
|
+
*
|
|
4
|
+
* Detects unused exports, files, dependencies, and more.
|
|
5
|
+
* Essential for safe refactoring — I need to know what's dead code
|
|
6
|
+
* before I can clean it up.
|
|
7
|
+
*
|
|
8
|
+
* Requires: npm install -D knip
|
|
9
|
+
* Docs: https://knip.dev/
|
|
10
|
+
*/
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { safeSpawn } from "./safe-spawn.js";
|
|
13
|
+
// --- Client ---
|
|
14
|
+
export class KnipClient {
|
|
15
|
+
knipAvailable = null;
|
|
16
|
+
log;
|
|
17
|
+
constructor(verbose = false) {
|
|
18
|
+
this.log = verbose
|
|
19
|
+
? (msg) => console.error(`[knip] ${msg}`)
|
|
20
|
+
: () => { };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if knip CLI is available
|
|
24
|
+
*/
|
|
25
|
+
isAvailable() {
|
|
26
|
+
if (this.knipAvailable !== null)
|
|
27
|
+
return this.knipAvailable;
|
|
28
|
+
const result = safeSpawn("npx", ["knip", "--version"], {
|
|
29
|
+
timeout: 10000,
|
|
30
|
+
});
|
|
31
|
+
this.knipAvailable = !result.error && result.status === 0;
|
|
32
|
+
if (this.knipAvailable) {
|
|
33
|
+
this.log(`Knip available`);
|
|
34
|
+
}
|
|
35
|
+
return this.knipAvailable;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run knip analysis on the project
|
|
39
|
+
*/
|
|
40
|
+
analyze(cwd, ignore) {
|
|
41
|
+
if (!this.isAvailable()) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
issues: [],
|
|
45
|
+
unusedExports: [],
|
|
46
|
+
unusedFiles: [],
|
|
47
|
+
unusedDeps: [],
|
|
48
|
+
unlistedDeps: [],
|
|
49
|
+
summary: "Knip not available. Install with: npm install -D knip",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const targetDir = cwd || process.cwd();
|
|
53
|
+
try {
|
|
54
|
+
const args = [
|
|
55
|
+
"knip",
|
|
56
|
+
"--reporter=json",
|
|
57
|
+
"--include",
|
|
58
|
+
"files,exports,types,dependencies,unlisted",
|
|
59
|
+
];
|
|
60
|
+
if (ignore && ignore.length > 0) {
|
|
61
|
+
args.push("--ignore", ignore.join(","));
|
|
62
|
+
}
|
|
63
|
+
const result = safeSpawn("npx", args, {
|
|
64
|
+
timeout: 30000,
|
|
65
|
+
cwd: targetDir,
|
|
66
|
+
});
|
|
67
|
+
// Knip exits 0 on success (even with issues), 1 on errors
|
|
68
|
+
const output = result.stdout || "";
|
|
69
|
+
this.log(`Knip output length: ${output.length}`);
|
|
70
|
+
if (output.length < 500) {
|
|
71
|
+
this.log(`Knip output sample: ${output}`);
|
|
72
|
+
}
|
|
73
|
+
if (!output.trim()) {
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
issues: [],
|
|
77
|
+
unusedExports: [],
|
|
78
|
+
unusedFiles: [],
|
|
79
|
+
unusedDeps: [],
|
|
80
|
+
unlistedDeps: [],
|
|
81
|
+
summary: "No issues found",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return this.parseOutput(output);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
this.log(`Analysis error: ${err.message}`);
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
issues: [],
|
|
91
|
+
unusedExports: [],
|
|
92
|
+
unusedFiles: [],
|
|
93
|
+
unusedDeps: [],
|
|
94
|
+
unlistedDeps: [],
|
|
95
|
+
summary: `Error: ${err.message}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Find unused exports in a specific file
|
|
101
|
+
*/
|
|
102
|
+
findUnusedExports(filePath) {
|
|
103
|
+
const result = this.analyze(path.dirname(filePath));
|
|
104
|
+
const basename = path.basename(filePath);
|
|
105
|
+
return result.unusedExports
|
|
106
|
+
.filter((e) => e.file?.includes(basename))
|
|
107
|
+
.map((e) => e.name);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Format results for LLM consumption
|
|
111
|
+
*/
|
|
112
|
+
formatResult(result, maxItems = 20) {
|
|
113
|
+
if (!result.success)
|
|
114
|
+
return `[Knip] ${result.summary}`;
|
|
115
|
+
if (result.issues.length === 0)
|
|
116
|
+
return "";
|
|
117
|
+
let output = `[Knip] ${result.issues.length} issue(s)`;
|
|
118
|
+
if (result.unusedExports.length)
|
|
119
|
+
output += ` — ${result.unusedExports.length} unused export(s)`;
|
|
120
|
+
if (result.unusedFiles.length)
|
|
121
|
+
output += ` — ${result.unusedFiles.length} unused file(s)`;
|
|
122
|
+
if (result.unusedDeps.length)
|
|
123
|
+
output += ` — ${result.unusedDeps.length} unused dep(s)`;
|
|
124
|
+
if (result.unlistedDeps.length)
|
|
125
|
+
output += ` — ${result.unlistedDeps.length} unlisted dep(s)`;
|
|
126
|
+
output += ":\n";
|
|
127
|
+
// Show unused exports first (most useful for refactoring)
|
|
128
|
+
if (result.unusedExports.length > 0) {
|
|
129
|
+
output += "\n Unused exports:\n";
|
|
130
|
+
for (const issue of result.unusedExports.slice(0, maxItems)) {
|
|
131
|
+
const loc = issue.file ? ` (${path.basename(issue.file)})` : "";
|
|
132
|
+
output += ` - ${issue.name}${loc}\n`;
|
|
133
|
+
}
|
|
134
|
+
if (result.unusedExports.length > maxItems) {
|
|
135
|
+
output += ` ... and ${result.unusedExports.length - maxItems} more\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Show unused files
|
|
139
|
+
if (result.unusedFiles.length > 0) {
|
|
140
|
+
output += "\n Unused files:\n";
|
|
141
|
+
for (const issue of result.unusedFiles.slice(0, 10)) {
|
|
142
|
+
output += ` - ${issue.name}\n`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Show unused deps (might be worth removing)
|
|
146
|
+
if (result.unusedDeps.length > 0) {
|
|
147
|
+
output += "\n Unused dependencies:\n";
|
|
148
|
+
for (const issue of result.unusedDeps) {
|
|
149
|
+
output += ` - ${issue.package || issue.name}\n`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
// --- Internal ---
|
|
155
|
+
parseOutput(output) {
|
|
156
|
+
try {
|
|
157
|
+
const data = JSON.parse(output);
|
|
158
|
+
const issues = [];
|
|
159
|
+
const unusedExports = [];
|
|
160
|
+
const unusedFiles = [];
|
|
161
|
+
const unusedDeps = [];
|
|
162
|
+
const unlistedDeps = [];
|
|
163
|
+
// Knip JSON format: { issues: [ { file, exports:[], files:[], dependencies:[], ... } ] }
|
|
164
|
+
const fileEntries = data.issues ?? [];
|
|
165
|
+
for (const entry of fileEntries) {
|
|
166
|
+
const file = entry.file ?? "";
|
|
167
|
+
const push = (arr, type, target) => {
|
|
168
|
+
for (const item of arr) {
|
|
169
|
+
const issue = {
|
|
170
|
+
type,
|
|
171
|
+
name: item.name ?? item.symbol ?? String(item),
|
|
172
|
+
file,
|
|
173
|
+
line: item.line,
|
|
174
|
+
package: item.package,
|
|
175
|
+
};
|
|
176
|
+
issues.push(issue);
|
|
177
|
+
target.push(issue);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
push(entry.exports ?? [], "export", unusedExports);
|
|
181
|
+
push(entry.types ?? [], "export", unusedExports);
|
|
182
|
+
push(entry.files ?? [], "file", unusedFiles);
|
|
183
|
+
push(entry.dependencies ?? [], "dependency", unusedDeps);
|
|
184
|
+
push(entry.devDependencies ?? [], "devDependency", unusedDeps);
|
|
185
|
+
push(entry.unlisted ?? [], "unlisted", unlistedDeps);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
issues,
|
|
190
|
+
unusedExports,
|
|
191
|
+
unusedFiles,
|
|
192
|
+
unusedDeps,
|
|
193
|
+
unlistedDeps,
|
|
194
|
+
summary: `Found ${issues.length} issues`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
void err;
|
|
199
|
+
this.log("Failed to parse knip JSON output");
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
issues: [],
|
|
203
|
+
unusedExports: [],
|
|
204
|
+
unusedFiles: [],
|
|
205
|
+
unusedDeps: [],
|
|
206
|
+
unlistedDeps: [],
|
|
207
|
+
summary: "Failed to parse output",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|