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.
Files changed (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. 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
+ }