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,624 @@
1
+ /**
2
+ * Project Metadata Detection for pi-lens
3
+ *
4
+ * Extracts project configuration from common config files:
5
+ * - package.json (Node.js/npm/pnpm/yarn/bun)
6
+ * - pyproject.toml (Python)
7
+ * - Cargo.toml (Rust)
8
+ * - go.mod (Go)
9
+ * - composer.json (PHP)
10
+ * - Gemfile (Ruby)
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+
16
+ // --- Types ---
17
+
18
+ export type ProjectType = "node" | "python" | "rust" | "go" | "php" | "ruby" | "multi" | "unknown";
19
+
20
+ export type PackageManager = "npm" | "yarn" | "pnpm" | "bun" | "pip" | "poetry" | "uv" | "cargo" | "gomod" | "composer" | "bundler";
21
+
22
+ export interface ProjectMetadata {
23
+ /** Project type detected from config files */
24
+ type: ProjectType;
25
+ /** Package manager/toolchain detected */
26
+ packageManager?: PackageManager;
27
+ /** Available scripts/commands (e.g., npm scripts, Makefile targets) */
28
+ scripts: Record<string, string>;
29
+ /** Project name from config */
30
+ name?: string;
31
+ /** Project version */
32
+ version?: string;
33
+ /** Detected languages in the project */
34
+ languages: string[];
35
+ /** Whether the project has tests configured */
36
+ hasTests: boolean;
37
+ /** Test framework detected */
38
+ testFramework?: string;
39
+ /** Whether the project has linting configured */
40
+ hasLinting: boolean;
41
+ /** Linter detected */
42
+ linter?: string;
43
+ /** Whether the project has formatting configured */
44
+ hasFormatting: boolean;
45
+ /** Formatter detected */
46
+ formatter?: string;
47
+ /** Whether TypeScript is used (for Node projects) */
48
+ hasTypeScript: boolean;
49
+ /** Key config files found */
50
+ configFiles: string[];
51
+ }
52
+
53
+ // --- Detection Functions ---
54
+
55
+ /**
56
+ * Detect project metadata from a target directory.
57
+ * Reads common config files and extracts structured information.
58
+ */
59
+ export function detectProjectMetadata(targetPath: string): ProjectMetadata {
60
+ const metadata: ProjectMetadata = {
61
+ type: "unknown",
62
+ scripts: {},
63
+ languages: [],
64
+ hasTests: false,
65
+ hasLinting: false,
66
+ hasFormatting: false,
67
+ hasTypeScript: false,
68
+ configFiles: [],
69
+ };
70
+
71
+ // Check for Node.js project
72
+ const nodeMeta = detectNodeProject(targetPath);
73
+ if (nodeMeta) {
74
+ Object.assign(metadata, nodeMeta);
75
+ }
76
+
77
+ // Check for Python project
78
+ const pythonMeta = detectPythonProject(targetPath);
79
+ if (pythonMeta && metadata.type === "unknown") {
80
+ Object.assign(metadata, pythonMeta);
81
+ }
82
+
83
+ // Check for Rust project
84
+ const rustMeta = detectRustProject(targetPath);
85
+ if (rustMeta && metadata.type === "unknown") {
86
+ Object.assign(metadata, rustMeta);
87
+ }
88
+
89
+ // Check for Go project
90
+ const goMeta = detectGoProject(targetPath);
91
+ if (goMeta && metadata.type === "unknown") {
92
+ Object.assign(metadata, goMeta);
93
+ }
94
+
95
+ // Check for PHP project
96
+ const phpMeta = detectPhpProject(targetPath);
97
+ if (phpMeta && metadata.type === "unknown") {
98
+ Object.assign(metadata, phpMeta);
99
+ }
100
+
101
+ // Check for Ruby project
102
+ const rubyMeta = detectRubyProject(targetPath);
103
+ if (rubyMeta && metadata.type === "unknown") {
104
+ Object.assign(metadata, rubyMeta);
105
+ }
106
+
107
+ // Multi-project detection: if multiple types found
108
+ const types = [nodeMeta, pythonMeta, rustMeta, goMeta, phpMeta, rubyMeta]
109
+ .filter(Boolean)
110
+ .map(m => m!.type);
111
+
112
+ if (types.length > 1) {
113
+ metadata.type = "multi";
114
+ metadata.languages = [...new Set(types)];
115
+ }
116
+
117
+ return metadata;
118
+ }
119
+
120
+ /**
121
+ * Detect Node.js project from package.json and lockfiles
122
+ */
123
+ function detectNodeProject(targetPath: string): ProjectMetadata | null {
124
+ const packageJsonPath = path.join(targetPath, "package.json");
125
+ if (!fs.existsSync(packageJsonPath)) {
126
+ return null;
127
+ }
128
+
129
+ let packageJson: Record<string, unknown>;
130
+ try {
131
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
132
+ } catch {
133
+ return null;
134
+ }
135
+
136
+ const metadata: ProjectMetadata = {
137
+ type: "node",
138
+ name: typeof packageJson.name === "string" ? packageJson.name : undefined,
139
+ version: typeof packageJson.version === "string" ? packageJson.version : undefined,
140
+ packageManager: detectNodePackageManager(targetPath),
141
+ scripts: typeof packageJson.scripts === "object" && packageJson.scripts !== null
142
+ ? (packageJson.scripts as Record<string, string>)
143
+ : {},
144
+ languages: ["javascript"],
145
+ hasTests: false,
146
+ hasLinting: false,
147
+ hasFormatting: false,
148
+ hasTypeScript: fs.existsSync(path.join(targetPath, "tsconfig.json")),
149
+ configFiles: ["package.json"],
150
+ };
151
+
152
+ // Detect TypeScript
153
+ if (metadata.hasTypeScript) {
154
+ metadata.languages.push("typescript");
155
+ metadata.configFiles.push("tsconfig.json");
156
+ }
157
+
158
+ // Detect tests from scripts or config files
159
+ const scripts = Object.keys(metadata.scripts).join(" ").toLowerCase();
160
+ if (scripts.includes("test") || scripts.includes("spec")) {
161
+ metadata.hasTests = true;
162
+ }
163
+ // Detect test frameworks from dependencies
164
+ const allDeps = {
165
+ ...((packageJson.dependencies || {}) as Record<string, string>),
166
+ ...((packageJson.devDependencies || {}) as Record<string, string>),
167
+ };
168
+ if (allDeps["vitest"]) {
169
+ metadata.hasTests = true;
170
+ metadata.testFramework = "vitest";
171
+ } else if (allDeps["jest"]) {
172
+ metadata.hasTests = true;
173
+ metadata.testFramework = "jest";
174
+ } else if (allDeps["mocha"]) {
175
+ metadata.hasTests = true;
176
+ metadata.testFramework = "mocha";
177
+ } else if (allDeps["ava"]) {
178
+ metadata.hasTests = true;
179
+ metadata.testFramework = "ava";
180
+ } else if (allDeps["tap"]) {
181
+ metadata.hasTests = true;
182
+ metadata.testFramework = "tap";
183
+ } else if (allDeps["node:test"]) {
184
+ metadata.hasTests = true;
185
+ metadata.testFramework = "node:test";
186
+ }
187
+
188
+ // Detect linting
189
+ if (allDeps["eslint"] || fs.existsSync(path.join(targetPath, ".eslintrc")) ||
190
+ fs.existsSync(path.join(targetPath, ".eslintrc.js")) ||
191
+ fs.existsSync(path.join(targetPath, "eslint.config.js")) ||
192
+ fs.existsSync(path.join(targetPath, "eslint.config.mjs"))) {
193
+ metadata.hasLinting = true;
194
+ metadata.linter = "eslint";
195
+ metadata.configFiles.push("eslint config");
196
+ }
197
+ if (allDeps["@biomejs/biome"] || fs.existsSync(path.join(targetPath, "biome.json"))) {
198
+ metadata.hasLinting = true;
199
+ metadata.linter = metadata.linter ? `${metadata.linter}, biome` : "biome";
200
+ metadata.configFiles.push("biome.json");
201
+ }
202
+
203
+ // Detect formatting
204
+ if (allDeps["prettier"] || fs.existsSync(path.join(targetPath, ".prettierrc")) ||
205
+ fs.existsSync(path.join(targetPath, ".prettierrc.json"))) {
206
+ metadata.hasFormatting = true;
207
+ metadata.formatter = metadata.formatter ? `${metadata.formatter}, prettier` : "prettier";
208
+ metadata.configFiles.push("prettier config");
209
+ }
210
+ if (allDeps["@biomejs/biome"]) {
211
+ metadata.hasFormatting = true;
212
+ metadata.formatter = metadata.formatter ? `${metadata.formatter}, biome` : "biome";
213
+ }
214
+
215
+ return metadata;
216
+ }
217
+
218
+ /**
219
+ * Detect Node.js package manager from lockfiles
220
+ */
221
+ function detectNodePackageManager(targetPath: string): PackageManager | undefined {
222
+ if (fs.existsSync(path.join(targetPath, "bun.lockb")) || fs.existsSync(path.join(targetPath, "bun.lock"))) {
223
+ return "bun";
224
+ }
225
+ if (fs.existsSync(path.join(targetPath, "pnpm-lock.yaml"))) {
226
+ return "pnpm";
227
+ }
228
+ if (fs.existsSync(path.join(targetPath, "yarn.lock"))) {
229
+ return "yarn";
230
+ }
231
+ if (fs.existsSync(path.join(targetPath, "package-lock.json"))) {
232
+ return "npm";
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ /**
238
+ * Detect Python project from pyproject.toml, setup.py, requirements.txt
239
+ */
240
+ function detectPythonProject(targetPath: string): ProjectMetadata | null {
241
+ const pyprojectPath = path.join(targetPath, "pyproject.toml");
242
+ const setupPyPath = path.join(targetPath, "setup.py");
243
+ const requirementsPath = path.join(targetPath, "requirements.txt");
244
+
245
+ if (!fs.existsSync(pyprojectPath) && !fs.existsSync(setupPyPath) && !fs.existsSync(requirementsPath)) {
246
+ return null;
247
+ }
248
+
249
+ const metadata: ProjectMetadata = {
250
+ type: "python",
251
+ packageManager: detectPythonPackageManager(targetPath),
252
+ scripts: {},
253
+ languages: ["python"],
254
+ hasTests: false,
255
+ hasLinting: false,
256
+ hasFormatting: false,
257
+ hasTypeScript: false,
258
+ configFiles: [],
259
+ };
260
+
261
+ // Read pyproject.toml if available
262
+ if (fs.existsSync(pyprojectPath)) {
263
+ metadata.configFiles.push("pyproject.toml");
264
+ try {
265
+ const content = fs.readFileSync(pyprojectPath, "utf-8");
266
+
267
+ // Extract project name
268
+ const nameMatch = content.match(/\[project\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
269
+ if (nameMatch) metadata.name = nameMatch[1];
270
+
271
+ // Extract version
272
+ const versionMatch = content.match(/\[project\][\s\S]*?version\s*=\s*["']([^"']+)["']/);
273
+ if (versionMatch) metadata.version = versionMatch[1];
274
+
275
+ // Detect test framework
276
+ if (content.includes("pytest") || fs.existsSync(path.join(targetPath, "pytest.ini"))) {
277
+ metadata.hasTests = true;
278
+ metadata.testFramework = "pytest";
279
+ }
280
+
281
+ // Detect linting
282
+ if (content.includes("ruff") || fs.existsSync(path.join(targetPath, "ruff.toml"))) {
283
+ metadata.hasLinting = true;
284
+ metadata.linter = "ruff";
285
+ metadata.hasFormatting = true;
286
+ metadata.formatter = "ruff";
287
+ } else if (content.includes("pylint") || content.includes("flake8")) {
288
+ metadata.hasLinting = true;
289
+ metadata.linter = content.includes("pylint") ? "pylint" : "flake8";
290
+ }
291
+ } catch {
292
+ // Ignore parse errors
293
+ }
294
+ }
295
+
296
+ if (fs.existsSync(setupPyPath)) metadata.configFiles.push("setup.py");
297
+ if (fs.existsSync(requirementsPath)) metadata.configFiles.push("requirements.txt");
298
+
299
+ return metadata;
300
+ }
301
+
302
+ /**
303
+ * Detect Python package manager
304
+ */
305
+ function detectPythonPackageManager(targetPath: string): PackageManager | undefined {
306
+ if (fs.existsSync(path.join(targetPath, "uv.lock"))) {
307
+ return "uv";
308
+ }
309
+ if (fs.existsSync(path.join(targetPath, "poetry.lock"))) {
310
+ return "poetry";
311
+ }
312
+ if (fs.existsSync(path.join(targetPath, "Pipfile.lock")) || fs.existsSync(path.join(targetPath, "Pipfile"))) {
313
+ return "pip"; // pipenv uses Pipfile
314
+ }
315
+ if (fs.existsSync(path.join(targetPath, "requirements.txt"))) {
316
+ return "pip";
317
+ }
318
+ return undefined;
319
+ }
320
+
321
+ /**
322
+ * Detect Rust project from Cargo.toml
323
+ */
324
+ function detectRustProject(targetPath: string): ProjectMetadata | null {
325
+ const cargoPath = path.join(targetPath, "Cargo.toml");
326
+ if (!fs.existsSync(cargoPath)) {
327
+ return null;
328
+ }
329
+
330
+ const metadata: ProjectMetadata = {
331
+ type: "rust",
332
+ packageManager: "cargo",
333
+ scripts: {},
334
+ languages: ["rust"],
335
+ hasTests: true, // Cargo has built-in test support
336
+ hasLinting: false,
337
+ hasFormatting: false,
338
+ hasTypeScript: false,
339
+ configFiles: ["Cargo.toml"],
340
+ };
341
+
342
+ // Read Cargo.toml
343
+ try {
344
+ const content = fs.readFileSync(cargoPath, "utf-8");
345
+
346
+ // Extract package name
347
+ const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
348
+ if (nameMatch) metadata.name = nameMatch[1];
349
+
350
+ // Extract version
351
+ const versionMatch = content.match(/\[package\][\s\S]*?version\s*=\s*["']([^"']+)["']/);
352
+ if (versionMatch) metadata.version = versionMatch[1];
353
+ } catch {
354
+ // Ignore parse errors
355
+ }
356
+
357
+ // Check for clippy (linter)
358
+ if (fs.existsSync(path.join(targetPath, ".clippy.toml")) ||
359
+ fs.existsSync(path.join(targetPath, "clippy.toml"))) {
360
+ metadata.hasLinting = true;
361
+ metadata.linter = "clippy";
362
+ }
363
+
364
+ // Check for rustfmt
365
+ if (fs.existsSync(path.join(targetPath, ".rustfmt.toml")) ||
366
+ fs.existsSync(path.join(targetPath, "rustfmt.toml"))) {
367
+ metadata.hasFormatting = true;
368
+ metadata.formatter = "rustfmt";
369
+ }
370
+
371
+ return metadata;
372
+ }
373
+
374
+ /**
375
+ * Detect Go project from go.mod
376
+ */
377
+ function detectGoProject(targetPath: string): ProjectMetadata | null {
378
+ const goModPath = path.join(targetPath, "go.mod");
379
+ if (!fs.existsSync(goModPath)) {
380
+ return null;
381
+ }
382
+
383
+ const metadata: ProjectMetadata = {
384
+ type: "go",
385
+ packageManager: "gomod",
386
+ scripts: {},
387
+ languages: ["go"],
388
+ hasTests: true, // Go has built-in test support
389
+ hasLinting: false,
390
+ hasFormatting: false,
391
+ hasTypeScript: false,
392
+ configFiles: ["go.mod"],
393
+ };
394
+
395
+ // Read go.mod
396
+ try {
397
+ const content = fs.readFileSync(goModPath, "utf-8");
398
+
399
+ // Extract module name (first line: module example.com/module)
400
+ const moduleMatch = content.match(/^module\s+(\S+)/m);
401
+ if (moduleMatch) metadata.name = moduleMatch[1];
402
+
403
+ // Extract go version
404
+ const versionMatch = content.match(/^go\s+(\S+)/m);
405
+ if (versionMatch) metadata.version = versionMatch[1];
406
+ } catch {
407
+ // Ignore parse errors
408
+ }
409
+
410
+ // Check for golangci-lint
411
+ if (fs.existsSync(path.join(targetPath, ".golangci.yml")) ||
412
+ fs.existsSync(path.join(targetPath, ".golangci.yaml"))) {
413
+ metadata.hasLinting = true;
414
+ metadata.linter = "golangci-lint";
415
+ }
416
+
417
+ return metadata;
418
+ }
419
+
420
+ /**
421
+ * Detect PHP project from composer.json
422
+ */
423
+ function detectPhpProject(targetPath: string): ProjectMetadata | null {
424
+ const composerPath = path.join(targetPath, "composer.json");
425
+ if (!fs.existsSync(composerPath)) {
426
+ return null;
427
+ }
428
+
429
+ let composerJson: Record<string, unknown>;
430
+ try {
431
+ composerJson = JSON.parse(fs.readFileSync(composerPath, "utf-8"));
432
+ } catch {
433
+ return null;
434
+ }
435
+
436
+ const metadata: ProjectMetadata = {
437
+ type: "php",
438
+ packageManager: "composer",
439
+ name: typeof composerJson.name === "string" ? composerJson.name : undefined,
440
+ scripts: typeof composerJson.scripts === "object" && composerJson.scripts !== null
441
+ ? (composerJson.scripts as Record<string, string>)
442
+ : {},
443
+ languages: ["php"],
444
+ hasTests: false,
445
+ hasLinting: false,
446
+ hasFormatting: false,
447
+ hasTypeScript: false,
448
+ configFiles: ["composer.json"],
449
+ };
450
+
451
+ // Detect tests from scripts
452
+ const scripts = Object.keys(metadata.scripts).join(" ").toLowerCase();
453
+ if (scripts.includes("test") || scripts.includes("phpunit")) {
454
+ metadata.hasTests = true;
455
+ metadata.testFramework = "phpunit";
456
+ }
457
+
458
+ return metadata;
459
+ }
460
+
461
+ /**
462
+ * Detect Ruby project from Gemfile
463
+ */
464
+ function detectRubyProject(targetPath: string): ProjectMetadata | null {
465
+ const gemfilePath = path.join(targetPath, "Gemfile");
466
+ const gemspecPath = fs.readdirSync(targetPath).find(f => f.endsWith(".gemspec"));
467
+
468
+ if (!fs.existsSync(gemfilePath) && !gemspecPath) {
469
+ return null;
470
+ }
471
+
472
+ const metadata: ProjectMetadata = {
473
+ type: "ruby",
474
+ packageManager: "bundler",
475
+ scripts: {},
476
+ languages: ["ruby"],
477
+ hasTests: false,
478
+ hasLinting: false,
479
+ hasFormatting: false,
480
+ hasTypeScript: false,
481
+ configFiles: [],
482
+ };
483
+
484
+ if (fs.existsSync(gemfilePath)) metadata.configFiles.push("Gemfile");
485
+ if (gemspecPath) metadata.configFiles.push(gemspecPath);
486
+
487
+ // Check for Rakefile to get tasks
488
+ const rakefilePath = path.join(targetPath, "Rakefile");
489
+ if (fs.existsSync(rakefilePath)) {
490
+ metadata.configFiles.push("Rakefile");
491
+ }
492
+
493
+ // Check for test framework
494
+ if (fs.existsSync(path.join(targetPath, "spec"))) {
495
+ metadata.hasTests = true;
496
+ metadata.testFramework = "rspec";
497
+ } else if (fs.existsSync(path.join(targetPath, "test"))) {
498
+ metadata.hasTests = true;
499
+ metadata.testFramework = "minitest";
500
+ }
501
+
502
+ // Check for rubocop
503
+ if (fs.existsSync(path.join(targetPath, ".rubocop.yml"))) {
504
+ metadata.hasLinting = true;
505
+ metadata.linter = "rubocop";
506
+ metadata.configFiles.push(".rubocop.yml");
507
+ }
508
+
509
+ return metadata;
510
+ }
511
+
512
+ // --- Formatting Utilities ---
513
+
514
+ /**
515
+ * Format project metadata for display in reports
516
+ */
517
+ export function formatProjectMetadata(metadata: ProjectMetadata): string {
518
+ const lines: string[] = [];
519
+
520
+ // Header
521
+ const name = metadata.name ? `**${metadata.name}**` : "Project";
522
+ const type = metadata.type !== "unknown" ? `(${capitalize(metadata.type)})` : "";
523
+ lines.push(`๐Ÿ“Š ${name} ${type}`.trim());
524
+
525
+ // Package manager
526
+ if (metadata.packageManager) {
527
+ lines.push(`๐Ÿ“ฆ Package Manager: ${capitalize(metadata.packageManager)}`);
528
+ }
529
+
530
+ // Languages
531
+ if (metadata.languages.length > 0) {
532
+ lines.push(`๐Ÿ“ Languages: ${metadata.languages.map(capitalize).join(", ")}`);
533
+ }
534
+
535
+ // Tools
536
+ const tools: string[] = [];
537
+ if (metadata.hasTests) {
538
+ tools.push(metadata.testFramework ? `๐Ÿงช ${metadata.testFramework}` : "๐Ÿงช tests");
539
+ }
540
+ if (metadata.hasLinting) {
541
+ tools.push(metadata.linter ? `๐Ÿ” ${metadata.linter}` : "๐Ÿ” linting");
542
+ }
543
+ if (metadata.hasFormatting) {
544
+ tools.push(metadata.formatter ? `โœจ ${metadata.formatter}` : "โœจ formatting");
545
+ }
546
+ if (tools.length > 0) {
547
+ lines.push(tools.join(" | "));
548
+ }
549
+
550
+ // Config files (limited)
551
+ if (metadata.configFiles.length > 0) {
552
+ const limited = metadata.configFiles.slice(0, 5);
553
+ const more = metadata.configFiles.length > 5 ? ` (+${metadata.configFiles.length - 5} more)` : "";
554
+ lines.push(`โš™๏ธ Config: ${limited.join(", ")}${more}`);
555
+ }
556
+
557
+ return lines.join("\n");
558
+ }
559
+
560
+ /**
561
+ * Get available commands for a project (build, test, lint, etc.)
562
+ */
563
+ export function getAvailableCommands(metadata: ProjectMetadata): Array<{action: string; command: string}> {
564
+ const commands: Array<{action: string; command: string}> = [];
565
+
566
+ // Node.js projects - use npm scripts
567
+ if (metadata.type === "node" && Object.keys(metadata.scripts).length > 0) {
568
+ const scriptPriority = ["test", "build", "lint", "format", "dev", "start", "typecheck"];
569
+
570
+ for (const priority of scriptPriority) {
571
+ const matching = Object.entries(metadata.scripts).find(([name]) =>
572
+ name.toLowerCase().includes(priority)
573
+ );
574
+ if (matching) {
575
+ const runCmd = metadata.packageManager === "bun" ? "bun run" :
576
+ metadata.packageManager === "pnpm" ? "pnpm" :
577
+ metadata.packageManager === "yarn" ? "yarn" :
578
+ "npm run";
579
+ commands.push({
580
+ action: priority,
581
+ command: `${runCmd} ${matching[0]}`,
582
+ });
583
+ }
584
+ }
585
+ }
586
+
587
+ // Python projects
588
+ if (metadata.type === "python") {
589
+ if (metadata.hasTests) {
590
+ const testCmd = metadata.packageManager === "poetry" ? "poetry run pytest" :
591
+ metadata.packageManager === "uv" ? "uv run pytest" :
592
+ "pytest";
593
+ commands.push({ action: "test", command: testCmd });
594
+ }
595
+ if (metadata.linter?.includes("ruff")) {
596
+ commands.push({ action: "lint", command: "ruff check ." });
597
+ commands.push({ action: "format", command: "ruff format ." });
598
+ }
599
+ }
600
+
601
+ // Rust projects
602
+ if (metadata.type === "rust") {
603
+ commands.push({ action: "build", command: "cargo build" });
604
+ commands.push({ action: "test", command: "cargo test" });
605
+ if (metadata.hasLinting) {
606
+ commands.push({ action: "lint", command: "cargo clippy" });
607
+ }
608
+ }
609
+
610
+ // Go projects
611
+ if (metadata.type === "go") {
612
+ commands.push({ action: "build", command: "go build" });
613
+ commands.push({ action: "test", command: "go test ./..." });
614
+ if (metadata.hasLinting) {
615
+ commands.push({ action: "lint", command: "golangci-lint run" });
616
+ }
617
+ }
618
+
619
+ return commands;
620
+ }
621
+
622
+ function capitalize(str: string): string {
623
+ return str.charAt(0).toUpperCase() + str.slice(1);
624
+ }