@hyperfrontend/project-scope 0.1.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 (391) hide show
  1. package/ARCHITECTURE.md +370 -0
  2. package/CHANGELOG.md +10 -0
  3. package/FUNDING.md +141 -0
  4. package/LICENSE.md +21 -0
  5. package/README.md +242 -0
  6. package/SECURITY.md +82 -0
  7. package/analyze.d.ts +33 -0
  8. package/analyze.d.ts.map +1 -0
  9. package/cli/commands/analyze.d.ts +20 -0
  10. package/cli/commands/analyze.d.ts.map +1 -0
  11. package/cli/commands/config.d.ts +20 -0
  12. package/cli/commands/config.d.ts.map +1 -0
  13. package/cli/commands/deps.d.ts +18 -0
  14. package/cli/commands/deps.d.ts.map +1 -0
  15. package/cli/commands/tree.d.ts +24 -0
  16. package/cli/commands/tree.d.ts.map +1 -0
  17. package/cli/index.cjs.js +6639 -0
  18. package/cli/index.cjs.js.map +1 -0
  19. package/cli/index.d.ts +7 -0
  20. package/cli/index.d.ts.map +1 -0
  21. package/cli/index.esm.js +6629 -0
  22. package/cli/index.esm.js.map +1 -0
  23. package/cli/run.d.ts +25 -0
  24. package/cli/run.d.ts.map +1 -0
  25. package/cli/types.d.ts +55 -0
  26. package/cli/types.d.ts.map +1 -0
  27. package/core/cache.d.ts +157 -0
  28. package/core/cache.d.ts.map +1 -0
  29. package/core/encoding/convert.d.ts +32 -0
  30. package/core/encoding/convert.d.ts.map +1 -0
  31. package/core/encoding/detect.d.ts +86 -0
  32. package/core/encoding/detect.d.ts.map +1 -0
  33. package/core/encoding/index.cjs.js +751 -0
  34. package/core/encoding/index.cjs.js.map +1 -0
  35. package/core/encoding/index.d.ts +3 -0
  36. package/core/encoding/index.d.ts.map +1 -0
  37. package/core/encoding/index.esm.js +736 -0
  38. package/core/encoding/index.esm.js.map +1 -0
  39. package/core/errors/structured-errors.d.ts +64 -0
  40. package/core/errors/structured-errors.d.ts.map +1 -0
  41. package/core/fs/directory.d.ts +88 -0
  42. package/core/fs/directory.d.ts.map +1 -0
  43. package/core/fs/index.cjs.js +1079 -0
  44. package/core/fs/index.cjs.js.map +1 -0
  45. package/core/fs/index.d.ts +6 -0
  46. package/core/fs/index.d.ts.map +1 -0
  47. package/core/fs/index.esm.js +1056 -0
  48. package/core/fs/index.esm.js.map +1 -0
  49. package/core/fs/read.d.ts +86 -0
  50. package/core/fs/read.d.ts.map +1 -0
  51. package/core/fs/stat.d.ts +58 -0
  52. package/core/fs/stat.d.ts.map +1 -0
  53. package/core/fs/traversal.d.ts +26 -0
  54. package/core/fs/traversal.d.ts.map +1 -0
  55. package/core/fs/write.d.ts +75 -0
  56. package/core/fs/write.d.ts.map +1 -0
  57. package/core/index.cjs.js +2376 -0
  58. package/core/index.cjs.js.map +1 -0
  59. package/core/index.d.ts +9 -0
  60. package/core/index.d.ts.map +1 -0
  61. package/core/index.esm.js +2286 -0
  62. package/core/index.esm.js.map +1 -0
  63. package/core/logger.d.ts +111 -0
  64. package/core/logger.d.ts.map +1 -0
  65. package/core/path/index.cjs.js +254 -0
  66. package/core/path/index.cjs.js.map +1 -0
  67. package/core/path/index.d.ts +5 -0
  68. package/core/path/index.d.ts.map +1 -0
  69. package/core/path/index.esm.js +233 -0
  70. package/core/path/index.esm.js.map +1 -0
  71. package/core/path/join.d.ts +17 -0
  72. package/core/path/join.d.ts.map +1 -0
  73. package/core/path/normalize.d.ts +37 -0
  74. package/core/path/normalize.d.ts.map +1 -0
  75. package/core/path/resolve.d.ts +52 -0
  76. package/core/path/resolve.d.ts.map +1 -0
  77. package/core/path/segments.d.ts +59 -0
  78. package/core/path/segments.d.ts.map +1 -0
  79. package/core/patterns/glob.d.ts +46 -0
  80. package/core/patterns/glob.d.ts.map +1 -0
  81. package/core/platform/detect.d.ts +66 -0
  82. package/core/platform/detect.d.ts.map +1 -0
  83. package/core/platform/index.cjs.js +241 -0
  84. package/core/platform/index.cjs.js.map +1 -0
  85. package/core/platform/index.d.ts +3 -0
  86. package/core/platform/index.d.ts.map +1 -0
  87. package/core/platform/index.esm.js +226 -0
  88. package/core/platform/index.esm.js.map +1 -0
  89. package/core/platform/line-endings.d.ts +48 -0
  90. package/core/platform/line-endings.d.ts.map +1 -0
  91. package/heuristics/dependencies/analyze.d.ts +77 -0
  92. package/heuristics/dependencies/analyze.d.ts.map +1 -0
  93. package/heuristics/dependencies/index.cjs.js +1126 -0
  94. package/heuristics/dependencies/index.cjs.js.map +1 -0
  95. package/heuristics/dependencies/index.d.ts +2 -0
  96. package/heuristics/dependencies/index.d.ts.map +1 -0
  97. package/heuristics/dependencies/index.esm.js +1122 -0
  98. package/heuristics/dependencies/index.esm.js.map +1 -0
  99. package/heuristics/entry-points/discover.d.ts +86 -0
  100. package/heuristics/entry-points/discover.d.ts.map +1 -0
  101. package/heuristics/entry-points/index.cjs.js +1581 -0
  102. package/heuristics/entry-points/index.cjs.js.map +1 -0
  103. package/heuristics/entry-points/index.d.ts +2 -0
  104. package/heuristics/entry-points/index.d.ts.map +1 -0
  105. package/heuristics/entry-points/index.esm.js +1577 -0
  106. package/heuristics/entry-points/index.esm.js.map +1 -0
  107. package/heuristics/framework/identify.d.ts +84 -0
  108. package/heuristics/framework/identify.d.ts.map +1 -0
  109. package/heuristics/framework/index.cjs.js +3618 -0
  110. package/heuristics/framework/index.cjs.js.map +1 -0
  111. package/heuristics/framework/index.d.ts +2 -0
  112. package/heuristics/framework/index.d.ts.map +1 -0
  113. package/heuristics/framework/index.esm.js +3614 -0
  114. package/heuristics/framework/index.esm.js.map +1 -0
  115. package/heuristics/index.cjs.js +4833 -0
  116. package/heuristics/index.cjs.js.map +1 -0
  117. package/heuristics/index.d.ts +5 -0
  118. package/heuristics/index.d.ts.map +1 -0
  119. package/heuristics/index.esm.js +4822 -0
  120. package/heuristics/index.esm.js.map +1 -0
  121. package/heuristics/project-type/detect.d.ts +61 -0
  122. package/heuristics/project-type/detect.d.ts.map +1 -0
  123. package/heuristics/project-type/index.cjs.js +3633 -0
  124. package/heuristics/project-type/index.cjs.js.map +1 -0
  125. package/heuristics/project-type/index.d.ts +2 -0
  126. package/heuristics/project-type/index.d.ts.map +1 -0
  127. package/heuristics/project-type/index.esm.js +3631 -0
  128. package/heuristics/project-type/index.esm.js.map +1 -0
  129. package/index.cjs.js +10255 -0
  130. package/index.cjs.js.map +1 -0
  131. package/index.d.ts +10 -0
  132. package/index.d.ts.map +1 -0
  133. package/index.esm.js +10006 -0
  134. package/index.esm.js.map +1 -0
  135. package/models/index.cjs.js +3 -0
  136. package/models/index.cjs.js.map +1 -0
  137. package/models/index.d.ts +176 -0
  138. package/models/index.d.ts.map +1 -0
  139. package/models/index.esm.js +2 -0
  140. package/models/index.esm.js.map +1 -0
  141. package/nx/detect.d.ts +105 -0
  142. package/nx/detect.d.ts.map +1 -0
  143. package/nx/devkit-loader.d.ts +62 -0
  144. package/nx/devkit-loader.d.ts.map +1 -0
  145. package/nx/index.cjs.js +1302 -0
  146. package/nx/index.cjs.js.map +1 -0
  147. package/nx/index.d.ts +4 -0
  148. package/nx/index.d.ts.map +1 -0
  149. package/nx/index.esm.js +1286 -0
  150. package/nx/index.esm.js.map +1 -0
  151. package/nx/project-config.d.ts +109 -0
  152. package/nx/project-config.d.ts.map +1 -0
  153. package/package.json +218 -0
  154. package/project/config/detect.d.ts +77 -0
  155. package/project/config/detect.d.ts.map +1 -0
  156. package/project/config/index.cjs.js +1982 -0
  157. package/project/config/index.cjs.js.map +1 -0
  158. package/project/config/index.d.ts +4 -0
  159. package/project/config/index.d.ts.map +1 -0
  160. package/project/config/index.esm.js +1971 -0
  161. package/project/config/index.esm.js.map +1 -0
  162. package/project/config/parse.d.ts +53 -0
  163. package/project/config/parse.d.ts.map +1 -0
  164. package/project/config/patterns.d.ts +31 -0
  165. package/project/config/patterns.d.ts.map +1 -0
  166. package/project/index.cjs.js +2585 -0
  167. package/project/index.cjs.js.map +1 -0
  168. package/project/index.d.ts +5 -0
  169. package/project/index.d.ts.map +1 -0
  170. package/project/index.esm.js +2549 -0
  171. package/project/index.esm.js.map +1 -0
  172. package/project/package/dependencies.d.ts +101 -0
  173. package/project/package/dependencies.d.ts.map +1 -0
  174. package/project/package/index.cjs.js +972 -0
  175. package/project/package/index.cjs.js.map +1 -0
  176. package/project/package/index.d.ts +3 -0
  177. package/project/package/index.d.ts.map +1 -0
  178. package/project/package/index.esm.js +957 -0
  179. package/project/package/index.esm.js.map +1 -0
  180. package/project/package/read.d.ts +66 -0
  181. package/project/package/read.d.ts.map +1 -0
  182. package/project/root/detect.d.ts +65 -0
  183. package/project/root/detect.d.ts.map +1 -0
  184. package/project/root/index.cjs.js +860 -0
  185. package/project/root/index.cjs.js.map +1 -0
  186. package/project/root/index.d.ts +2 -0
  187. package/project/root/index.d.ts.map +1 -0
  188. package/project/root/index.esm.js +853 -0
  189. package/project/root/index.esm.js.map +1 -0
  190. package/project/traversal/index.cjs.js +1179 -0
  191. package/project/traversal/index.cjs.js.map +1 -0
  192. package/project/traversal/index.d.ts +3 -0
  193. package/project/traversal/index.d.ts.map +1 -0
  194. package/project/traversal/index.esm.js +1173 -0
  195. package/project/traversal/index.esm.js.map +1 -0
  196. package/project/traversal/search.d.ts +59 -0
  197. package/project/traversal/search.d.ts.map +1 -0
  198. package/project/traversal/walk.d.ts +63 -0
  199. package/project/traversal/walk.d.ts.map +1 -0
  200. package/tech/backend/detect-all.d.ts +13 -0
  201. package/tech/backend/detect-all.d.ts.map +1 -0
  202. package/tech/backend/express.d.ts +11 -0
  203. package/tech/backend/express.d.ts.map +1 -0
  204. package/tech/backend/fastify.d.ts +11 -0
  205. package/tech/backend/fastify.d.ts.map +1 -0
  206. package/tech/backend/hono.d.ts +11 -0
  207. package/tech/backend/hono.d.ts.map +1 -0
  208. package/tech/backend/index.cjs.js +939 -0
  209. package/tech/backend/index.cjs.js.map +1 -0
  210. package/tech/backend/index.d.ts +8 -0
  211. package/tech/backend/index.d.ts.map +1 -0
  212. package/tech/backend/index.esm.js +931 -0
  213. package/tech/backend/index.esm.js.map +1 -0
  214. package/tech/backend/koa.d.ts +11 -0
  215. package/tech/backend/koa.d.ts.map +1 -0
  216. package/tech/backend/nestjs.d.ts +11 -0
  217. package/tech/backend/nestjs.d.ts.map +1 -0
  218. package/tech/backend/types.d.ts +31 -0
  219. package/tech/backend/types.d.ts.map +1 -0
  220. package/tech/build/babel.d.ts +13 -0
  221. package/tech/build/babel.d.ts.map +1 -0
  222. package/tech/build/detect-all.d.ts +13 -0
  223. package/tech/build/detect-all.d.ts.map +1 -0
  224. package/tech/build/esbuild.d.ts +11 -0
  225. package/tech/build/esbuild.d.ts.map +1 -0
  226. package/tech/build/index.cjs.js +1118 -0
  227. package/tech/build/index.cjs.js.map +1 -0
  228. package/tech/build/index.d.ts +10 -0
  229. package/tech/build/index.d.ts.map +1 -0
  230. package/tech/build/index.esm.js +1102 -0
  231. package/tech/build/index.esm.js.map +1 -0
  232. package/tech/build/parcel.d.ts +13 -0
  233. package/tech/build/parcel.d.ts.map +1 -0
  234. package/tech/build/rollup.d.ts +13 -0
  235. package/tech/build/rollup.d.ts.map +1 -0
  236. package/tech/build/swc.d.ts +13 -0
  237. package/tech/build/swc.d.ts.map +1 -0
  238. package/tech/build/types.d.ts +31 -0
  239. package/tech/build/types.d.ts.map +1 -0
  240. package/tech/build/vite.d.ts +13 -0
  241. package/tech/build/vite.d.ts.map +1 -0
  242. package/tech/build/webpack.d.ts +13 -0
  243. package/tech/build/webpack.d.ts.map +1 -0
  244. package/tech/frontend/angular.d.ts +11 -0
  245. package/tech/frontend/angular.d.ts.map +1 -0
  246. package/tech/frontend/astro.d.ts +11 -0
  247. package/tech/frontend/astro.d.ts.map +1 -0
  248. package/tech/frontend/detect-all.d.ts +13 -0
  249. package/tech/frontend/detect-all.d.ts.map +1 -0
  250. package/tech/frontend/gatsby.d.ts +11 -0
  251. package/tech/frontend/gatsby.d.ts.map +1 -0
  252. package/tech/frontend/index.cjs.js +1310 -0
  253. package/tech/frontend/index.cjs.js.map +1 -0
  254. package/tech/frontend/index.d.ts +15 -0
  255. package/tech/frontend/index.d.ts.map +1 -0
  256. package/tech/frontend/index.esm.js +1295 -0
  257. package/tech/frontend/index.esm.js.map +1 -0
  258. package/tech/frontend/nextjs.d.ts +11 -0
  259. package/tech/frontend/nextjs.d.ts.map +1 -0
  260. package/tech/frontend/nuxt.d.ts +11 -0
  261. package/tech/frontend/nuxt.d.ts.map +1 -0
  262. package/tech/frontend/qwik.d.ts +11 -0
  263. package/tech/frontend/qwik.d.ts.map +1 -0
  264. package/tech/frontend/react.d.ts +11 -0
  265. package/tech/frontend/react.d.ts.map +1 -0
  266. package/tech/frontend/remix.d.ts +11 -0
  267. package/tech/frontend/remix.d.ts.map +1 -0
  268. package/tech/frontend/solid.d.ts +11 -0
  269. package/tech/frontend/solid.d.ts.map +1 -0
  270. package/tech/frontend/svelte.d.ts +11 -0
  271. package/tech/frontend/svelte.d.ts.map +1 -0
  272. package/tech/frontend/sveltekit.d.ts +11 -0
  273. package/tech/frontend/sveltekit.d.ts.map +1 -0
  274. package/tech/frontend/types.d.ts +35 -0
  275. package/tech/frontend/types.d.ts.map +1 -0
  276. package/tech/frontend/vue.d.ts +11 -0
  277. package/tech/frontend/vue.d.ts.map +1 -0
  278. package/tech/index.cjs.js +3684 -0
  279. package/tech/index.cjs.js.map +1 -0
  280. package/tech/index.d.ts +96 -0
  281. package/tech/index.d.ts.map +1 -0
  282. package/tech/index.esm.js +3603 -0
  283. package/tech/index.esm.js.map +1 -0
  284. package/tech/legacy/angularjs.d.ts +12 -0
  285. package/tech/legacy/angularjs.d.ts.map +1 -0
  286. package/tech/legacy/backbone.d.ts +11 -0
  287. package/tech/legacy/backbone.d.ts.map +1 -0
  288. package/tech/legacy/detect-all.d.ts +13 -0
  289. package/tech/legacy/detect-all.d.ts.map +1 -0
  290. package/tech/legacy/ember.d.ts +11 -0
  291. package/tech/legacy/ember.d.ts.map +1 -0
  292. package/tech/legacy/index.cjs.js +903 -0
  293. package/tech/legacy/index.cjs.js.map +1 -0
  294. package/tech/legacy/index.d.ts +7 -0
  295. package/tech/legacy/index.d.ts.map +1 -0
  296. package/tech/legacy/index.esm.js +896 -0
  297. package/tech/legacy/index.esm.js.map +1 -0
  298. package/tech/legacy/jquery.d.ts +11 -0
  299. package/tech/legacy/jquery.d.ts.map +1 -0
  300. package/tech/legacy/types.d.ts +33 -0
  301. package/tech/legacy/types.d.ts.map +1 -0
  302. package/tech/linting/biome.d.ts +11 -0
  303. package/tech/linting/biome.d.ts.map +1 -0
  304. package/tech/linting/detect-all.d.ts +13 -0
  305. package/tech/linting/detect-all.d.ts.map +1 -0
  306. package/tech/linting/eslint.d.ts +13 -0
  307. package/tech/linting/eslint.d.ts.map +1 -0
  308. package/tech/linting/index.cjs.js +992 -0
  309. package/tech/linting/index.cjs.js.map +1 -0
  310. package/tech/linting/index.d.ts +7 -0
  311. package/tech/linting/index.d.ts.map +1 -0
  312. package/tech/linting/index.esm.js +982 -0
  313. package/tech/linting/index.esm.js.map +1 -0
  314. package/tech/linting/prettier.d.ts +13 -0
  315. package/tech/linting/prettier.d.ts.map +1 -0
  316. package/tech/linting/stylelint.d.ts +13 -0
  317. package/tech/linting/stylelint.d.ts.map +1 -0
  318. package/tech/linting/types.d.ts +31 -0
  319. package/tech/linting/types.d.ts.map +1 -0
  320. package/tech/monorepo/detect-all.d.ts +13 -0
  321. package/tech/monorepo/detect-all.d.ts.map +1 -0
  322. package/tech/monorepo/index.cjs.js +1021 -0
  323. package/tech/monorepo/index.cjs.js.map +1 -0
  324. package/tech/monorepo/index.d.ts +10 -0
  325. package/tech/monorepo/index.d.ts.map +1 -0
  326. package/tech/monorepo/index.esm.js +1011 -0
  327. package/tech/monorepo/index.esm.js.map +1 -0
  328. package/tech/monorepo/lerna.d.ts +11 -0
  329. package/tech/monorepo/lerna.d.ts.map +1 -0
  330. package/tech/monorepo/npm-workspaces.d.ts +11 -0
  331. package/tech/monorepo/npm-workspaces.d.ts.map +1 -0
  332. package/tech/monorepo/nx.d.ts +11 -0
  333. package/tech/monorepo/nx.d.ts.map +1 -0
  334. package/tech/monorepo/pnpm-workspaces.d.ts +9 -0
  335. package/tech/monorepo/pnpm-workspaces.d.ts.map +1 -0
  336. package/tech/monorepo/rush.d.ts +11 -0
  337. package/tech/monorepo/rush.d.ts.map +1 -0
  338. package/tech/monorepo/turborepo.d.ts +11 -0
  339. package/tech/monorepo/turborepo.d.ts.map +1 -0
  340. package/tech/monorepo/types.d.ts +39 -0
  341. package/tech/monorepo/types.d.ts.map +1 -0
  342. package/tech/monorepo/yarn-workspaces.d.ts +11 -0
  343. package/tech/monorepo/yarn-workspaces.d.ts.map +1 -0
  344. package/tech/shared-utils/detector-helpers.d.ts +52 -0
  345. package/tech/shared-utils/detector-helpers.d.ts.map +1 -0
  346. package/tech/shared-utils/types.d.ts +41 -0
  347. package/tech/shared-utils/types.d.ts.map +1 -0
  348. package/tech/testing/cypress.d.ts +13 -0
  349. package/tech/testing/cypress.d.ts.map +1 -0
  350. package/tech/testing/detect-all.d.ts +13 -0
  351. package/tech/testing/detect-all.d.ts.map +1 -0
  352. package/tech/testing/index.cjs.js +1031 -0
  353. package/tech/testing/index.cjs.js.map +1 -0
  354. package/tech/testing/index.d.ts +8 -0
  355. package/tech/testing/index.d.ts.map +1 -0
  356. package/tech/testing/index.esm.js +1018 -0
  357. package/tech/testing/index.esm.js.map +1 -0
  358. package/tech/testing/jest.d.ts +13 -0
  359. package/tech/testing/jest.d.ts.map +1 -0
  360. package/tech/testing/mocha.d.ts +13 -0
  361. package/tech/testing/mocha.d.ts.map +1 -0
  362. package/tech/testing/playwright.d.ts +13 -0
  363. package/tech/testing/playwright.d.ts.map +1 -0
  364. package/tech/testing/types.d.ts +35 -0
  365. package/tech/testing/types.d.ts.map +1 -0
  366. package/tech/testing/vitest.d.ts +13 -0
  367. package/tech/testing/vitest.d.ts.map +1 -0
  368. package/tech/types/detectors.d.ts +67 -0
  369. package/tech/types/detectors.d.ts.map +1 -0
  370. package/tech/types/index.cjs.js +1045 -0
  371. package/tech/types/index.cjs.js.map +1 -0
  372. package/tech/types/index.d.ts +2 -0
  373. package/tech/types/index.d.ts.map +1 -0
  374. package/tech/types/index.esm.js +1039 -0
  375. package/tech/types/index.esm.js.map +1 -0
  376. package/vfs/commit.d.ts +32 -0
  377. package/vfs/commit.d.ts.map +1 -0
  378. package/vfs/diff.d.ts +73 -0
  379. package/vfs/diff.d.ts.map +1 -0
  380. package/vfs/factory.d.ts +37 -0
  381. package/vfs/factory.d.ts.map +1 -0
  382. package/vfs/fs-tree.d.ts +13 -0
  383. package/vfs/fs-tree.d.ts.map +1 -0
  384. package/vfs/index.cjs.js +1600 -0
  385. package/vfs/index.cjs.js.map +1 -0
  386. package/vfs/index.d.ts +6 -0
  387. package/vfs/index.d.ts.map +1 -0
  388. package/vfs/index.esm.js +1590 -0
  389. package/vfs/index.esm.js.map +1 -0
  390. package/vfs/types.d.ts +178 -0
  391. package/vfs/types.d.ts.map +1 -0
@@ -0,0 +1,3614 @@
1
+ import { join as join$1 } from 'node:path';
2
+ import { existsSync, readFileSync, statSync, lstatSync, readdirSync } from 'node:fs';
3
+
4
+ /**
5
+ * Safe copies of Map built-in via factory function.
6
+ *
7
+ * Since constructors cannot be safely captured via Object.assign, this module
8
+ * provides a factory function that uses Reflect.construct internally.
9
+ *
10
+ * These references are captured at module initialization time to protect against
11
+ * prototype pollution attacks. Import only what you need for tree-shaking.
12
+ *
13
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/map
14
+ */
15
+ // Capture references at module initialization time
16
+ const _Map = globalThis.Map;
17
+ const _Reflect$2 = globalThis.Reflect;
18
+ /**
19
+ * (Safe copy) Creates a new Map using the captured Map constructor.
20
+ * Use this instead of `new Map()`.
21
+ *
22
+ * @param iterable - Optional iterable of key-value pairs.
23
+ * @returns A new Map instance.
24
+ */
25
+ const createMap = (iterable) => _Reflect$2.construct(_Map, iterable ? [iterable] : []);
26
+
27
+ /**
28
+ * Safe copies of Object built-in methods.
29
+ *
30
+ * These references are captured at module initialization time to protect against
31
+ * prototype pollution attacks. Import only what you need for tree-shaking.
32
+ *
33
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/object
34
+ */
35
+ // Capture references at module initialization time
36
+ const _Object = globalThis.Object;
37
+ /**
38
+ * (Safe copy) Prevents modification of existing property attributes and values,
39
+ * and prevents the addition of new properties.
40
+ */
41
+ const freeze = _Object.freeze;
42
+ /**
43
+ * (Safe copy) Returns the names of the enumerable string properties and methods of an object.
44
+ */
45
+ const keys = _Object.keys;
46
+ /**
47
+ * (Safe copy) Returns an array of key/values of the enumerable own properties of an object.
48
+ */
49
+ const entries = _Object.entries;
50
+ /**
51
+ * (Safe copy) Returns an array of values of the enumerable own properties of an object.
52
+ */
53
+ const values = _Object.values;
54
+ /**
55
+ * (Safe copy) Adds one or more properties to an object, and/or modifies attributes of existing properties.
56
+ */
57
+ const defineProperties = _Object.defineProperties;
58
+
59
+ /**
60
+ * Safe copies of Set built-in via factory function.
61
+ *
62
+ * Since constructors cannot be safely captured via Object.assign, this module
63
+ * provides a factory function that uses Reflect.construct internally.
64
+ *
65
+ * These references are captured at module initialization time to protect against
66
+ * prototype pollution attacks. Import only what you need for tree-shaking.
67
+ *
68
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/set
69
+ */
70
+ // Capture references at module initialization time
71
+ const _Set = globalThis.Set;
72
+ const _Reflect$1 = globalThis.Reflect;
73
+ /**
74
+ * (Safe copy) Creates a new Set using the captured Set constructor.
75
+ * Use this instead of `new Set()`.
76
+ *
77
+ * @param iterable - Optional iterable of values.
78
+ * @returns A new Set instance.
79
+ */
80
+ const createSet = (iterable) => _Reflect$1.construct(_Set, iterable ? [iterable] : []);
81
+
82
+ /**
83
+ * Global registry of all caches for bulk operations.
84
+ */
85
+ const cacheRegistry = createSet();
86
+ /**
87
+ * Create a cache with optional TTL and size limits.
88
+ *
89
+ * The cache provides a simple key-value store with:
90
+ * - Optional TTL (time-to-live) for automatic expiration
91
+ * - Optional maxSize for limiting cache size with FIFO eviction
92
+ * - Lazy expiration (entries are checked on access)
93
+ *
94
+ * @param options - Cache configuration options
95
+ * @returns Cache instance
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // Basic cache
100
+ * const cache = createCache<string, number>()
101
+ * cache.set('answer', 42)
102
+ * cache.get('answer') // 42
103
+ *
104
+ * // Cache with TTL (expires after 60 seconds)
105
+ * const ttlCache = createCache<string, object>({ ttl: 60000 })
106
+ *
107
+ * // Cache with max size (evicts oldest when full)
108
+ * const lruCache = createCache<string, object>({ maxSize: 100 })
109
+ *
110
+ * // Combined options
111
+ * const configCache = createCache<string, object>({
112
+ * ttl: 30000,
113
+ * maxSize: 50
114
+ * })
115
+ * ```
116
+ */
117
+ function createCache(options) {
118
+ const { ttl, maxSize } = options ?? {};
119
+ const store = createMap();
120
+ // Track insertion order for FIFO eviction
121
+ const insertionOrder = [];
122
+ /**
123
+ * Check if an entry is expired.
124
+ *
125
+ * @param entry - Cache entry to check
126
+ * @returns True if entry is expired
127
+ */
128
+ function isExpired(entry) {
129
+ if (ttl === undefined)
130
+ return false;
131
+ // eslint-disable-next-line workspace/no-unsafe-builtin-methods -- Date.now() is needed for Jest fake timers compatibility
132
+ return Date.now() - entry.timestamp > ttl;
133
+ }
134
+ /**
135
+ * Evict oldest entries to make room for new ones.
136
+ */
137
+ function evictIfNeeded() {
138
+ if (maxSize === undefined)
139
+ return;
140
+ while (store.size >= maxSize && insertionOrder.length > 0) {
141
+ const oldestKey = insertionOrder.shift();
142
+ if (oldestKey !== undefined) {
143
+ store.delete(oldestKey);
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Remove key from insertion order tracking.
149
+ *
150
+ * @param key - Key to remove from order tracking
151
+ */
152
+ function removeFromOrder(key) {
153
+ const index = insertionOrder.indexOf(key);
154
+ if (index !== -1) {
155
+ insertionOrder.splice(index, 1);
156
+ }
157
+ }
158
+ const cache = {
159
+ get(key) {
160
+ const entry = store.get(key);
161
+ if (!entry)
162
+ return undefined;
163
+ if (isExpired(entry)) {
164
+ store.delete(key);
165
+ removeFromOrder(key);
166
+ return undefined;
167
+ }
168
+ return entry.value;
169
+ },
170
+ set(key, value) {
171
+ // If key exists, remove from order first
172
+ if (store.has(key)) {
173
+ removeFromOrder(key);
174
+ }
175
+ else {
176
+ // Evict if needed before adding new entry
177
+ evictIfNeeded();
178
+ }
179
+ // eslint-disable-next-line workspace/no-unsafe-builtin-methods -- Date.now() is needed for Jest fake timers compatibility
180
+ store.set(key, { value, timestamp: Date.now() });
181
+ insertionOrder.push(key);
182
+ },
183
+ has(key) {
184
+ const entry = store.get(key);
185
+ if (!entry)
186
+ return false;
187
+ if (isExpired(entry)) {
188
+ store.delete(key);
189
+ removeFromOrder(key);
190
+ return false;
191
+ }
192
+ return true;
193
+ },
194
+ delete(key) {
195
+ removeFromOrder(key);
196
+ return store.delete(key);
197
+ },
198
+ clear() {
199
+ store.clear();
200
+ insertionOrder.length = 0;
201
+ },
202
+ size() {
203
+ return store.size;
204
+ },
205
+ keys() {
206
+ return [...insertionOrder];
207
+ },
208
+ };
209
+ // Register cache for global operations
210
+ cacheRegistry.add(cache);
211
+ return freeze(cache);
212
+ }
213
+
214
+ /**
215
+ * Safe copies of Array built-in static methods.
216
+ *
217
+ * These references are captured at module initialization time to protect against
218
+ * prototype pollution attacks. Import only what you need for tree-shaking.
219
+ *
220
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/array
221
+ */
222
+ // Capture references at module initialization time
223
+ const _Array = globalThis.Array;
224
+ /**
225
+ * (Safe copy) Determines whether the passed value is an Array.
226
+ */
227
+ const isArray = _Array.isArray;
228
+
229
+ /**
230
+ * Safe copies of Console built-in methods.
231
+ *
232
+ * These references are captured at module initialization time to protect against
233
+ * prototype pollution attacks. Import only what you need for tree-shaking.
234
+ *
235
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/console
236
+ */
237
+ // Capture references at module initialization time
238
+ const _console = globalThis.console;
239
+ /**
240
+ * (Safe copy) Outputs a message to the console.
241
+ */
242
+ const log = _console.log.bind(_console);
243
+ /**
244
+ * (Safe copy) Outputs a warning message to the console.
245
+ */
246
+ const warn = _console.warn.bind(_console);
247
+ /**
248
+ * (Safe copy) Outputs an error message to the console.
249
+ */
250
+ const error = _console.error.bind(_console);
251
+ /**
252
+ * (Safe copy) Outputs an informational message to the console.
253
+ */
254
+ const info = _console.info.bind(_console);
255
+ /**
256
+ * (Safe copy) Outputs a debug message to the console.
257
+ */
258
+ const debug = _console.debug.bind(_console);
259
+ /**
260
+ * (Safe copy) Outputs a stack trace to the console.
261
+ */
262
+ _console.trace.bind(_console);
263
+ /**
264
+ * (Safe copy) Displays an interactive listing of the properties of a specified object.
265
+ */
266
+ _console.dir.bind(_console);
267
+ /**
268
+ * (Safe copy) Displays tabular data as a table.
269
+ */
270
+ _console.table.bind(_console);
271
+ /**
272
+ * (Safe copy) Writes an error message to the console if the assertion is false.
273
+ */
274
+ _console.assert.bind(_console);
275
+ /**
276
+ * (Safe copy) Clears the console.
277
+ */
278
+ _console.clear.bind(_console);
279
+ /**
280
+ * (Safe copy) Logs the number of times that this particular call to count() has been called.
281
+ */
282
+ _console.count.bind(_console);
283
+ /**
284
+ * (Safe copy) Resets the counter used with console.count().
285
+ */
286
+ _console.countReset.bind(_console);
287
+ /**
288
+ * (Safe copy) Creates a new inline group in the console.
289
+ */
290
+ _console.group.bind(_console);
291
+ /**
292
+ * (Safe copy) Creates a new inline group in the console that is initially collapsed.
293
+ */
294
+ _console.groupCollapsed.bind(_console);
295
+ /**
296
+ * (Safe copy) Exits the current inline group.
297
+ */
298
+ _console.groupEnd.bind(_console);
299
+ /**
300
+ * (Safe copy) Starts a timer with a name specified as an input parameter.
301
+ */
302
+ _console.time.bind(_console);
303
+ /**
304
+ * (Safe copy) Stops a timer that was previously started.
305
+ */
306
+ _console.timeEnd.bind(_console);
307
+ /**
308
+ * (Safe copy) Logs the current value of a timer that was previously started.
309
+ */
310
+ _console.timeLog.bind(_console);
311
+
312
+ /**
313
+ * Safe copies of JSON built-in methods.
314
+ *
315
+ * These references are captured at module initialization time to protect against
316
+ * prototype pollution attacks. Import only what you need for tree-shaking.
317
+ *
318
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/json
319
+ */
320
+ // Capture references at module initialization time
321
+ const _JSON = globalThis.JSON;
322
+ /**
323
+ * (Safe copy) Converts a JavaScript Object Notation (JSON) string into an object.
324
+ */
325
+ const parse = _JSON.parse;
326
+ /**
327
+ * (Safe copy) Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
328
+ */
329
+ const stringify = _JSON.stringify;
330
+
331
+ const registeredClasses = [];
332
+
333
+ /**
334
+ * Returns the data type of the target.
335
+ * Uses native `typeof` operator, however, makes distinction between `null`, `array`, and `object`.
336
+ * Also, when classes are registered via `registerClass`, it checks if objects are instance of any known registered class.
337
+ *
338
+ * @param target - The target to get the data type of.
339
+ * @returns The data type of the target.
340
+ */
341
+ const getType = (target) => {
342
+ if (target === null)
343
+ return 'null';
344
+ const nativeDataType = typeof target;
345
+ if (nativeDataType === 'object') {
346
+ if (isArray(target))
347
+ return 'array';
348
+ for (const registeredClass of registeredClasses) {
349
+ if (target instanceof registeredClass)
350
+ return registeredClass.name;
351
+ }
352
+ }
353
+ return nativeDataType;
354
+ };
355
+
356
+ /**
357
+ * Safe copies of Error built-ins via factory functions.
358
+ *
359
+ * Since constructors cannot be safely captured via Object.assign, this module
360
+ * provides factory functions that use Reflect.construct internally.
361
+ *
362
+ * These references are captured at module initialization time to protect against
363
+ * prototype pollution attacks. Import only what you need for tree-shaking.
364
+ *
365
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/error
366
+ */
367
+ // Capture references at module initialization time
368
+ const _Error = globalThis.Error;
369
+ const _Reflect = globalThis.Reflect;
370
+ /**
371
+ * (Safe copy) Creates a new Error using the captured Error constructor.
372
+ * Use this instead of `new Error()`.
373
+ *
374
+ * @param message - Optional error message.
375
+ * @param options - Optional error options.
376
+ * @returns A new Error instance.
377
+ */
378
+ const createError = (message, options) => _Reflect.construct(_Error, [message, options]);
379
+
380
+ /**
381
+ * Safe copies of Math built-in methods.
382
+ *
383
+ * These references are captured at module initialization time to protect against
384
+ * prototype pollution attacks. Import only what you need for tree-shaking.
385
+ *
386
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
387
+ */
388
+ // Capture references at module initialization time
389
+ const _Math = globalThis.Math;
390
+ /**
391
+ * (Safe copy) Returns the smaller of zero or more numbers.
392
+ */
393
+ const min = _Math.min;
394
+
395
+ /* eslint-disable @typescript-eslint/no-explicit-any */
396
+ /**
397
+ * Creates a wrapper function that only executes the wrapped function if the condition function returns true.
398
+ *
399
+ * @param func - The function to be conditionally executed.
400
+ * @param conditionFunc - A function that returns a boolean, determining if `func` should be executed.
401
+ * @returns A wrapped version of `func` that executes conditionally.
402
+ */
403
+ function createConditionalExecutionFunction(func, conditionFunc) {
404
+ return function (...args) {
405
+ if (conditionFunc()) {
406
+ return func(...args);
407
+ }
408
+ };
409
+ }
410
+
411
+ /* eslint-disable @typescript-eslint/no-explicit-any */
412
+ /**
413
+ * Creates a wrapper function that silently ignores any errors thrown by the wrapped void function.
414
+ * This function is specifically for wrapping functions that do not return a value (void functions).
415
+ * Exceptions are swallowed without any logging or handling.
416
+ *
417
+ * @param func - The void function to be wrapped.
418
+ * @returns A wrapped version of the input function that ignores errors.
419
+ */
420
+ function createErrorIgnoringFunction(func) {
421
+ return function (...args) {
422
+ try {
423
+ func(...args);
424
+ }
425
+ catch {
426
+ // Deliberately swallowing/ignoring the exception
427
+ }
428
+ };
429
+ }
430
+
431
+ /* eslint-disable @typescript-eslint/no-unused-vars */
432
+ /**
433
+ * A no-operation function (noop) that does nothing regardless of the arguments passed.
434
+ * It is designed to be as permissive as possible in its typing without using the `Function` keyword.
435
+ *
436
+ * @param args - Any arguments passed to the function (ignored)
437
+ */
438
+ const noop = (...args) => {
439
+ // Intentionally does nothing
440
+ };
441
+
442
+ const logLevels = ['none', 'error', 'warn', 'log', 'info', 'debug'];
443
+ const priority = {
444
+ error: 4,
445
+ warn: 3,
446
+ log: 2,
447
+ info: 1,
448
+ debug: 0,
449
+ };
450
+ /**
451
+ * Validates whether a given string is a valid log level.
452
+ *
453
+ * @param level - The log level to validate
454
+ * @returns True if the level is valid, false otherwise
455
+ */
456
+ function isValidLogLevel(level) {
457
+ return logLevels.includes(level);
458
+ }
459
+ /**
460
+ * Creates a log level configuration manager for controlling logging behavior.
461
+ * Provides methods to get, set, and evaluate log levels based on priority.
462
+ *
463
+ * @param level - The initial log level (defaults to 'error')
464
+ * @returns A configuration object with log level management methods
465
+ * @throws {Error} When the provided level is not a valid log level
466
+ */
467
+ function createLogLevelConfig(level = 'error') {
468
+ if (!isValidLogLevel(level)) {
469
+ throw createError('Cannot create log level configuration with a valid default log level');
470
+ }
471
+ const state = { level };
472
+ const getLogLevel = () => state.level;
473
+ const setLogLevel = (level) => {
474
+ if (!isValidLogLevel(level)) {
475
+ throw createError(`Cannot set value '${level}' level. Expected levels are ${logLevels}.`);
476
+ }
477
+ state.level = level;
478
+ };
479
+ const shouldLog = (level) => {
480
+ if (state.level === 'none' || level === 'none' || !isValidLogLevel(level)) {
481
+ return false;
482
+ }
483
+ return priority[level] >= priority[state.level];
484
+ };
485
+ return freeze({
486
+ getLogLevel,
487
+ setLogLevel,
488
+ shouldLog,
489
+ });
490
+ }
491
+
492
+ /**
493
+ * Creates a logger instance with configurable log level filtering.
494
+ * Each log function is wrapped to respect the current log level setting.
495
+ *
496
+ * @param error - Function to handle error-level logs (required)
497
+ * @param warn - Function to handle warning-level logs (optional, defaults to noop)
498
+ * @param log - Function to handle standard logs (optional, defaults to noop)
499
+ * @param info - Function to handle info-level logs (optional, defaults to noop)
500
+ * @param debug - Function to handle debug-level logs (optional, defaults to noop)
501
+ * @returns A frozen logger object with log methods and level control
502
+ * @throws {ErrorLevelFn} When any provided log function is invalid
503
+ */
504
+ function createLogger(error, warn = noop, log = noop, info = noop, debug = noop) {
505
+ if (notValidLogFn(error)) {
506
+ throw createError(notFnMsg('error'));
507
+ }
508
+ if (notValidLogFn(warn)) {
509
+ throw createError(notFnMsg('warn'));
510
+ }
511
+ if (notValidLogFn(log)) {
512
+ throw createError(notFnMsg('log'));
513
+ }
514
+ if (notValidLogFn(info)) {
515
+ throw createError(notFnMsg('info'));
516
+ }
517
+ if (notValidLogFn(debug)) {
518
+ throw createError(notFnMsg('debug'));
519
+ }
520
+ const { setLogLevel, getLogLevel, shouldLog } = createLogLevelConfig();
521
+ const wrapLogFn = (fn, level) => {
522
+ if (fn === noop)
523
+ return fn;
524
+ const condition = () => shouldLog(level);
525
+ return createConditionalExecutionFunction(createErrorIgnoringFunction(fn), condition);
526
+ };
527
+ return freeze({
528
+ error: wrapLogFn(error, 'error'),
529
+ warn: wrapLogFn(warn, 'warn'),
530
+ log: wrapLogFn(log, 'log'),
531
+ info: wrapLogFn(info, 'info'),
532
+ debug: wrapLogFn(debug, 'debug'),
533
+ setLogLevel,
534
+ getLogLevel,
535
+ });
536
+ }
537
+ /**
538
+ * Validates whether a given value is a valid log function.
539
+ *
540
+ * @param fn - The value to validate
541
+ * @returns True if the value is not a function (invalid), false if it is valid
542
+ */
543
+ function notValidLogFn(fn) {
544
+ return getType(fn) !== 'function' && fn !== noop;
545
+ }
546
+ /**
547
+ * Generates an error message for invalid log function parameters.
548
+ *
549
+ * @param label - The name of the log function that failed validation
550
+ * @returns A formatted error message string
551
+ */
552
+ function notFnMsg(label) {
553
+ return `Cannot create a logger when ${label} is not a function`;
554
+ }
555
+
556
+ createLogger(error, warn, log, info, debug);
557
+
558
+ /**
559
+ * Global log level registry.
560
+ * Tracks all created scoped loggers to allow global log level changes.
561
+ */
562
+ const loggerRegistry = createSet();
563
+ /** Redacted placeholder for sensitive values */
564
+ const REDACTED = '[REDACTED]';
565
+ /**
566
+ * Patterns that indicate a sensitive key name.
567
+ * Keys containing these patterns will have their values sanitized.
568
+ */
569
+ const SENSITIVE_KEY_PATTERNS = [
570
+ /token/i,
571
+ /key/i,
572
+ /password/i,
573
+ /secret/i,
574
+ /credential/i,
575
+ /auth/i,
576
+ /bearer/i,
577
+ /api[_-]?key/i,
578
+ /private/i,
579
+ /passphrase/i,
580
+ ];
581
+ /**
582
+ * Checks if a key name indicates sensitive data.
583
+ *
584
+ * @param key - Key name to check
585
+ * @returns True if the key indicates sensitive data
586
+ */
587
+ function isSensitiveKey(key) {
588
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
589
+ }
590
+ /**
591
+ * Sanitizes an object by replacing sensitive values with REDACTED.
592
+ * This function recursively processes nested objects and arrays.
593
+ *
594
+ * @param obj - Object to sanitize
595
+ * @returns New object with sensitive values redacted
596
+ */
597
+ function sanitize(obj) {
598
+ if (obj === null || obj === undefined) {
599
+ return obj;
600
+ }
601
+ if (isArray(obj)) {
602
+ return obj.map((item) => sanitize(item));
603
+ }
604
+ if (typeof obj === 'object') {
605
+ const result = {};
606
+ for (const [key, value] of entries(obj)) {
607
+ if (isSensitiveKey(key)) {
608
+ result[key] = REDACTED;
609
+ }
610
+ else if (typeof value === 'object' && value !== null) {
611
+ result[key] = sanitize(value);
612
+ }
613
+ else {
614
+ result[key] = value;
615
+ }
616
+ }
617
+ return result;
618
+ }
619
+ return obj;
620
+ }
621
+ /**
622
+ * Formats a log message with optional metadata.
623
+ *
624
+ * @param namespace - Logger namespace prefix
625
+ * @param message - Log message
626
+ * @param meta - Optional metadata object
627
+ * @returns Formatted log string
628
+ */
629
+ function formatMessage(namespace, message, meta) {
630
+ const prefix = `[${namespace}]`;
631
+ if (meta && keys(meta).length > 0) {
632
+ const sanitizedMeta = sanitize(meta);
633
+ return `${prefix} ${message} ${stringify(sanitizedMeta)}`;
634
+ }
635
+ return `${prefix} ${message}`;
636
+ }
637
+ /**
638
+ * Creates a scoped logger with namespace prefix and optional secret sanitization.
639
+ * All log messages will be prefixed with [namespace] and sensitive metadata
640
+ * values will be automatically redacted.
641
+ *
642
+ * @param namespace - Logger namespace (e.g., 'project-scope', 'analyze')
643
+ * @param options - Logger configuration options
644
+ * @returns A configured scoped logger instance
645
+ *
646
+ * @example
647
+ * ```typescript
648
+ * const logger = createScopedLogger('project-scope')
649
+ * logger.setLogLevel('debug')
650
+ *
651
+ * // Basic logging
652
+ * logger.info('Starting analysis', { path: './project' })
653
+ *
654
+ * // Sensitive data is automatically redacted
655
+ * logger.debug('Config loaded', { apiKey: 'secret123' })
656
+ * // Output: [project-scope] Config loaded {"apiKey":"[REDACTED]"}
657
+ * ```
658
+ */
659
+ function createScopedLogger(namespace, options = {}) {
660
+ const { level = 'error', sanitizeSecrets = true } = options;
661
+ // Create wrapper functions that add namespace prefix and sanitization
662
+ const createLogFn = (baseFn) => (message, meta) => {
663
+ const processedMeta = sanitizeSecrets && meta ? sanitize(meta) : meta;
664
+ baseFn(formatMessage(namespace, message, processedMeta));
665
+ };
666
+ // Create base logger with wrapped functions
667
+ const baseLogger = createLogger(createLogFn(error), createLogFn(warn), createLogFn(log), createLogFn(info), createLogFn(debug));
668
+ // Set initial log level (use global override if set)
669
+ baseLogger.setLogLevel(level);
670
+ const scopedLogger = freeze({
671
+ error: (message, meta) => baseLogger.error(message, meta),
672
+ warn: (message, meta) => baseLogger.warn(message, meta),
673
+ log: (message, meta) => baseLogger.log(message, meta),
674
+ info: (message, meta) => baseLogger.info(message, meta),
675
+ debug: (message, meta) => baseLogger.debug(message, meta),
676
+ setLogLevel: baseLogger.setLogLevel,
677
+ getLogLevel: baseLogger.getLogLevel,
678
+ });
679
+ // Register logger for global level management
680
+ loggerRegistry.add(scopedLogger);
681
+ return scopedLogger;
682
+ }
683
+ /**
684
+ * Default logger instance for the project-scope library.
685
+ * Use this for general logging within the library.
686
+ *
687
+ * @example
688
+ * ```typescript
689
+ * import { logger } from '@hyperfrontend/project-scope/core'
690
+ *
691
+ * logger.setLogLevel('debug')
692
+ * logger.debug('Analyzing project', { path: './src' })
693
+ * ```
694
+ */
695
+ createScopedLogger('project-scope');
696
+
697
+ createScopedLogger('project-scope:fs');
698
+ /**
699
+ * Create a file system error with code and context.
700
+ *
701
+ * @param message - The error message describing what went wrong
702
+ * @param code - The category code for this type of filesystem failure
703
+ * @param context - Additional context including path, operation, and cause
704
+ * @returns A configured Error object with code and context properties
705
+ */
706
+ function createFileSystemError(message, code, context) {
707
+ const error = createError(message);
708
+ defineProperties(error, {
709
+ code: { value: code, enumerable: true },
710
+ context: { value: context, enumerable: true },
711
+ });
712
+ return error;
713
+ }
714
+ /**
715
+ * Read file if exists, return null otherwise.
716
+ *
717
+ * @param filePath - Path to file
718
+ * @param encoding - File encoding (default: utf-8)
719
+ * @returns File contents or null if file doesn't exist
720
+ */
721
+ function readFileIfExists(filePath, encoding = 'utf-8') {
722
+ if (!existsSync(filePath)) {
723
+ return null;
724
+ }
725
+ try {
726
+ return readFileSync(filePath, { encoding });
727
+ }
728
+ catch {
729
+ return null;
730
+ }
731
+ }
732
+
733
+ createScopedLogger('project-scope:fs:write');
734
+
735
+ /**
736
+ * Get file stats with error handling.
737
+ *
738
+ * @param filePath - Path to file
739
+ * @param followSymlinks - Whether to follow symlinks (default: true)
740
+ * @returns File stats or null if path doesn't exist
741
+ */
742
+ function getFileStat(filePath, followSymlinks = true) {
743
+ if (!existsSync(filePath)) {
744
+ return null;
745
+ }
746
+ try {
747
+ const stat = followSymlinks ? statSync(filePath) : lstatSync(filePath);
748
+ return {
749
+ isFile: stat.isFile(),
750
+ isDirectory: stat.isDirectory(),
751
+ isSymlink: stat.isSymbolicLink(),
752
+ size: stat.size,
753
+ created: stat.birthtime,
754
+ modified: stat.mtime,
755
+ accessed: stat.atime,
756
+ mode: stat.mode,
757
+ };
758
+ }
759
+ catch {
760
+ return null;
761
+ }
762
+ }
763
+ /**
764
+ * Check if path is a directory.
765
+ *
766
+ * @param dirPath - Path to check
767
+ * @returns True if path is a directory
768
+ */
769
+ function isDirectory(dirPath) {
770
+ const stats = getFileStat(dirPath);
771
+ return stats?.isDirectory ?? false;
772
+ }
773
+ /**
774
+ * Check if path exists.
775
+ *
776
+ * @param filePath - Path to check
777
+ * @returns True if path exists
778
+ */
779
+ function exists(filePath) {
780
+ return existsSync(filePath);
781
+ }
782
+
783
+ const fsDirLogger = createScopedLogger('project-scope:fs:dir');
784
+ /**
785
+ * List immediate contents of a directory.
786
+ *
787
+ * @param dirPath - Absolute or relative path to the directory
788
+ * @returns Array of entries with metadata for each file/directory
789
+ * @throws {Error} If directory doesn't exist or isn't a directory
790
+ *
791
+ * @example
792
+ * ```typescript
793
+ * import { readDirectory } from '@hyperfrontend/project-scope'
794
+ *
795
+ * const entries = readDirectory('./src')
796
+ * for (const entry of entries) {
797
+ * console.log(entry.name, entry.isFile ? 'file' : 'directory')
798
+ * }
799
+ * ```
800
+ */
801
+ function readDirectory(dirPath) {
802
+ fsDirLogger.debug('Reading directory', { path: dirPath });
803
+ if (!existsSync(dirPath)) {
804
+ fsDirLogger.debug('Directory not found', { path: dirPath });
805
+ throw createFileSystemError(`Directory not found: ${dirPath}`, 'FS_NOT_FOUND', { path: dirPath, operation: 'readdir' });
806
+ }
807
+ if (!isDirectory(dirPath)) {
808
+ fsDirLogger.debug('Path is not a directory', { path: dirPath });
809
+ throw createFileSystemError(`Not a directory: ${dirPath}`, 'FS_NOT_A_DIRECTORY', { path: dirPath, operation: 'readdir' });
810
+ }
811
+ try {
812
+ const entries = readdirSync(dirPath, { withFileTypes: true });
813
+ fsDirLogger.debug('Directory read complete', { path: dirPath, entryCount: entries.length });
814
+ return entries.map((entry) => ({
815
+ name: entry.name,
816
+ path: join$1(dirPath, entry.name),
817
+ isFile: entry.isFile(),
818
+ isDirectory: entry.isDirectory(),
819
+ isSymlink: entry.isSymbolicLink(),
820
+ }));
821
+ }
822
+ catch (error) {
823
+ fsDirLogger.warn('Failed to read directory', { path: dirPath, error: error instanceof Error ? error.message : String(error) });
824
+ throw createFileSystemError(`Failed to read directory: ${dirPath}`, 'FS_READ_ERROR', {
825
+ path: dirPath,
826
+ operation: 'readdir',
827
+ cause: error,
828
+ });
829
+ }
830
+ }
831
+
832
+ /**
833
+ * Join path segments.
834
+ * Uses platform-specific separators (e.g., / or \).
835
+ *
836
+ * @param paths - Path segments to join
837
+ * @returns Joined path
838
+ */
839
+ function join(...paths) {
840
+ return join$1(...paths);
841
+ }
842
+
843
+ createScopedLogger('project-scope:fs:traversal');
844
+
845
+ const packageLogger = createScopedLogger('project-scope:project:package');
846
+ /**
847
+ * Verifies that a value is an object with only string values,
848
+ * used for validating dependency maps and script definitions.
849
+ *
850
+ * @param value - Value to check
851
+ * @returns True if value is a record of strings
852
+ */
853
+ function isStringRecord(value) {
854
+ if (typeof value !== 'object' || value === null)
855
+ return false;
856
+ return values(value).every((v) => typeof v === 'string');
857
+ }
858
+ /**
859
+ * Extracts and normalizes the workspaces field from package.json,
860
+ * supporting both array format and object with packages array.
861
+ *
862
+ * @param value - Raw workspaces value from package.json
863
+ * @returns Normalized workspace patterns or undefined if invalid
864
+ */
865
+ function parseWorkspaces(value) {
866
+ if (isArray(value) && value.every((v) => typeof v === 'string')) {
867
+ return value;
868
+ }
869
+ if (typeof value === 'object' && value !== null) {
870
+ const obj = value;
871
+ if (isArray(obj['packages'])) {
872
+ return { packages: obj['packages'] };
873
+ }
874
+ }
875
+ return undefined;
876
+ }
877
+ /**
878
+ * Validate and normalize package.json data.
879
+ *
880
+ * @param data - Raw parsed data
881
+ * @returns Validated package.json
882
+ */
883
+ function validatePackageJson(data) {
884
+ if (typeof data !== 'object' || data === null) {
885
+ throw createError('package.json must be an object');
886
+ }
887
+ const pkg = data;
888
+ return {
889
+ name: typeof pkg['name'] === 'string' ? pkg['name'] : undefined,
890
+ version: typeof pkg['version'] === 'string' ? pkg['version'] : undefined,
891
+ description: typeof pkg['description'] === 'string' ? pkg['description'] : undefined,
892
+ main: typeof pkg['main'] === 'string' ? pkg['main'] : undefined,
893
+ module: typeof pkg['module'] === 'string' ? pkg['module'] : undefined,
894
+ browser: typeof pkg['browser'] === 'string' ? pkg['browser'] : undefined,
895
+ types: typeof pkg['types'] === 'string' ? pkg['types'] : undefined,
896
+ bin: typeof pkg['bin'] === 'string' || isStringRecord(pkg['bin']) ? pkg['bin'] : undefined,
897
+ scripts: isStringRecord(pkg['scripts']) ? pkg['scripts'] : undefined,
898
+ dependencies: isStringRecord(pkg['dependencies']) ? pkg['dependencies'] : undefined,
899
+ devDependencies: isStringRecord(pkg['devDependencies']) ? pkg['devDependencies'] : undefined,
900
+ peerDependencies: isStringRecord(pkg['peerDependencies']) ? pkg['peerDependencies'] : undefined,
901
+ optionalDependencies: isStringRecord(pkg['optionalDependencies']) ? pkg['optionalDependencies'] : undefined,
902
+ workspaces: parseWorkspaces(pkg['workspaces']),
903
+ exports: typeof pkg['exports'] === 'object' ? pkg['exports'] : undefined,
904
+ engines: isStringRecord(pkg['engines']) ? pkg['engines'] : undefined,
905
+ ...pkg,
906
+ };
907
+ }
908
+ /**
909
+ * Attempts to read and parse package.json if it exists,
910
+ * returning null on missing file or parse failure.
911
+ *
912
+ * @param projectPath - Project directory path or path to package.json
913
+ * @returns Parsed package.json or null if not found
914
+ */
915
+ function readPackageJsonIfExists(projectPath) {
916
+ const packageJsonPath = projectPath.endsWith('package.json') ? projectPath : join$1(projectPath, 'package.json');
917
+ const content = readFileIfExists(packageJsonPath);
918
+ if (!content) {
919
+ packageLogger.debug('Package.json not found', { path: packageJsonPath });
920
+ return null;
921
+ }
922
+ try {
923
+ const validated = validatePackageJson(parse(content));
924
+ packageLogger.debug('Package.json loaded', { path: packageJsonPath, name: validated.name });
925
+ return validated;
926
+ }
927
+ catch {
928
+ packageLogger.debug('Failed to parse package.json, returning null', { path: packageJsonPath });
929
+ return null;
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Get combined dependencies from package.json.
935
+ * Merges dependencies, devDependencies, peerDependencies, and optionalDependencies.
936
+ *
937
+ * @param packageJson - The package.json object to extract dependencies from
938
+ * @returns Combined dependencies as a single record
939
+ */
940
+ function collectAllDependencies(packageJson) {
941
+ return {
942
+ ...packageJson?.dependencies,
943
+ ...packageJson?.devDependencies,
944
+ ...packageJson?.peerDependencies,
945
+ ...packageJson?.optionalDependencies,
946
+ };
947
+ }
948
+ /**
949
+ * Extract clean version from dependency version string.
950
+ * Removes semver prefixes like ^, ~, >=, etc.
951
+ * Uses character-by-character parsing to avoid ReDoS vulnerabilities.
952
+ *
953
+ * @param versionString - The version string with optional prefix characters
954
+ * @returns The cleaned version string without prefix characters
955
+ */
956
+ function parseVersionString(versionString) {
957
+ if (versionString === undefined || versionString === null)
958
+ return undefined;
959
+ // Manual parsing instead of regex to avoid ReDoS
960
+ let start = 0;
961
+ while (start < versionString.length) {
962
+ const char = versionString[start];
963
+ if (char !== '^' && char !== '~' && char !== '>' && char !== '=' && char !== '<') {
964
+ break;
965
+ }
966
+ start++;
967
+ }
968
+ return versionString.slice(start);
969
+ }
970
+ /**
971
+ * Find first matching config file in project.
972
+ * Note: Name avoids similarity to fs.readFile/fs.readFileSync.
973
+ *
974
+ * @param projectPath - The project directory path
975
+ * @param patterns - Array of config file patterns to search for
976
+ * @returns The first matching config file path or undefined
977
+ */
978
+ function locateConfigFile(projectPath, patterns) {
979
+ for (const pattern of patterns) {
980
+ const fullPath = join(projectPath, pattern);
981
+ if (exists(fullPath)) {
982
+ return pattern;
983
+ }
984
+ }
985
+ return undefined;
986
+ }
987
+ /**
988
+ * Find scripts containing a specific command.
989
+ *
990
+ * @param scripts - The scripts object from package.json
991
+ * @param command - The command string to search for
992
+ * @returns Array of script names that contain the command
993
+ */
994
+ function filterScriptsByCommand(scripts, command) {
995
+ if (!scripts)
996
+ return [];
997
+ return entries(scripts)
998
+ .filter(([, script]) => script.includes(command))
999
+ .map(([name]) => name);
1000
+ }
1001
+
1002
+ /**
1003
+ * Detect Express in project.
1004
+ *
1005
+ * @param projectPath - Project directory path
1006
+ * @param packageJson - Optional pre-loaded package.json
1007
+ * @returns Detection result or null if not detected
1008
+ */
1009
+ function expressDetector(projectPath, packageJson) {
1010
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1011
+ const sources = [];
1012
+ let confidence = 0;
1013
+ let version;
1014
+ const deps = collectAllDependencies(pkg);
1015
+ if (deps['express']) {
1016
+ confidence += 80;
1017
+ version = parseVersionString(deps['express']);
1018
+ sources.push({ type: 'package.json', field: 'dependencies.express' });
1019
+ }
1020
+ // @types/express (indicates usage)
1021
+ if (deps['@types/express']) {
1022
+ confidence += 10;
1023
+ sources.push({ type: 'package.json', field: 'dependencies.@types/express' });
1024
+ }
1025
+ const expressMiddleware = keys(deps).filter((d) => d.includes('express-') || d === 'body-parser' || d === 'cors' || d === 'helmet' || d === 'morgan');
1026
+ if (expressMiddleware.length > 0) {
1027
+ confidence += 10;
1028
+ sources.push({ type: 'package.json', field: 'dependencies (express middleware)' });
1029
+ }
1030
+ if (confidence === 0) {
1031
+ return null;
1032
+ }
1033
+ return {
1034
+ id: 'express',
1035
+ name: 'Express',
1036
+ version,
1037
+ confidence: min(confidence, 100),
1038
+ detectedFrom: sources,
1039
+ };
1040
+ }
1041
+
1042
+ /**
1043
+ * Detect NestJS in project.
1044
+ *
1045
+ * @param projectPath - Project directory path
1046
+ * @param packageJson - Optional pre-loaded package.json
1047
+ * @returns Detection result or null if not detected
1048
+ */
1049
+ function nestDetector(projectPath, packageJson) {
1050
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1051
+ const sources = [];
1052
+ let confidence = 0;
1053
+ let version;
1054
+ let configPath;
1055
+ const deps = collectAllDependencies(pkg);
1056
+ // @nestjs/core package
1057
+ if (deps['@nestjs/core']) {
1058
+ confidence += 70;
1059
+ version = parseVersionString(deps['@nestjs/core']);
1060
+ sources.push({ type: 'package.json', field: 'dependencies.@nestjs/core' });
1061
+ }
1062
+ // @nestjs/common
1063
+ if (deps['@nestjs/common']) {
1064
+ confidence += 15;
1065
+ sources.push({ type: 'package.json', field: 'dependencies.@nestjs/common' });
1066
+ }
1067
+ if (exists(join$1(projectPath, 'nest-cli.json'))) {
1068
+ confidence += 15;
1069
+ configPath = 'nest-cli.json';
1070
+ sources.push({ type: 'config-file', path: 'nest-cli.json' });
1071
+ }
1072
+ const nestPackages = keys(deps).filter((d) => d.startsWith('@nestjs/'));
1073
+ if (nestPackages.length > 2) {
1074
+ confidence += 5;
1075
+ sources.push({ type: 'package.json', field: 'dependencies (@nestjs packages)' });
1076
+ }
1077
+ if (confidence === 0) {
1078
+ return null;
1079
+ }
1080
+ return {
1081
+ id: 'nestjs',
1082
+ name: 'NestJS',
1083
+ version,
1084
+ configPath,
1085
+ confidence: min(confidence, 100),
1086
+ detectedFrom: sources,
1087
+ };
1088
+ }
1089
+
1090
+ /**
1091
+ * Detect Fastify in project.
1092
+ *
1093
+ * @param projectPath - Project directory path
1094
+ * @param packageJson - Optional pre-loaded package.json
1095
+ * @returns Detection result or null if not detected
1096
+ */
1097
+ function fastifyDetector(projectPath, packageJson) {
1098
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1099
+ const sources = [];
1100
+ let confidence = 0;
1101
+ let version;
1102
+ const deps = collectAllDependencies(pkg);
1103
+ if (deps['fastify']) {
1104
+ confidence += 80;
1105
+ version = parseVersionString(deps['fastify']);
1106
+ sources.push({ type: 'package.json', field: 'dependencies.fastify' });
1107
+ }
1108
+ const fastifyPlugins = keys(deps).filter((d) => d.startsWith('@fastify/') || d.startsWith('fastify-'));
1109
+ if (fastifyPlugins.length > 0) {
1110
+ confidence += 15;
1111
+ sources.push({ type: 'package.json', field: 'dependencies (fastify plugins)' });
1112
+ }
1113
+ // @types/fastify (older versions)
1114
+ if (deps['@types/fastify']) {
1115
+ confidence += 5;
1116
+ sources.push({ type: 'package.json', field: 'dependencies.@types/fastify' });
1117
+ }
1118
+ if (confidence === 0) {
1119
+ return null;
1120
+ }
1121
+ return {
1122
+ id: 'fastify',
1123
+ name: 'Fastify',
1124
+ version,
1125
+ confidence: min(confidence, 100),
1126
+ detectedFrom: sources,
1127
+ };
1128
+ }
1129
+
1130
+ /**
1131
+ * Detect Koa in project.
1132
+ *
1133
+ * @param projectPath - Project directory path
1134
+ * @param packageJson - Optional pre-loaded package.json
1135
+ * @returns Detection result or null if not detected
1136
+ */
1137
+ function koaDetector(projectPath, packageJson) {
1138
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1139
+ const sources = [];
1140
+ let confidence = 0;
1141
+ let version;
1142
+ const deps = collectAllDependencies(pkg);
1143
+ if (deps['koa']) {
1144
+ confidence += 80;
1145
+ version = parseVersionString(deps['koa']);
1146
+ sources.push({ type: 'package.json', field: 'dependencies.koa' });
1147
+ }
1148
+ // @types/koa
1149
+ if (deps['@types/koa']) {
1150
+ confidence += 10;
1151
+ sources.push({ type: 'package.json', field: 'dependencies.@types/koa' });
1152
+ }
1153
+ const koaMiddleware = keys(deps).filter((d) => d.startsWith('koa-') || d.startsWith('@koa/'));
1154
+ if (koaMiddleware.length > 0) {
1155
+ confidence += 10;
1156
+ sources.push({ type: 'package.json', field: 'dependencies (koa middleware)' });
1157
+ }
1158
+ if (confidence === 0) {
1159
+ return null;
1160
+ }
1161
+ return {
1162
+ id: 'koa',
1163
+ name: 'Koa',
1164
+ version,
1165
+ confidence: min(confidence, 100),
1166
+ detectedFrom: sources,
1167
+ };
1168
+ }
1169
+
1170
+ /**
1171
+ * Detect Hono in project.
1172
+ *
1173
+ * @param projectPath - Project directory path
1174
+ * @param packageJson - Optional pre-loaded package.json
1175
+ * @returns Detection result or null if not detected
1176
+ */
1177
+ function honoDetector(projectPath, packageJson) {
1178
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1179
+ const sources = [];
1180
+ let confidence = 0;
1181
+ let version;
1182
+ const deps = collectAllDependencies(pkg);
1183
+ if (deps['hono']) {
1184
+ confidence += 85;
1185
+ version = parseVersionString(deps['hono']);
1186
+ sources.push({ type: 'package.json', field: 'dependencies.hono' });
1187
+ }
1188
+ const honoAdapters = keys(deps).filter((d) => d.startsWith('@hono/'));
1189
+ if (honoAdapters.length > 0) {
1190
+ confidence += 15;
1191
+ sources.push({ type: 'package.json', field: 'dependencies (@hono adapters)' });
1192
+ }
1193
+ if (confidence === 0) {
1194
+ return null;
1195
+ }
1196
+ return {
1197
+ id: 'hono',
1198
+ name: 'Hono',
1199
+ version,
1200
+ confidence: min(confidence, 100),
1201
+ detectedFrom: sources,
1202
+ };
1203
+ }
1204
+
1205
+ /** All backend framework detectors */
1206
+ const backendDetectors = [
1207
+ { id: 'express', name: 'Express', detect: expressDetector },
1208
+ { id: 'nestjs', name: 'NestJS', detect: nestDetector },
1209
+ { id: 'fastify', name: 'Fastify', detect: fastifyDetector },
1210
+ { id: 'koa', name: 'Koa', detect: koaDetector },
1211
+ { id: 'hono', name: 'Hono', detect: honoDetector },
1212
+ ];
1213
+
1214
+ /** Config patterns for Webpack */
1215
+ const WEBPACK_CONFIG_PATTERNS = [
1216
+ 'webpack.config.js',
1217
+ 'webpack.config.ts',
1218
+ 'webpack.config.cjs',
1219
+ 'webpack.config.mjs',
1220
+ 'webpack.config.babel.js',
1221
+ ];
1222
+ /**
1223
+ * Detect Webpack in project.
1224
+ *
1225
+ * @param projectPath - Project directory path
1226
+ * @param packageJson - Optional pre-loaded package.json
1227
+ * @returns Detection result or null if not detected
1228
+ */
1229
+ function webpackDetector(projectPath, packageJson) {
1230
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1231
+ const sources = [];
1232
+ let confidence = 0;
1233
+ let version;
1234
+ const deps = collectAllDependencies(pkg);
1235
+ if (deps['webpack']) {
1236
+ confidence += 50;
1237
+ version = parseVersionString(deps['webpack']);
1238
+ sources.push({ type: 'package.json', field: 'dependencies.webpack' });
1239
+ }
1240
+ const configPath = locateConfigFile(projectPath, WEBPACK_CONFIG_PATTERNS);
1241
+ if (configPath) {
1242
+ confidence += 40;
1243
+ sources.push({ type: 'config-file', path: configPath });
1244
+ }
1245
+ if (deps['webpack-cli']) {
1246
+ confidence += 10;
1247
+ sources.push({ type: 'package.json', field: 'dependencies.webpack-cli' });
1248
+ }
1249
+ const scriptMatches = filterScriptsByCommand(pkg?.scripts, 'webpack');
1250
+ for (const name of scriptMatches) {
1251
+ confidence = min(confidence + 5, 100);
1252
+ sources.push({ type: 'package.json', field: `scripts.${name}` });
1253
+ }
1254
+ if (confidence === 0) {
1255
+ return null;
1256
+ }
1257
+ return {
1258
+ id: 'webpack',
1259
+ name: 'Webpack',
1260
+ version,
1261
+ configPath,
1262
+ confidence: min(confidence, 100),
1263
+ detectedFrom: sources,
1264
+ };
1265
+ }
1266
+
1267
+ /** Config patterns for Vite */
1268
+ const VITE_CONFIG_PATTERNS = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'];
1269
+ /**
1270
+ * Detect Vite in project.
1271
+ *
1272
+ * @param projectPath - Project directory path
1273
+ * @param packageJson - Optional pre-loaded package.json
1274
+ * @returns Detection result or null if not detected
1275
+ */
1276
+ function viteDetector(projectPath, packageJson) {
1277
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1278
+ const sources = [];
1279
+ let confidence = 0;
1280
+ let version;
1281
+ const deps = collectAllDependencies(pkg);
1282
+ if (deps['vite']) {
1283
+ confidence += 60;
1284
+ version = parseVersionString(deps['vite']);
1285
+ sources.push({ type: 'package.json', field: 'dependencies.vite' });
1286
+ }
1287
+ const configPath = locateConfigFile(projectPath, VITE_CONFIG_PATTERNS);
1288
+ if (configPath) {
1289
+ confidence += 35;
1290
+ sources.push({ type: 'config-file', path: configPath });
1291
+ }
1292
+ if (deps['vitest']) {
1293
+ confidence += 10;
1294
+ sources.push({ type: 'package.json', field: 'dependencies.vitest' });
1295
+ }
1296
+ const vitePlugins = keys(deps).filter((d) => d.startsWith('vite-plugin-') || d.startsWith('@vitejs/'));
1297
+ if (vitePlugins.length > 0) {
1298
+ confidence += 10;
1299
+ sources.push({ type: 'package.json', field: 'dependencies (vite plugins)' });
1300
+ }
1301
+ if (confidence === 0) {
1302
+ return null;
1303
+ }
1304
+ return {
1305
+ id: 'vite',
1306
+ name: 'Vite',
1307
+ version,
1308
+ configPath,
1309
+ confidence: min(confidence, 100),
1310
+ detectedFrom: sources,
1311
+ };
1312
+ }
1313
+
1314
+ /** Config patterns for Rollup */
1315
+ const ROLLUP_CONFIG_PATTERNS = ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs', 'rollup.config.cjs'];
1316
+ /**
1317
+ * Detect Rollup in project.
1318
+ *
1319
+ * @param projectPath - Project directory path
1320
+ * @param packageJson - Optional pre-loaded package.json
1321
+ * @returns Detection result or null if not detected
1322
+ */
1323
+ function rollupDetector(projectPath, packageJson) {
1324
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1325
+ const sources = [];
1326
+ let confidence = 0;
1327
+ let version;
1328
+ const deps = collectAllDependencies(pkg);
1329
+ if (deps['rollup']) {
1330
+ confidence += 55;
1331
+ version = parseVersionString(deps['rollup']);
1332
+ sources.push({ type: 'package.json', field: 'dependencies.rollup' });
1333
+ }
1334
+ const configPath = locateConfigFile(projectPath, ROLLUP_CONFIG_PATTERNS);
1335
+ if (configPath) {
1336
+ confidence += 40;
1337
+ sources.push({ type: 'config-file', path: configPath });
1338
+ }
1339
+ const rollupPlugins = keys(deps).filter((d) => d.startsWith('@rollup/') || d.startsWith('rollup-plugin-'));
1340
+ if (rollupPlugins.length > 0) {
1341
+ confidence += 10;
1342
+ sources.push({ type: 'package.json', field: 'dependencies (rollup plugins)' });
1343
+ }
1344
+ const scriptMatches = filterScriptsByCommand(pkg?.scripts, 'rollup');
1345
+ for (const name of scriptMatches) {
1346
+ confidence = min(confidence + 5, 100);
1347
+ sources.push({ type: 'package.json', field: `scripts.${name}` });
1348
+ }
1349
+ if (confidence === 0) {
1350
+ return null;
1351
+ }
1352
+ return {
1353
+ id: 'rollup',
1354
+ name: 'Rollup',
1355
+ version,
1356
+ configPath,
1357
+ confidence: min(confidence, 100),
1358
+ detectedFrom: sources,
1359
+ };
1360
+ }
1361
+
1362
+ /**
1363
+ * Detect esbuild in project.
1364
+ *
1365
+ * @param projectPath - Project directory path
1366
+ * @param packageJson - Optional pre-loaded package.json
1367
+ * @returns Detection result or null if not detected
1368
+ */
1369
+ function esbuildDetector(projectPath, packageJson) {
1370
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1371
+ const sources = [];
1372
+ let confidence = 0;
1373
+ let version;
1374
+ const deps = collectAllDependencies(pkg);
1375
+ if (deps['esbuild']) {
1376
+ confidence += 70;
1377
+ version = parseVersionString(deps['esbuild']);
1378
+ sources.push({ type: 'package.json', field: 'dependencies.esbuild' });
1379
+ }
1380
+ const esbuildPlugins = keys(deps).filter((d) => d.includes('esbuild-plugin') || d.includes('esbuild-'));
1381
+ if (esbuildPlugins.length > 0) {
1382
+ confidence += 15;
1383
+ sources.push({ type: 'package.json', field: 'dependencies (esbuild plugins)' });
1384
+ }
1385
+ const scriptMatches = filterScriptsByCommand(pkg?.scripts, 'esbuild');
1386
+ for (const name of scriptMatches) {
1387
+ confidence = min(confidence + 10, 100);
1388
+ sources.push({ type: 'package.json', field: `scripts.${name}` });
1389
+ }
1390
+ if (confidence === 0) {
1391
+ return null;
1392
+ }
1393
+ return {
1394
+ id: 'esbuild',
1395
+ name: 'esbuild',
1396
+ version,
1397
+ confidence: min(confidence, 100),
1398
+ detectedFrom: sources,
1399
+ };
1400
+ }
1401
+
1402
+ /** Config patterns for Babel */
1403
+ const BABEL_CONFIG_PATTERNS = ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs', 'babel.config.json', '.babelrc', '.babelrc.json', '.babelrc.js'];
1404
+ /**
1405
+ * Detect Babel in project.
1406
+ *
1407
+ * @param projectPath - Project directory path
1408
+ * @param packageJson - Optional pre-loaded package.json
1409
+ * @returns Detection result or null if not detected
1410
+ */
1411
+ function babelDetector(projectPath, packageJson) {
1412
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1413
+ const sources = [];
1414
+ let confidence = 0;
1415
+ let version;
1416
+ const deps = collectAllDependencies(pkg);
1417
+ if (deps['@babel/core']) {
1418
+ confidence += 50;
1419
+ version = parseVersionString(deps['@babel/core']);
1420
+ sources.push({ type: 'package.json', field: 'dependencies.@babel/core' });
1421
+ }
1422
+ const configPath = locateConfigFile(projectPath, BABEL_CONFIG_PATTERNS);
1423
+ if (configPath) {
1424
+ confidence += 40;
1425
+ sources.push({ type: 'config-file', path: configPath });
1426
+ }
1427
+ if (pkg && 'babel' in pkg) {
1428
+ confidence += 30;
1429
+ sources.push({ type: 'package.json', field: 'babel' });
1430
+ }
1431
+ const babelPackages = keys(deps).filter((d) => d.startsWith('@babel/'));
1432
+ if (babelPackages.length > 1) {
1433
+ confidence += 10;
1434
+ sources.push({ type: 'package.json', field: 'dependencies (@babel packages)' });
1435
+ }
1436
+ if (confidence === 0) {
1437
+ return null;
1438
+ }
1439
+ return {
1440
+ id: 'babel',
1441
+ name: 'Babel',
1442
+ version,
1443
+ configPath,
1444
+ confidence: min(confidence, 100),
1445
+ detectedFrom: sources,
1446
+ };
1447
+ }
1448
+
1449
+ /** Config patterns for SWC */
1450
+ const SWC_CONFIG_PATTERNS = ['.swcrc', 'swc.config.js'];
1451
+ /**
1452
+ * Detect SWC in project.
1453
+ *
1454
+ * @param projectPath - Project directory path
1455
+ * @param packageJson - Optional pre-loaded package.json
1456
+ * @returns Detection result or null if not detected
1457
+ */
1458
+ function swcDetector(projectPath, packageJson) {
1459
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1460
+ const sources = [];
1461
+ let confidence = 0;
1462
+ let version;
1463
+ const deps = collectAllDependencies(pkg);
1464
+ if (deps['@swc/core']) {
1465
+ confidence += 60;
1466
+ version = parseVersionString(deps['@swc/core']);
1467
+ sources.push({ type: 'package.json', field: 'dependencies.@swc/core' });
1468
+ }
1469
+ const configPath = locateConfigFile(projectPath, SWC_CONFIG_PATTERNS);
1470
+ if (configPath) {
1471
+ confidence += 35;
1472
+ sources.push({ type: 'config-file', path: configPath });
1473
+ }
1474
+ if (deps['@swc/cli']) {
1475
+ confidence += 10;
1476
+ sources.push({ type: 'package.json', field: 'dependencies.@swc/cli' });
1477
+ }
1478
+ const swcPlugins = keys(deps).filter((d) => d.startsWith('@swc/') || d.includes('swc-plugin'));
1479
+ if (swcPlugins.length > 1) {
1480
+ confidence += 5;
1481
+ sources.push({ type: 'package.json', field: 'dependencies (@swc packages)' });
1482
+ }
1483
+ if (confidence === 0) {
1484
+ return null;
1485
+ }
1486
+ return {
1487
+ id: 'swc',
1488
+ name: 'SWC',
1489
+ version,
1490
+ configPath,
1491
+ confidence: min(confidence, 100),
1492
+ detectedFrom: sources,
1493
+ };
1494
+ }
1495
+
1496
+ /** Config patterns for Parcel */
1497
+ const PARCEL_CONFIG_PATTERNS = ['.parcelrc'];
1498
+ /**
1499
+ * Detect Parcel in project.
1500
+ *
1501
+ * @param projectPath - Project directory path
1502
+ * @param packageJson - Optional pre-loaded package.json
1503
+ * @returns Detection result or null if not detected
1504
+ */
1505
+ function parcelDetector(projectPath, packageJson) {
1506
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1507
+ const sources = [];
1508
+ let confidence = 0;
1509
+ let version;
1510
+ const deps = collectAllDependencies(pkg);
1511
+ if (deps['parcel']) {
1512
+ confidence += 60;
1513
+ version = parseVersionString(deps['parcel']);
1514
+ sources.push({ type: 'package.json', field: 'dependencies.parcel' });
1515
+ }
1516
+ if (deps['parcel-bundler']) {
1517
+ confidence += 60;
1518
+ version = parseVersionString(deps['parcel-bundler']);
1519
+ sources.push({ type: 'package.json', field: 'dependencies.parcel-bundler' });
1520
+ }
1521
+ const configPath = locateConfigFile(projectPath, PARCEL_CONFIG_PATTERNS);
1522
+ if (configPath) {
1523
+ confidence += 30;
1524
+ sources.push({ type: 'config-file', path: configPath });
1525
+ }
1526
+ const scriptMatches = filterScriptsByCommand(pkg?.scripts, 'parcel');
1527
+ for (const name of scriptMatches) {
1528
+ confidence = min(confidence + 10, 100);
1529
+ sources.push({ type: 'package.json', field: `scripts.${name}` });
1530
+ }
1531
+ if (confidence === 0) {
1532
+ return null;
1533
+ }
1534
+ return {
1535
+ id: 'parcel',
1536
+ name: 'Parcel',
1537
+ version,
1538
+ configPath,
1539
+ confidence: min(confidence, 100),
1540
+ detectedFrom: sources,
1541
+ };
1542
+ }
1543
+
1544
+ /** All build tool detectors */
1545
+ const buildToolDetectors = [
1546
+ { id: 'webpack', name: 'Webpack', detect: webpackDetector },
1547
+ { id: 'vite', name: 'Vite', detect: viteDetector },
1548
+ { id: 'rollup', name: 'Rollup', detect: rollupDetector },
1549
+ { id: 'esbuild', name: 'esbuild', detect: esbuildDetector },
1550
+ { id: 'babel', name: 'Babel', detect: babelDetector },
1551
+ { id: 'swc', name: 'SWC', detect: swcDetector },
1552
+ { id: 'parcel', name: 'Parcel', detect: parcelDetector },
1553
+ ];
1554
+
1555
+ /**
1556
+ * Detect React in project.
1557
+ *
1558
+ * @param projectPath - Project directory path
1559
+ * @param packageJson - Optional pre-loaded package.json
1560
+ * @returns Detection result or null if not detected
1561
+ */
1562
+ function reactDetector(projectPath, packageJson) {
1563
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1564
+ const sources = [];
1565
+ let confidence = 0;
1566
+ let version;
1567
+ const metaFrameworks = [];
1568
+ const deps = collectAllDependencies(pkg);
1569
+ if (deps['react']) {
1570
+ confidence += 60;
1571
+ version = parseVersionString(deps['react']);
1572
+ sources.push({ type: 'package.json', field: 'dependencies.react' });
1573
+ }
1574
+ if (deps['react-dom']) {
1575
+ confidence += 20;
1576
+ sources.push({ type: 'package.json', field: 'dependencies.react-dom' });
1577
+ }
1578
+ if (deps['react-native']) {
1579
+ confidence += 20;
1580
+ sources.push({ type: 'package.json', field: 'dependencies.react-native' });
1581
+ }
1582
+ const hasJsxFiles = exists(join$1(projectPath, 'src', 'App.tsx')) ||
1583
+ exists(join$1(projectPath, 'src', 'App.jsx')) ||
1584
+ exists(join$1(projectPath, 'src', 'index.tsx')) ||
1585
+ exists(join$1(projectPath, 'src', 'index.jsx'));
1586
+ if (hasJsxFiles) {
1587
+ confidence += 10;
1588
+ sources.push({ type: 'directory', path: 'src/*.tsx or src/*.jsx' });
1589
+ }
1590
+ if (deps['next']) {
1591
+ metaFrameworks.push({
1592
+ id: 'nextjs',
1593
+ name: 'Next.js',
1594
+ category: 'meta-framework',
1595
+ version: parseVersionString(deps['next']),
1596
+ confidence: 90,
1597
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.next' }],
1598
+ });
1599
+ }
1600
+ if (deps['gatsby']) {
1601
+ metaFrameworks.push({
1602
+ id: 'gatsby',
1603
+ name: 'Gatsby',
1604
+ category: 'meta-framework',
1605
+ version: parseVersionString(deps['gatsby']),
1606
+ confidence: 90,
1607
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.gatsby' }],
1608
+ });
1609
+ }
1610
+ if (deps['@remix-run/react'] || deps['remix']) {
1611
+ metaFrameworks.push({
1612
+ id: 'remix',
1613
+ name: 'Remix',
1614
+ category: 'meta-framework',
1615
+ version: parseVersionString(deps['@remix-run/react'] ?? deps['remix']),
1616
+ confidence: 90,
1617
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.@remix-run/react' }],
1618
+ });
1619
+ }
1620
+ if (confidence === 0) {
1621
+ return null;
1622
+ }
1623
+ return {
1624
+ id: 'react',
1625
+ name: 'React',
1626
+ category: 'frontend',
1627
+ version,
1628
+ confidence: min(confidence, 100),
1629
+ detectedFrom: sources,
1630
+ metaFrameworks: metaFrameworks.length > 0 ? metaFrameworks : undefined,
1631
+ };
1632
+ }
1633
+
1634
+ /**
1635
+ * Detect Next.js in project.
1636
+ *
1637
+ * @param projectPath - Project directory path
1638
+ * @param packageJson - Optional pre-loaded package.json
1639
+ * @returns Detection result or null if not detected
1640
+ */
1641
+ function nextjsDetector(projectPath, packageJson) {
1642
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1643
+ const sources = [];
1644
+ let confidence = 0;
1645
+ let version;
1646
+ const deps = collectAllDependencies(pkg);
1647
+ if (deps['next']) {
1648
+ confidence += 70;
1649
+ version = parseVersionString(deps['next']);
1650
+ sources.push({ type: 'package.json', field: 'dependencies.next' });
1651
+ }
1652
+ if (exists(join$1(projectPath, 'next.config.js')) ||
1653
+ exists(join$1(projectPath, 'next.config.mjs')) ||
1654
+ exists(join$1(projectPath, 'next.config.ts'))) {
1655
+ confidence += 25;
1656
+ sources.push({ type: 'config-file', path: 'next.config.*' });
1657
+ }
1658
+ if (exists(join$1(projectPath, 'pages')) ||
1659
+ exists(join$1(projectPath, 'app')) ||
1660
+ exists(join$1(projectPath, 'src', 'pages')) ||
1661
+ exists(join$1(projectPath, 'src', 'app'))) {
1662
+ confidence += 5;
1663
+ sources.push({ type: 'directory', path: 'pages/ or app/' });
1664
+ }
1665
+ if (confidence === 0) {
1666
+ return null;
1667
+ }
1668
+ return {
1669
+ id: 'nextjs',
1670
+ name: 'Next.js',
1671
+ category: 'meta-framework',
1672
+ version,
1673
+ confidence: min(confidence, 100),
1674
+ detectedFrom: sources,
1675
+ };
1676
+ }
1677
+
1678
+ /**
1679
+ * Detect Remix in project.
1680
+ *
1681
+ * @param projectPath - Project directory path
1682
+ * @param packageJson - Optional pre-loaded package.json
1683
+ * @returns Detection result or null if not detected
1684
+ */
1685
+ function remixDetector(projectPath, packageJson) {
1686
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1687
+ const sources = [];
1688
+ let confidence = 0;
1689
+ let version;
1690
+ const deps = collectAllDependencies(pkg);
1691
+ // @remix-run packages
1692
+ if (deps['@remix-run/react']) {
1693
+ confidence += 70;
1694
+ version = parseVersionString(deps['@remix-run/react']);
1695
+ sources.push({ type: 'package.json', field: 'dependencies.@remix-run/react' });
1696
+ }
1697
+ if (deps['@remix-run/node'] || deps['@remix-run/cloudflare'] || deps['@remix-run/deno']) {
1698
+ confidence += 20;
1699
+ sources.push({ type: 'package.json', field: 'dependencies.@remix-run/*' });
1700
+ }
1701
+ if (exists(join$1(projectPath, 'remix.config.js')) || exists(join$1(projectPath, 'remix.config.ts'))) {
1702
+ confidence += 10;
1703
+ sources.push({ type: 'config-file', path: 'remix.config.*' });
1704
+ }
1705
+ if (confidence === 0) {
1706
+ return null;
1707
+ }
1708
+ return {
1709
+ id: 'remix',
1710
+ name: 'Remix',
1711
+ category: 'meta-framework',
1712
+ version,
1713
+ confidence: min(confidence, 100),
1714
+ detectedFrom: sources,
1715
+ };
1716
+ }
1717
+
1718
+ /**
1719
+ * Detect Gatsby in project.
1720
+ *
1721
+ * @param projectPath - Project directory path
1722
+ * @param packageJson - Optional pre-loaded package.json
1723
+ * @returns Detection result or null if not detected
1724
+ */
1725
+ function gatsbyDetector(projectPath, packageJson) {
1726
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1727
+ const sources = [];
1728
+ let confidence = 0;
1729
+ let version;
1730
+ const deps = collectAllDependencies(pkg);
1731
+ if (deps['gatsby']) {
1732
+ confidence += 70;
1733
+ version = parseVersionString(deps['gatsby']);
1734
+ sources.push({ type: 'package.json', field: 'dependencies.gatsby' });
1735
+ }
1736
+ if (exists(join$1(projectPath, 'gatsby-config.js')) || exists(join$1(projectPath, 'gatsby-config.ts'))) {
1737
+ confidence += 25;
1738
+ sources.push({ type: 'config-file', path: 'gatsby-config.*' });
1739
+ }
1740
+ const gatsbyPlugins = keys(deps).filter((d) => d.startsWith('gatsby-plugin-') || d.startsWith('gatsby-source-'));
1741
+ if (gatsbyPlugins.length > 0) {
1742
+ confidence += 5;
1743
+ sources.push({ type: 'package.json', field: 'dependencies (gatsby plugins)' });
1744
+ }
1745
+ if (confidence === 0) {
1746
+ return null;
1747
+ }
1748
+ return {
1749
+ id: 'gatsby',
1750
+ name: 'Gatsby',
1751
+ category: 'meta-framework',
1752
+ version,
1753
+ confidence: min(confidence, 100),
1754
+ detectedFrom: sources,
1755
+ };
1756
+ }
1757
+
1758
+ /**
1759
+ * Detect Vue in project.
1760
+ *
1761
+ * @param projectPath - Project directory path
1762
+ * @param packageJson - Optional pre-loaded package.json
1763
+ * @returns Detection result or null if not detected
1764
+ */
1765
+ function vueDetector(projectPath, packageJson) {
1766
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1767
+ const sources = [];
1768
+ let confidence = 0;
1769
+ let version;
1770
+ const metaFrameworks = [];
1771
+ const deps = collectAllDependencies(pkg);
1772
+ if (deps['vue']) {
1773
+ confidence += 70;
1774
+ version = parseVersionString(deps['vue']);
1775
+ sources.push({ type: 'package.json', field: 'dependencies.vue' });
1776
+ }
1777
+ if (deps['@vue/cli-service']) {
1778
+ confidence += 15;
1779
+ sources.push({ type: 'package.json', field: 'dependencies.@vue/cli-service' });
1780
+ }
1781
+ const hasVueFiles = exists(join$1(projectPath, 'src', 'App.vue')) || exists(join$1(projectPath, 'src', 'main.vue'));
1782
+ if (hasVueFiles) {
1783
+ confidence += 10;
1784
+ sources.push({ type: 'directory', path: 'src/*.vue' });
1785
+ }
1786
+ if (exists(join$1(projectPath, 'vue.config.js'))) {
1787
+ confidence += 5;
1788
+ sources.push({ type: 'config-file', path: 'vue.config.js' });
1789
+ }
1790
+ if (deps['nuxt'] || deps['nuxt3']) {
1791
+ metaFrameworks.push({
1792
+ id: 'nuxt',
1793
+ name: 'Nuxt',
1794
+ category: 'meta-framework',
1795
+ version: parseVersionString(deps['nuxt'] ?? deps['nuxt3']),
1796
+ confidence: 90,
1797
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.nuxt' }],
1798
+ });
1799
+ }
1800
+ if (confidence === 0) {
1801
+ return null;
1802
+ }
1803
+ return {
1804
+ id: 'vue',
1805
+ name: 'Vue',
1806
+ category: 'frontend',
1807
+ version,
1808
+ confidence: min(confidence, 100),
1809
+ detectedFrom: sources,
1810
+ metaFrameworks: metaFrameworks.length > 0 ? metaFrameworks : undefined,
1811
+ };
1812
+ }
1813
+
1814
+ /**
1815
+ * Detect Nuxt in project.
1816
+ *
1817
+ * @param projectPath - Project directory path
1818
+ * @param packageJson - Optional pre-loaded package.json
1819
+ * @returns Detection result or null if not detected
1820
+ */
1821
+ function nuxtDetector(projectPath, packageJson) {
1822
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1823
+ const sources = [];
1824
+ let confidence = 0;
1825
+ let version;
1826
+ const deps = collectAllDependencies(pkg);
1827
+ if (deps['nuxt'] || deps['nuxt3']) {
1828
+ confidence += 70;
1829
+ version = parseVersionString(deps['nuxt'] ?? deps['nuxt3']);
1830
+ sources.push({ type: 'package.json', field: 'dependencies.nuxt' });
1831
+ }
1832
+ if (exists(join$1(projectPath, 'nuxt.config.js')) || exists(join$1(projectPath, 'nuxt.config.ts'))) {
1833
+ confidence += 25;
1834
+ sources.push({ type: 'config-file', path: 'nuxt.config.*' });
1835
+ }
1836
+ if (exists(join$1(projectPath, 'pages'))) {
1837
+ confidence += 5;
1838
+ sources.push({ type: 'directory', path: 'pages/' });
1839
+ }
1840
+ if (confidence === 0) {
1841
+ return null;
1842
+ }
1843
+ return {
1844
+ id: 'nuxt',
1845
+ name: 'Nuxt',
1846
+ category: 'meta-framework',
1847
+ version,
1848
+ confidence: min(confidence, 100),
1849
+ detectedFrom: sources,
1850
+ };
1851
+ }
1852
+
1853
+ /**
1854
+ * Detect Angular in project.
1855
+ *
1856
+ * @param projectPath - Project directory path
1857
+ * @param packageJson - Optional pre-loaded package.json
1858
+ * @returns Detection result or null if not detected
1859
+ */
1860
+ function angularDetector(projectPath, packageJson) {
1861
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1862
+ const sources = [];
1863
+ let confidence = 0;
1864
+ let version;
1865
+ const deps = collectAllDependencies(pkg);
1866
+ if (deps['@angular/core']) {
1867
+ confidence += 70;
1868
+ version = parseVersionString(deps['@angular/core']);
1869
+ sources.push({ type: 'package.json', field: 'dependencies.@angular/core' });
1870
+ }
1871
+ if (deps['@angular/cli']) {
1872
+ confidence += 15;
1873
+ sources.push({ type: 'package.json', field: 'dependencies.@angular/cli' });
1874
+ }
1875
+ if (exists(join$1(projectPath, 'angular.json'))) {
1876
+ confidence += 15;
1877
+ sources.push({ type: 'config-file', path: 'angular.json' });
1878
+ }
1879
+ if (deps['angular'] && !deps['@angular/core']) {
1880
+ return {
1881
+ id: 'angularjs',
1882
+ name: 'AngularJS (Legacy)',
1883
+ category: 'frontend',
1884
+ version: parseVersionString(deps['angular']),
1885
+ confidence: 80,
1886
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.angular' }],
1887
+ };
1888
+ }
1889
+ if (confidence === 0) {
1890
+ return null;
1891
+ }
1892
+ return {
1893
+ id: 'angular',
1894
+ name: 'Angular',
1895
+ category: 'frontend',
1896
+ version,
1897
+ confidence: min(confidence, 100),
1898
+ detectedFrom: sources,
1899
+ };
1900
+ }
1901
+
1902
+ /**
1903
+ * Detect Svelte in project.
1904
+ *
1905
+ * @param projectPath - Project directory path
1906
+ * @param packageJson - Optional pre-loaded package.json
1907
+ * @returns Detection result or null if not detected
1908
+ */
1909
+ function svelteDetector(projectPath, packageJson) {
1910
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1911
+ const sources = [];
1912
+ let confidence = 0;
1913
+ let version;
1914
+ const metaFrameworks = [];
1915
+ const deps = collectAllDependencies(pkg);
1916
+ if (deps['svelte']) {
1917
+ confidence += 70;
1918
+ version = parseVersionString(deps['svelte']);
1919
+ sources.push({ type: 'package.json', field: 'dependencies.svelte' });
1920
+ }
1921
+ if (exists(join$1(projectPath, 'svelte.config.js'))) {
1922
+ confidence += 20;
1923
+ sources.push({ type: 'config-file', path: 'svelte.config.js' });
1924
+ }
1925
+ const hasSvelteFiles = exists(join$1(projectPath, 'src', 'App.svelte')) || exists(join$1(projectPath, 'src', 'routes'));
1926
+ if (hasSvelteFiles) {
1927
+ confidence += 10;
1928
+ sources.push({ type: 'directory', path: 'src/*.svelte or src/routes/' });
1929
+ }
1930
+ if (deps['@sveltejs/kit']) {
1931
+ metaFrameworks.push({
1932
+ id: 'sveltekit',
1933
+ name: 'SvelteKit',
1934
+ category: 'meta-framework',
1935
+ version: parseVersionString(deps['@sveltejs/kit']),
1936
+ confidence: 90,
1937
+ detectedFrom: [{ type: 'package.json', field: 'dependencies.@sveltejs/kit' }],
1938
+ });
1939
+ }
1940
+ if (confidence === 0) {
1941
+ return null;
1942
+ }
1943
+ return {
1944
+ id: 'svelte',
1945
+ name: 'Svelte',
1946
+ category: 'frontend',
1947
+ version,
1948
+ confidence: min(confidence, 100),
1949
+ detectedFrom: sources,
1950
+ metaFrameworks: metaFrameworks.length > 0 ? metaFrameworks : undefined,
1951
+ };
1952
+ }
1953
+
1954
+ /**
1955
+ * Detect SvelteKit in project.
1956
+ *
1957
+ * @param projectPath - Project directory path
1958
+ * @param packageJson - Optional pre-loaded package.json
1959
+ * @returns Detection result or null if not detected
1960
+ */
1961
+ function sveltekitDetector(projectPath, packageJson) {
1962
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
1963
+ const sources = [];
1964
+ let confidence = 0;
1965
+ let version;
1966
+ const deps = collectAllDependencies(pkg);
1967
+ // @sveltejs/kit package
1968
+ if (deps['@sveltejs/kit']) {
1969
+ confidence += 70;
1970
+ version = parseVersionString(deps['@sveltejs/kit']);
1971
+ sources.push({ type: 'package.json', field: 'dependencies.@sveltejs/kit' });
1972
+ }
1973
+ if (exists(join$1(projectPath, 'svelte.config.js'))) {
1974
+ confidence += 20;
1975
+ sources.push({ type: 'config-file', path: 'svelte.config.js' });
1976
+ }
1977
+ if (exists(join$1(projectPath, 'src', 'routes'))) {
1978
+ confidence += 10;
1979
+ sources.push({ type: 'directory', path: 'src/routes/' });
1980
+ }
1981
+ if (confidence === 0) {
1982
+ return null;
1983
+ }
1984
+ return {
1985
+ id: 'sveltekit',
1986
+ name: 'SvelteKit',
1987
+ category: 'meta-framework',
1988
+ version,
1989
+ confidence: min(confidence, 100),
1990
+ detectedFrom: sources,
1991
+ };
1992
+ }
1993
+
1994
+ /**
1995
+ * Detect Solid in project.
1996
+ *
1997
+ * @param projectPath - Project directory path
1998
+ * @param packageJson - Optional pre-loaded package.json
1999
+ * @returns Detection result or null if not detected
2000
+ */
2001
+ function solidDetector(projectPath, packageJson) {
2002
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2003
+ const sources = [];
2004
+ let confidence = 0;
2005
+ let version;
2006
+ const deps = collectAllDependencies(pkg);
2007
+ if (deps['solid-js']) {
2008
+ confidence += 70;
2009
+ version = parseVersionString(deps['solid-js']);
2010
+ sources.push({ type: 'package.json', field: 'dependencies.solid-js' });
2011
+ }
2012
+ if (deps['vite-plugin-solid']) {
2013
+ confidence += 20;
2014
+ sources.push({ type: 'package.json', field: 'dependencies.vite-plugin-solid' });
2015
+ }
2016
+ if (deps['solid-start'] || deps['@solidjs/start']) {
2017
+ confidence += 10;
2018
+ sources.push({ type: 'package.json', field: 'dependencies.solid-start' });
2019
+ }
2020
+ if (confidence === 0) {
2021
+ return null;
2022
+ }
2023
+ return {
2024
+ id: 'solid',
2025
+ name: 'Solid',
2026
+ category: 'frontend',
2027
+ version,
2028
+ confidence: min(confidence, 100),
2029
+ detectedFrom: sources,
2030
+ };
2031
+ }
2032
+
2033
+ /**
2034
+ * Detect Qwik in project.
2035
+ *
2036
+ * @param projectPath - Project directory path
2037
+ * @param packageJson - Optional pre-loaded package.json
2038
+ * @returns Detection result or null if not detected
2039
+ */
2040
+ function qwikDetector(projectPath, packageJson) {
2041
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2042
+ const sources = [];
2043
+ let confidence = 0;
2044
+ let version;
2045
+ const deps = collectAllDependencies(pkg);
2046
+ // @builder.io/qwik package
2047
+ if (deps['@builder.io/qwik']) {
2048
+ confidence += 70;
2049
+ version = parseVersionString(deps['@builder.io/qwik']);
2050
+ sources.push({ type: 'package.json', field: 'dependencies.@builder.io/qwik' });
2051
+ }
2052
+ // @builder.io/qwik-city
2053
+ if (deps['@builder.io/qwik-city']) {
2054
+ confidence += 20;
2055
+ sources.push({ type: 'package.json', field: 'dependencies.@builder.io/qwik-city' });
2056
+ }
2057
+ if (exists(join$1(projectPath, 'qwik.config.ts')) || exists(join$1(projectPath, 'qwik.config.js'))) {
2058
+ confidence += 10;
2059
+ sources.push({ type: 'config-file', path: 'qwik.config.*' });
2060
+ }
2061
+ if (confidence === 0) {
2062
+ return null;
2063
+ }
2064
+ return {
2065
+ id: 'qwik',
2066
+ name: 'Qwik',
2067
+ category: 'frontend',
2068
+ version,
2069
+ confidence: min(confidence, 100),
2070
+ detectedFrom: sources,
2071
+ };
2072
+ }
2073
+
2074
+ /**
2075
+ * Detect Astro in project.
2076
+ *
2077
+ * @param projectPath - Project directory path
2078
+ * @param packageJson - Optional pre-loaded package.json
2079
+ * @returns Detection result or null if not detected
2080
+ */
2081
+ function astroDetector(projectPath, packageJson) {
2082
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2083
+ const sources = [];
2084
+ let confidence = 0;
2085
+ let version;
2086
+ const deps = collectAllDependencies(pkg);
2087
+ if (deps['astro']) {
2088
+ confidence += 70;
2089
+ version = parseVersionString(deps['astro']);
2090
+ sources.push({ type: 'package.json', field: 'dependencies.astro' });
2091
+ }
2092
+ if (exists(join$1(projectPath, 'astro.config.mjs')) ||
2093
+ exists(join$1(projectPath, 'astro.config.ts')) ||
2094
+ exists(join$1(projectPath, 'astro.config.js'))) {
2095
+ confidence += 25;
2096
+ sources.push({ type: 'config-file', path: 'astro.config.*' });
2097
+ }
2098
+ if (exists(join$1(projectPath, 'src', 'pages'))) {
2099
+ confidence += 5;
2100
+ sources.push({ type: 'directory', path: 'src/pages/' });
2101
+ }
2102
+ if (confidence === 0) {
2103
+ return null;
2104
+ }
2105
+ return {
2106
+ id: 'astro',
2107
+ name: 'Astro',
2108
+ category: 'meta-framework',
2109
+ version,
2110
+ confidence: min(confidence, 100),
2111
+ detectedFrom: sources,
2112
+ };
2113
+ }
2114
+
2115
+ /** All frontend framework detectors */
2116
+ const frameworkDetectors = [
2117
+ { id: 'react', name: 'React', category: 'frontend', detect: reactDetector },
2118
+ { id: 'nextjs', name: 'Next.js', category: 'meta-framework', detect: nextjsDetector },
2119
+ { id: 'remix', name: 'Remix', category: 'meta-framework', detect: remixDetector },
2120
+ { id: 'gatsby', name: 'Gatsby', category: 'meta-framework', detect: gatsbyDetector },
2121
+ { id: 'vue', name: 'Vue', category: 'frontend', detect: vueDetector },
2122
+ { id: 'nuxt', name: 'Nuxt', category: 'meta-framework', detect: nuxtDetector },
2123
+ { id: 'angular', name: 'Angular', category: 'frontend', detect: angularDetector },
2124
+ { id: 'svelte', name: 'Svelte', category: 'frontend', detect: svelteDetector },
2125
+ { id: 'sveltekit', name: 'SvelteKit', category: 'meta-framework', detect: sveltekitDetector },
2126
+ { id: 'solid', name: 'Solid', category: 'frontend', detect: solidDetector },
2127
+ { id: 'qwik', name: 'Qwik', category: 'frontend', detect: qwikDetector },
2128
+ { id: 'astro', name: 'Astro', category: 'meta-framework', detect: astroDetector },
2129
+ ];
2130
+
2131
+ /**
2132
+ * Detect AngularJS (1.x) in project.
2133
+ * AngularJS is the original Angular framework, distinct from Angular 2+.
2134
+ *
2135
+ * @param projectPath - Project directory path
2136
+ * @param packageJson - Optional pre-loaded package.json
2137
+ * @returns Detection result or null if not detected
2138
+ */
2139
+ function angularJSDetector(projectPath, packageJson) {
2140
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2141
+ const sources = [];
2142
+ let confidence = 0;
2143
+ let version;
2144
+ const deps = collectAllDependencies(pkg);
2145
+ // AngularJS package (angular, not @angular/core)
2146
+ if (deps['angular']) {
2147
+ confidence += 70;
2148
+ version = parseVersionString(deps['angular']);
2149
+ sources.push({ type: 'package.json', field: 'dependencies.angular' });
2150
+ }
2151
+ // AngularJS router
2152
+ if (deps['angular-route']) {
2153
+ confidence += 15;
2154
+ sources.push({ type: 'package.json', field: 'dependencies.angular-route' });
2155
+ }
2156
+ // AngularJS resource
2157
+ if (deps['angular-resource']) {
2158
+ confidence += 10;
2159
+ sources.push({ type: 'package.json', field: 'dependencies.angular-resource' });
2160
+ }
2161
+ // AngularJS animate
2162
+ if (deps['angular-animate']) {
2163
+ confidence += 5;
2164
+ sources.push({ type: 'package.json', field: 'dependencies.angular-animate' });
2165
+ }
2166
+ if (confidence === 0) {
2167
+ return null;
2168
+ }
2169
+ return {
2170
+ id: 'angularjs',
2171
+ name: 'AngularJS',
2172
+ category: 'legacy-frontend',
2173
+ version,
2174
+ confidence: min(confidence, 100),
2175
+ detectedFrom: sources,
2176
+ };
2177
+ }
2178
+
2179
+ /**
2180
+ * Detect Backbone.js in project.
2181
+ *
2182
+ * @param projectPath - Project directory path
2183
+ * @param packageJson - Optional pre-loaded package.json
2184
+ * @returns Detection result or null if not detected
2185
+ */
2186
+ function backboneDetector(projectPath, packageJson) {
2187
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2188
+ const sources = [];
2189
+ let confidence = 0;
2190
+ let version;
2191
+ const deps = collectAllDependencies(pkg);
2192
+ // Backbone package
2193
+ if (deps['backbone']) {
2194
+ confidence += 70;
2195
+ version = parseVersionString(deps['backbone']);
2196
+ sources.push({ type: 'package.json', field: 'dependencies.backbone' });
2197
+ // Underscore (commonly used with Backbone)
2198
+ if (deps['underscore']) {
2199
+ confidence += 15;
2200
+ sources.push({ type: 'package.json', field: 'dependencies.underscore' });
2201
+ }
2202
+ // Lodash can be used as underscore replacement
2203
+ if (deps['lodash']) {
2204
+ confidence += 5;
2205
+ sources.push({ type: 'package.json', field: 'dependencies.lodash' });
2206
+ }
2207
+ }
2208
+ // Marionette (Backbone framework)
2209
+ if (deps['backbone.marionette'] || deps['marionette']) {
2210
+ confidence += 10;
2211
+ sources.push({ type: 'package.json', field: 'dependencies.backbone.marionette' });
2212
+ }
2213
+ if (confidence === 0) {
2214
+ return null;
2215
+ }
2216
+ return {
2217
+ id: 'backbone',
2218
+ name: 'Backbone.js',
2219
+ category: 'legacy-frontend',
2220
+ version,
2221
+ confidence: min(confidence, 100),
2222
+ detectedFrom: sources,
2223
+ };
2224
+ }
2225
+
2226
+ /**
2227
+ * Detect Ember.js in project.
2228
+ *
2229
+ * @param projectPath - Project directory path
2230
+ * @param packageJson - Optional pre-loaded package.json
2231
+ * @returns Detection result or null if not detected
2232
+ */
2233
+ function emberDetector(projectPath, packageJson) {
2234
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2235
+ const sources = [];
2236
+ let confidence = 0;
2237
+ let version;
2238
+ const deps = collectAllDependencies(pkg);
2239
+ // Ember source package
2240
+ if (deps['ember-source']) {
2241
+ confidence += 70;
2242
+ version = parseVersionString(deps['ember-source']);
2243
+ sources.push({ type: 'package.json', field: 'dependencies.ember-source' });
2244
+ }
2245
+ // Ember CLI
2246
+ if (deps['ember-cli']) {
2247
+ confidence += 20;
2248
+ sources.push({ type: 'package.json', field: 'devDependencies.ember-cli' });
2249
+ }
2250
+ // Ember Data
2251
+ if (deps['ember-data']) {
2252
+ confidence += 10;
2253
+ sources.push({ type: 'package.json', field: 'dependencies.ember-data' });
2254
+ }
2255
+ if (confidence === 0) {
2256
+ return null;
2257
+ }
2258
+ return {
2259
+ id: 'ember',
2260
+ name: 'Ember.js',
2261
+ category: 'legacy-frontend',
2262
+ version,
2263
+ confidence: min(confidence, 100),
2264
+ detectedFrom: sources,
2265
+ };
2266
+ }
2267
+
2268
+ /**
2269
+ * Detect jQuery in project.
2270
+ *
2271
+ * @param projectPath - Project directory path
2272
+ * @param packageJson - Optional pre-loaded package.json
2273
+ * @returns Detection result or null if not detected
2274
+ */
2275
+ function jqueryDetector(projectPath, packageJson) {
2276
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2277
+ const sources = [];
2278
+ let confidence = 0;
2279
+ let version;
2280
+ const deps = collectAllDependencies(pkg);
2281
+ // jQuery package
2282
+ if (deps['jquery']) {
2283
+ confidence += 80;
2284
+ version = parseVersionString(deps['jquery']);
2285
+ sources.push({ type: 'package.json', field: 'dependencies.jquery' });
2286
+ }
2287
+ // jQuery UI
2288
+ if (deps['jquery-ui']) {
2289
+ confidence += 10;
2290
+ sources.push({ type: 'package.json', field: 'dependencies.jquery-ui' });
2291
+ }
2292
+ // jQuery plugins
2293
+ if (deps['jquery-validation']) {
2294
+ confidence += 5;
2295
+ sources.push({ type: 'package.json', field: 'dependencies.jquery-validation' });
2296
+ }
2297
+ if (confidence === 0) {
2298
+ return null;
2299
+ }
2300
+ return {
2301
+ id: 'jquery',
2302
+ name: 'jQuery',
2303
+ category: 'legacy-frontend',
2304
+ version,
2305
+ confidence: min(confidence, 100),
2306
+ detectedFrom: sources,
2307
+ };
2308
+ }
2309
+
2310
+ /** All legacy framework detectors */
2311
+ const legacyDetectors = [
2312
+ { id: 'angularjs', name: 'AngularJS', category: 'legacy-frontend', detect: angularJSDetector },
2313
+ { id: 'backbone', name: 'Backbone.js', category: 'legacy-frontend', detect: backboneDetector },
2314
+ { id: 'ember', name: 'Ember.js', category: 'legacy-frontend', detect: emberDetector },
2315
+ { id: 'jquery', name: 'jQuery', category: 'legacy-frontend', detect: jqueryDetector },
2316
+ ];
2317
+
2318
+ /** Config patterns for ESLint */
2319
+ const ESLINT_CONFIG_PATTERNS = [
2320
+ 'eslint.config.js',
2321
+ 'eslint.config.mjs',
2322
+ 'eslint.config.cjs',
2323
+ 'eslint.config.ts',
2324
+ '.eslintrc.js',
2325
+ '.eslintrc.cjs',
2326
+ '.eslintrc.json',
2327
+ '.eslintrc.yaml',
2328
+ '.eslintrc.yml',
2329
+ '.eslintrc',
2330
+ ];
2331
+ /**
2332
+ * Detect ESLint in project.
2333
+ *
2334
+ * @param projectPath - Project directory path
2335
+ * @param packageJson - Optional pre-loaded package.json
2336
+ * @returns Detection result or null if not detected
2337
+ */
2338
+ function eslintDetector(projectPath, packageJson) {
2339
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2340
+ const sources = [];
2341
+ let confidence = 0;
2342
+ let version;
2343
+ const deps = collectAllDependencies(pkg);
2344
+ if (deps['eslint']) {
2345
+ confidence += 50;
2346
+ version = parseVersionString(deps['eslint']);
2347
+ sources.push({ type: 'package.json', field: 'dependencies.eslint' });
2348
+ }
2349
+ const configPath = locateConfigFile(projectPath, ESLINT_CONFIG_PATTERNS);
2350
+ if (configPath) {
2351
+ confidence += 40;
2352
+ sources.push({ type: 'config-file', path: configPath });
2353
+ }
2354
+ if (pkg && 'eslintConfig' in pkg) {
2355
+ confidence += 30;
2356
+ sources.push({ type: 'package.json', field: 'eslintConfig' });
2357
+ }
2358
+ const eslintPlugins = keys(deps).filter((d) => d.startsWith('eslint-plugin-') || d.startsWith('@typescript-eslint/') || d.startsWith('eslint-config-'));
2359
+ if (eslintPlugins.length > 0) {
2360
+ confidence += 10;
2361
+ sources.push({ type: 'package.json', field: 'dependencies (eslint plugins)' });
2362
+ }
2363
+ const lintScript = pkg?.scripts?.['lint'] ?? '';
2364
+ if (lintScript.includes('eslint')) {
2365
+ confidence += 5;
2366
+ sources.push({ type: 'package.json', field: 'scripts.lint' });
2367
+ }
2368
+ if (confidence === 0) {
2369
+ return null;
2370
+ }
2371
+ return {
2372
+ id: 'eslint',
2373
+ name: 'ESLint',
2374
+ version,
2375
+ configPath,
2376
+ confidence: min(confidence, 100),
2377
+ detectedFrom: sources,
2378
+ };
2379
+ }
2380
+
2381
+ /** Config patterns for Prettier */
2382
+ const PRETTIER_CONFIG_PATTERNS = [
2383
+ 'prettier.config.js',
2384
+ 'prettier.config.mjs',
2385
+ 'prettier.config.cjs',
2386
+ '.prettierrc',
2387
+ '.prettierrc.json',
2388
+ '.prettierrc.yaml',
2389
+ '.prettierrc.yml',
2390
+ '.prettierrc.js',
2391
+ '.prettierrc.cjs',
2392
+ '.prettierrc.toml',
2393
+ ];
2394
+ /**
2395
+ * Detect Prettier in project.
2396
+ *
2397
+ * @param projectPath - Project directory path
2398
+ * @param packageJson - Optional pre-loaded package.json
2399
+ * @returns Detection result or null if not detected
2400
+ */
2401
+ function prettierDetector(projectPath, packageJson) {
2402
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2403
+ const sources = [];
2404
+ let confidence = 0;
2405
+ let version;
2406
+ const deps = collectAllDependencies(pkg);
2407
+ if (deps['prettier']) {
2408
+ confidence += 50;
2409
+ version = parseVersionString(deps['prettier']);
2410
+ sources.push({ type: 'package.json', field: 'dependencies.prettier' });
2411
+ }
2412
+ const configPath = locateConfigFile(projectPath, PRETTIER_CONFIG_PATTERNS);
2413
+ if (configPath) {
2414
+ confidence += 40;
2415
+ sources.push({ type: 'config-file', path: configPath });
2416
+ }
2417
+ if (pkg && 'prettier' in pkg) {
2418
+ confidence += 30;
2419
+ sources.push({ type: 'package.json', field: 'prettier' });
2420
+ }
2421
+ // .prettierignore file
2422
+ if (exists(join$1(projectPath, '.prettierignore'))) {
2423
+ confidence += 10;
2424
+ sources.push({ type: 'config-file', path: '.prettierignore' });
2425
+ }
2426
+ const prettierPlugins = keys(deps).filter((d) => d.startsWith('prettier-plugin-'));
2427
+ if (prettierPlugins.length > 0) {
2428
+ confidence += 5;
2429
+ sources.push({ type: 'package.json', field: 'dependencies (prettier plugins)' });
2430
+ }
2431
+ const formatScript = pkg?.scripts?.['format'] ?? pkg?.scripts?.['prettier'] ?? '';
2432
+ if (formatScript.includes('prettier')) {
2433
+ confidence += 5;
2434
+ sources.push({ type: 'package.json', field: 'scripts.format' });
2435
+ }
2436
+ if (confidence === 0) {
2437
+ return null;
2438
+ }
2439
+ return {
2440
+ id: 'prettier',
2441
+ name: 'Prettier',
2442
+ version,
2443
+ configPath,
2444
+ confidence: min(confidence, 100),
2445
+ detectedFrom: sources,
2446
+ };
2447
+ }
2448
+
2449
+ /** Config patterns for Stylelint */
2450
+ const STYLELINT_CONFIG_PATTERNS = [
2451
+ '.stylelintrc',
2452
+ '.stylelintrc.json',
2453
+ '.stylelintrc.js',
2454
+ 'stylelint.config.js',
2455
+ 'stylelint.config.cjs',
2456
+ ];
2457
+ /**
2458
+ * Detect Stylelint in project.
2459
+ *
2460
+ * @param projectPath - Project directory path
2461
+ * @param packageJson - Optional pre-loaded package.json
2462
+ * @returns Detection result or null if not detected
2463
+ */
2464
+ function stylelintDetector(projectPath, packageJson) {
2465
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2466
+ const sources = [];
2467
+ let confidence = 0;
2468
+ let configPath;
2469
+ let version;
2470
+ const deps = collectAllDependencies(pkg);
2471
+ if (deps['stylelint']) {
2472
+ confidence += 60;
2473
+ version = parseVersionString(deps['stylelint']);
2474
+ sources.push({ type: 'package.json', field: 'dependencies.stylelint' });
2475
+ }
2476
+ for (const config of STYLELINT_CONFIG_PATTERNS) {
2477
+ if (exists(join$1(projectPath, config))) {
2478
+ confidence += 35;
2479
+ configPath = config;
2480
+ sources.push({ type: 'config-file', path: config });
2481
+ break;
2482
+ }
2483
+ }
2484
+ const stylelintPlugins = keys(deps).filter((d) => d.startsWith('stylelint-'));
2485
+ if (stylelintPlugins.length > 0) {
2486
+ confidence += 5;
2487
+ sources.push({ type: 'package.json', field: 'dependencies (stylelint plugins)' });
2488
+ }
2489
+ if (confidence === 0) {
2490
+ return null;
2491
+ }
2492
+ return {
2493
+ id: 'stylelint',
2494
+ name: 'Stylelint',
2495
+ version,
2496
+ configPath,
2497
+ confidence: min(confidence, 100),
2498
+ detectedFrom: sources,
2499
+ };
2500
+ }
2501
+
2502
+ /**
2503
+ * Detect Biome in project.
2504
+ *
2505
+ * @param projectPath - Project directory path
2506
+ * @param packageJson - Optional pre-loaded package.json
2507
+ * @returns Detection result or null if not detected
2508
+ */
2509
+ function biomeDetector(projectPath, packageJson) {
2510
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2511
+ const sources = [];
2512
+ let confidence = 0;
2513
+ let configPath;
2514
+ let version;
2515
+ const deps = collectAllDependencies(pkg);
2516
+ // @biomejs/biome package
2517
+ if (deps['@biomejs/biome']) {
2518
+ confidence += 70;
2519
+ version = parseVersionString(deps['@biomejs/biome']);
2520
+ sources.push({ type: 'package.json', field: 'dependencies.@biomejs/biome' });
2521
+ }
2522
+ if (exists(join$1(projectPath, 'biome.json'))) {
2523
+ confidence += 30;
2524
+ configPath = 'biome.json';
2525
+ sources.push({ type: 'config-file', path: 'biome.json' });
2526
+ }
2527
+ if (!configPath && exists(join$1(projectPath, 'biome.jsonc'))) {
2528
+ confidence += 30;
2529
+ configPath = 'biome.jsonc';
2530
+ sources.push({ type: 'config-file', path: 'biome.jsonc' });
2531
+ }
2532
+ if (confidence === 0) {
2533
+ return null;
2534
+ }
2535
+ return {
2536
+ id: 'biome',
2537
+ name: 'Biome',
2538
+ version,
2539
+ configPath,
2540
+ confidence: min(confidence, 100),
2541
+ detectedFrom: sources,
2542
+ };
2543
+ }
2544
+
2545
+ /** All linting tool detectors */
2546
+ const lintingDetectors = [
2547
+ { id: 'eslint', name: 'ESLint', detect: eslintDetector },
2548
+ { id: 'prettier', name: 'Prettier', detect: prettierDetector },
2549
+ { id: 'stylelint', name: 'Stylelint', detect: stylelintDetector },
2550
+ { id: 'biome', name: 'Biome', detect: biomeDetector },
2551
+ ];
2552
+
2553
+ /**
2554
+ * Detect NX in project.
2555
+ *
2556
+ * @param workspacePath - Workspace directory path
2557
+ * @param packageJson - Optional pre-loaded package.json
2558
+ * @returns Detection result or null if not detected
2559
+ */
2560
+ function nxDetector(workspacePath, packageJson) {
2561
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2562
+ const sources = [];
2563
+ let confidence = 0;
2564
+ let version;
2565
+ let workspaceLayout;
2566
+ const nxJsonPath = join$1(workspacePath, 'nx.json');
2567
+ if (exists(nxJsonPath)) {
2568
+ confidence += 70;
2569
+ sources.push({ type: 'config-file', path: 'nx.json' });
2570
+ }
2571
+ const deps = collectAllDependencies(pkg);
2572
+ if (deps['nx']) {
2573
+ confidence += 20;
2574
+ version = parseVersionString(deps['nx']);
2575
+ sources.push({ type: 'package.json', field: 'dependencies.nx' });
2576
+ }
2577
+ const hasApps = exists(join$1(workspacePath, 'apps'));
2578
+ const hasLibs = exists(join$1(workspacePath, 'libs'));
2579
+ if (hasApps || hasLibs) {
2580
+ confidence += 10;
2581
+ sources.push({ type: 'directory', path: 'apps/ or libs/' });
2582
+ workspaceLayout = {
2583
+ appsDir: hasApps ? 'apps' : '',
2584
+ libsDir: hasLibs ? 'libs' : '',
2585
+ };
2586
+ }
2587
+ const nxPackages = keys(deps).filter((d) => d.startsWith('@nx/') || d.startsWith('@nrwl/'));
2588
+ if (nxPackages.length > 0) {
2589
+ confidence += 10;
2590
+ sources.push({ type: 'package.json', field: '@nx/* packages' });
2591
+ }
2592
+ if (confidence === 0) {
2593
+ return null;
2594
+ }
2595
+ return {
2596
+ id: 'nx',
2597
+ name: 'NX',
2598
+ version,
2599
+ configPath: exists(nxJsonPath) ? 'nx.json' : undefined,
2600
+ confidence: min(confidence, 100),
2601
+ detectedFrom: sources,
2602
+ workspaceLayout,
2603
+ };
2604
+ }
2605
+
2606
+ /**
2607
+ * Detect Turborepo in project.
2608
+ *
2609
+ * @param workspacePath - Workspace directory path
2610
+ * @param packageJson - Optional pre-loaded package.json
2611
+ * @returns Detection result or null if not detected
2612
+ */
2613
+ function turborepoDetector(workspacePath, packageJson) {
2614
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2615
+ const sources = [];
2616
+ let confidence = 0;
2617
+ let version;
2618
+ let configPath;
2619
+ const turboJsonPath = join$1(workspacePath, 'turbo.json');
2620
+ if (exists(turboJsonPath)) {
2621
+ confidence += 80;
2622
+ configPath = 'turbo.json';
2623
+ sources.push({ type: 'config-file', path: 'turbo.json' });
2624
+ }
2625
+ const deps = collectAllDependencies(pkg);
2626
+ if (deps['turbo']) {
2627
+ confidence += 15;
2628
+ version = parseVersionString(deps['turbo']);
2629
+ sources.push({ type: 'package.json', field: 'dependencies.turbo' });
2630
+ }
2631
+ const scripts = pkg?.scripts ?? {};
2632
+ if (values(scripts).some((s) => s?.includes('turbo'))) {
2633
+ confidence += 5;
2634
+ sources.push({ type: 'package.json', field: 'scripts (turbo commands)' });
2635
+ }
2636
+ if (confidence === 0) {
2637
+ return null;
2638
+ }
2639
+ return {
2640
+ id: 'turborepo',
2641
+ name: 'Turborepo',
2642
+ version,
2643
+ configPath,
2644
+ confidence: min(confidence, 100),
2645
+ detectedFrom: sources,
2646
+ };
2647
+ }
2648
+
2649
+ /**
2650
+ * Detect Lerna in project.
2651
+ *
2652
+ * @param workspacePath - Workspace directory path
2653
+ * @param packageJson - Optional pre-loaded package.json
2654
+ * @returns Detection result or null if not detected
2655
+ */
2656
+ function lernaDetector(workspacePath, packageJson) {
2657
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2658
+ const sources = [];
2659
+ let confidence = 0;
2660
+ let version;
2661
+ let configPath;
2662
+ const lernaJsonPath = join$1(workspacePath, 'lerna.json');
2663
+ if (exists(lernaJsonPath)) {
2664
+ confidence += 80;
2665
+ configPath = 'lerna.json';
2666
+ sources.push({ type: 'config-file', path: 'lerna.json' });
2667
+ }
2668
+ const deps = collectAllDependencies(pkg);
2669
+ if (deps['lerna']) {
2670
+ confidence += 15;
2671
+ version = parseVersionString(deps['lerna']);
2672
+ sources.push({ type: 'package.json', field: 'dependencies.lerna' });
2673
+ }
2674
+ if (exists(join$1(workspacePath, 'packages'))) {
2675
+ confidence += 5;
2676
+ sources.push({ type: 'directory', path: 'packages/' });
2677
+ }
2678
+ if (confidence === 0) {
2679
+ return null;
2680
+ }
2681
+ return {
2682
+ id: 'lerna',
2683
+ name: 'Lerna',
2684
+ version,
2685
+ configPath,
2686
+ confidence: min(confidence, 100),
2687
+ detectedFrom: sources,
2688
+ };
2689
+ }
2690
+
2691
+ /**
2692
+ * Detect Rush in project.
2693
+ *
2694
+ * @param workspacePath - Workspace directory path
2695
+ * @param packageJson - Optional pre-loaded package.json
2696
+ * @returns Detection result or null if not detected
2697
+ */
2698
+ function rushDetector(workspacePath, packageJson) {
2699
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2700
+ const sources = [];
2701
+ let confidence = 0;
2702
+ let version;
2703
+ let configPath;
2704
+ const rushJsonPath = join$1(workspacePath, 'rush.json');
2705
+ if (exists(rushJsonPath)) {
2706
+ confidence += 90;
2707
+ configPath = 'rush.json';
2708
+ sources.push({ type: 'config-file', path: 'rush.json' });
2709
+ }
2710
+ const deps = collectAllDependencies(pkg);
2711
+ if (deps['@microsoft/rush']) {
2712
+ confidence += 10;
2713
+ version = parseVersionString(deps['@microsoft/rush']);
2714
+ sources.push({ type: 'package.json', field: 'dependencies.@microsoft/rush' });
2715
+ }
2716
+ if (exists(join$1(workspacePath, 'common', 'config', 'rush'))) {
2717
+ confidence += 5;
2718
+ sources.push({ type: 'directory', path: 'common/config/rush/' });
2719
+ }
2720
+ if (confidence === 0) {
2721
+ return null;
2722
+ }
2723
+ return {
2724
+ id: 'rush',
2725
+ name: 'Rush',
2726
+ version,
2727
+ configPath,
2728
+ confidence: min(confidence, 100),
2729
+ detectedFrom: sources,
2730
+ };
2731
+ }
2732
+
2733
+ /**
2734
+ * Detect pnpm workspaces in project.
2735
+ *
2736
+ * @param workspacePath - Workspace directory path
2737
+ * @returns Detection result or null if not detected
2738
+ */
2739
+ function pnpmWorkspacesDetector(workspacePath) {
2740
+ const sources = [];
2741
+ let confidence = 0;
2742
+ let configPath;
2743
+ const pnpmWorkspacePath = join$1(workspacePath, 'pnpm-workspace.yaml');
2744
+ if (exists(pnpmWorkspacePath)) {
2745
+ confidence += 90;
2746
+ configPath = 'pnpm-workspace.yaml';
2747
+ sources.push({ type: 'config-file', path: 'pnpm-workspace.yaml' });
2748
+ }
2749
+ if (exists(join$1(workspacePath, 'pnpm-lock.yaml'))) {
2750
+ confidence += 10;
2751
+ sources.push({ type: 'lockfile', path: 'pnpm-lock.yaml' });
2752
+ }
2753
+ if (confidence === 0) {
2754
+ return null;
2755
+ }
2756
+ return {
2757
+ id: 'pnpm-workspaces',
2758
+ name: 'pnpm Workspaces',
2759
+ configPath,
2760
+ confidence: min(confidence, 100),
2761
+ detectedFrom: sources,
2762
+ };
2763
+ }
2764
+
2765
+ /**
2766
+ * Detect npm workspaces in project.
2767
+ *
2768
+ * @param workspacePath - Workspace directory path
2769
+ * @param packageJson - Optional pre-loaded package.json
2770
+ * @returns Detection result or null if not detected
2771
+ */
2772
+ function npmWorkspacesDetector(workspacePath, packageJson) {
2773
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2774
+ const sources = [];
2775
+ let confidence = 0;
2776
+ if (pkg?.workspaces) {
2777
+ confidence += 80;
2778
+ sources.push({ type: 'package.json', field: 'workspaces' });
2779
+ }
2780
+ if (exists(join$1(workspacePath, 'package-lock.json'))) {
2781
+ confidence += 10;
2782
+ sources.push({ type: 'lockfile', path: 'package-lock.json' });
2783
+ }
2784
+ if (exists(join$1(workspacePath, 'yarn.lock'))) {
2785
+ return null; // Let yarn workspace detector handle this
2786
+ }
2787
+ if (confidence === 0) {
2788
+ return null;
2789
+ }
2790
+ return {
2791
+ id: 'npm-workspaces',
2792
+ name: 'npm Workspaces',
2793
+ configPath: 'package.json',
2794
+ confidence: min(confidence, 100),
2795
+ detectedFrom: sources,
2796
+ };
2797
+ }
2798
+
2799
+ /**
2800
+ * Detect yarn workspaces in project.
2801
+ *
2802
+ * @param workspacePath - Workspace directory path
2803
+ * @param packageJson - Optional pre-loaded package.json
2804
+ * @returns Detection result or null if not detected
2805
+ */
2806
+ function yarnWorkspacesDetector(workspacePath, packageJson) {
2807
+ const pkg = packageJson ?? readPackageJsonIfExists(workspacePath);
2808
+ const sources = [];
2809
+ let confidence = 0;
2810
+ if (pkg?.workspaces) {
2811
+ confidence += 70;
2812
+ sources.push({ type: 'package.json', field: 'workspaces' });
2813
+ }
2814
+ if (exists(join$1(workspacePath, 'yarn.lock'))) {
2815
+ confidence += 20;
2816
+ sources.push({ type: 'lockfile', path: 'yarn.lock' });
2817
+ }
2818
+ if (exists(join$1(workspacePath, '.yarnrc.yml'))) {
2819
+ confidence += 10;
2820
+ sources.push({ type: 'config-file', path: '.yarnrc.yml' });
2821
+ }
2822
+ if (confidence === 0 || !pkg?.workspaces) {
2823
+ return null;
2824
+ }
2825
+ return {
2826
+ id: 'yarn-workspaces',
2827
+ name: 'Yarn Workspaces',
2828
+ configPath: 'package.json',
2829
+ confidence: min(confidence, 100),
2830
+ detectedFrom: sources,
2831
+ };
2832
+ }
2833
+
2834
+ /** All monorepo detectors */
2835
+ const monorepoDetectors = [
2836
+ { id: 'nx', name: 'NX', detect: nxDetector },
2837
+ { id: 'turborepo', name: 'Turborepo', detect: turborepoDetector },
2838
+ { id: 'lerna', name: 'Lerna', detect: lernaDetector },
2839
+ { id: 'rush', name: 'Rush', detect: rushDetector },
2840
+ { id: 'pnpm-workspaces', name: 'pnpm Workspaces', detect: pnpmWorkspacesDetector },
2841
+ { id: 'npm-workspaces', name: 'npm Workspaces', detect: npmWorkspacesDetector },
2842
+ { id: 'yarn-workspaces', name: 'Yarn Workspaces', detect: yarnWorkspacesDetector },
2843
+ ];
2844
+
2845
+ /** Config patterns for Jest */
2846
+ const JEST_CONFIG_PATTERNS = ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json'];
2847
+ /**
2848
+ * Detect Jest in project.
2849
+ *
2850
+ * @param projectPath - Project directory path
2851
+ * @param packageJson - Optional pre-loaded package.json
2852
+ * @returns Detection result or null if not detected
2853
+ */
2854
+ function jestDetector(projectPath, packageJson) {
2855
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2856
+ const sources = [];
2857
+ let confidence = 0;
2858
+ let version;
2859
+ const deps = collectAllDependencies(pkg);
2860
+ if (deps['jest']) {
2861
+ confidence += 60;
2862
+ version = parseVersionString(deps['jest']);
2863
+ sources.push({ type: 'package.json', field: 'dependencies.jest' });
2864
+ }
2865
+ const configPath = locateConfigFile(projectPath, JEST_CONFIG_PATTERNS);
2866
+ if (configPath) {
2867
+ confidence += 30;
2868
+ sources.push({ type: 'config-file', path: configPath });
2869
+ }
2870
+ if (pkg && 'jest' in pkg) {
2871
+ confidence += 20;
2872
+ sources.push({ type: 'package.json', field: 'jest' });
2873
+ }
2874
+ const testScript = pkg?.scripts?.['test'] ?? '';
2875
+ if (testScript.includes('jest')) {
2876
+ confidence += 10;
2877
+ sources.push({ type: 'package.json', field: 'scripts.test' });
2878
+ }
2879
+ if (deps['@types/jest']) {
2880
+ confidence += 5;
2881
+ sources.push({ type: 'package.json', field: 'dependencies.@types/jest' });
2882
+ }
2883
+ if (deps['ts-jest']) {
2884
+ confidence += 5;
2885
+ sources.push({ type: 'package.json', field: 'dependencies.ts-jest' });
2886
+ }
2887
+ if (confidence === 0) {
2888
+ return null;
2889
+ }
2890
+ return {
2891
+ id: 'jest',
2892
+ name: 'Jest',
2893
+ type: 'unit',
2894
+ version,
2895
+ configPath,
2896
+ confidence: min(confidence, 100),
2897
+ detectedFrom: sources,
2898
+ };
2899
+ }
2900
+
2901
+ /** Config patterns for Vitest */
2902
+ const VITEST_CONFIG_PATTERNS = ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'];
2903
+ /**
2904
+ * Detect Vitest in project.
2905
+ *
2906
+ * @param projectPath - Project directory path
2907
+ * @param packageJson - Optional pre-loaded package.json
2908
+ * @returns Detection result or null if not detected
2909
+ */
2910
+ function vitestDetector(projectPath, packageJson) {
2911
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2912
+ const sources = [];
2913
+ let confidence = 0;
2914
+ let version;
2915
+ const deps = collectAllDependencies(pkg);
2916
+ if (deps['vitest']) {
2917
+ confidence += 70;
2918
+ version = parseVersionString(deps['vitest']);
2919
+ sources.push({ type: 'package.json', field: 'dependencies.vitest' });
2920
+ }
2921
+ const configPath = locateConfigFile(projectPath, VITEST_CONFIG_PATTERNS);
2922
+ if (configPath) {
2923
+ confidence += 25;
2924
+ sources.push({ type: 'config-file', path: configPath });
2925
+ }
2926
+ if (!configPath) {
2927
+ const viteConfig = exists(join$1(projectPath, 'vite.config.ts')) ||
2928
+ exists(join$1(projectPath, 'vite.config.js')) ||
2929
+ exists(join$1(projectPath, 'vite.config.mjs'));
2930
+ if (viteConfig && deps['vitest']) {
2931
+ confidence += 5;
2932
+ sources.push({ type: 'config-file', path: 'vite.config.*' });
2933
+ }
2934
+ }
2935
+ const testScript = pkg?.scripts?.['test'] ?? '';
2936
+ if (testScript.includes('vitest')) {
2937
+ confidence += 10;
2938
+ sources.push({ type: 'package.json', field: 'scripts.test' });
2939
+ }
2940
+ if (confidence === 0) {
2941
+ return null;
2942
+ }
2943
+ return {
2944
+ id: 'vitest',
2945
+ name: 'Vitest',
2946
+ type: 'unit',
2947
+ version,
2948
+ configPath,
2949
+ confidence: min(confidence, 100),
2950
+ detectedFrom: sources,
2951
+ };
2952
+ }
2953
+
2954
+ /** Config patterns for Mocha */
2955
+ const MOCHA_CONFIG_PATTERNS = ['.mocharc.js', '.mocharc.json', '.mocharc.yaml', '.mocharc.yml', 'mocha.opts'];
2956
+ /**
2957
+ * Detect Mocha in project.
2958
+ *
2959
+ * @param projectPath - Project directory path
2960
+ * @param packageJson - Optional pre-loaded package.json
2961
+ * @returns Detection result or null if not detected
2962
+ */
2963
+ function mochaDetector(projectPath, packageJson) {
2964
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
2965
+ const sources = [];
2966
+ let confidence = 0;
2967
+ let version;
2968
+ const deps = collectAllDependencies(pkg);
2969
+ if (deps['mocha']) {
2970
+ confidence += 65;
2971
+ version = parseVersionString(deps['mocha']);
2972
+ sources.push({ type: 'package.json', field: 'dependencies.mocha' });
2973
+ }
2974
+ const configPath = locateConfigFile(projectPath, MOCHA_CONFIG_PATTERNS);
2975
+ if (configPath) {
2976
+ confidence += 30;
2977
+ sources.push({ type: 'config-file', path: configPath });
2978
+ }
2979
+ if (deps['@types/mocha']) {
2980
+ confidence += 5;
2981
+ sources.push({ type: 'package.json', field: 'dependencies.@types/mocha' });
2982
+ }
2983
+ if (deps['chai']) {
2984
+ confidence += 5;
2985
+ sources.push({ type: 'package.json', field: 'dependencies.chai' });
2986
+ }
2987
+ const testScript = pkg?.scripts?.['test'] ?? '';
2988
+ if (testScript.includes('mocha')) {
2989
+ confidence += 10;
2990
+ sources.push({ type: 'package.json', field: 'scripts.test' });
2991
+ }
2992
+ if (confidence === 0) {
2993
+ return null;
2994
+ }
2995
+ return {
2996
+ id: 'mocha',
2997
+ name: 'Mocha',
2998
+ type: 'unit',
2999
+ version,
3000
+ configPath,
3001
+ confidence: min(confidence, 100),
3002
+ detectedFrom: sources,
3003
+ };
3004
+ }
3005
+
3006
+ /** Config patterns for Cypress */
3007
+ const CYPRESS_CONFIG_PATTERNS = ['cypress.config.js', 'cypress.config.ts', 'cypress.config.mjs', 'cypress.json'];
3008
+ /**
3009
+ * Detect Cypress in project.
3010
+ *
3011
+ * @param projectPath - Project directory path
3012
+ * @param packageJson - Optional pre-loaded package.json
3013
+ * @returns Detection result or null if not detected
3014
+ */
3015
+ function cypressDetector(projectPath, packageJson) {
3016
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
3017
+ const sources = [];
3018
+ let confidence = 0;
3019
+ let version;
3020
+ const deps = collectAllDependencies(pkg);
3021
+ if (deps['cypress']) {
3022
+ confidence += 60;
3023
+ version = parseVersionString(deps['cypress']);
3024
+ sources.push({ type: 'package.json', field: 'dependencies.cypress' });
3025
+ }
3026
+ const configPath = locateConfigFile(projectPath, CYPRESS_CONFIG_PATTERNS);
3027
+ if (configPath) {
3028
+ confidence += 30;
3029
+ sources.push({ type: 'config-file', path: configPath });
3030
+ }
3031
+ if (exists(join$1(projectPath, 'cypress'))) {
3032
+ confidence += 10;
3033
+ sources.push({ type: 'directory', path: 'cypress/' });
3034
+ }
3035
+ const e2eScript = pkg?.scripts?.['e2e'] ?? pkg?.scripts?.['test:e2e'] ?? '';
3036
+ if (e2eScript.includes('cypress')) {
3037
+ confidence += 5;
3038
+ sources.push({ type: 'package.json', field: 'scripts.e2e or scripts.test:e2e' });
3039
+ }
3040
+ if (confidence === 0) {
3041
+ return null;
3042
+ }
3043
+ return {
3044
+ id: 'cypress',
3045
+ name: 'Cypress',
3046
+ type: 'e2e',
3047
+ version,
3048
+ configPath,
3049
+ confidence: min(confidence, 100),
3050
+ detectedFrom: sources,
3051
+ };
3052
+ }
3053
+
3054
+ /** Config patterns for Playwright */
3055
+ const PLAYWRIGHT_CONFIG_PATTERNS = ['playwright.config.js', 'playwright.config.ts', 'playwright.config.mjs'];
3056
+ /**
3057
+ * Detect Playwright in project.
3058
+ *
3059
+ * @param projectPath - Project directory path
3060
+ * @param packageJson - Optional pre-loaded package.json
3061
+ * @returns Detection result or null if not detected
3062
+ */
3063
+ function playwrightDetector(projectPath, packageJson) {
3064
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
3065
+ const sources = [];
3066
+ let confidence = 0;
3067
+ let version;
3068
+ const deps = collectAllDependencies(pkg);
3069
+ if (deps['@playwright/test']) {
3070
+ confidence += 70;
3071
+ version = parseVersionString(deps['@playwright/test']);
3072
+ sources.push({ type: 'package.json', field: 'dependencies.@playwright/test' });
3073
+ }
3074
+ if (deps['playwright']) {
3075
+ confidence += 50;
3076
+ version = version ?? parseVersionString(deps['playwright']);
3077
+ sources.push({ type: 'package.json', field: 'dependencies.playwright' });
3078
+ }
3079
+ const configPath = locateConfigFile(projectPath, PLAYWRIGHT_CONFIG_PATTERNS);
3080
+ if (configPath) {
3081
+ confidence += 25;
3082
+ sources.push({ type: 'config-file', path: configPath });
3083
+ }
3084
+ if (exists(join$1(projectPath, 'e2e')) || exists(join$1(projectPath, 'tests'))) {
3085
+ confidence += 5;
3086
+ sources.push({ type: 'directory', path: 'e2e/ or tests/' });
3087
+ }
3088
+ const e2eScript = pkg?.scripts?.['e2e'] ?? pkg?.scripts?.['test:e2e'] ?? '';
3089
+ if (e2eScript.includes('playwright')) {
3090
+ confidence += 5;
3091
+ sources.push({ type: 'package.json', field: 'scripts.e2e or scripts.test:e2e' });
3092
+ }
3093
+ if (confidence === 0) {
3094
+ return null;
3095
+ }
3096
+ return {
3097
+ id: 'playwright',
3098
+ name: 'Playwright',
3099
+ type: 'e2e',
3100
+ version,
3101
+ configPath,
3102
+ confidence: min(confidence, 100),
3103
+ detectedFrom: sources,
3104
+ };
3105
+ }
3106
+
3107
+ /** All testing framework detectors */
3108
+ const testingDetectors = [
3109
+ { id: 'jest', name: 'Jest', testType: 'unit', detect: jestDetector },
3110
+ { id: 'vitest', name: 'Vitest', testType: 'unit', detect: vitestDetector },
3111
+ { id: 'mocha', name: 'Mocha', testType: 'unit', detect: mochaDetector },
3112
+ { id: 'cypress', name: 'Cypress', testType: 'e2e', detect: cypressDetector },
3113
+ { id: 'playwright', name: 'Playwright', testType: 'e2e', detect: playwrightDetector },
3114
+ ];
3115
+
3116
+ /**
3117
+ * Check if tsconfig has strict mode enabled.
3118
+ *
3119
+ * @param projectPath - The project directory path
3120
+ * @returns True if strict mode is enabled, undefined if unable to determine
3121
+ */
3122
+ function checkTsConfigStrict(projectPath) {
3123
+ const tsconfigPath = join$1(projectPath, 'tsconfig.json');
3124
+ const content = readFileIfExists(tsconfigPath);
3125
+ if (!content)
3126
+ return undefined;
3127
+ try {
3128
+ // Simple JSON parsing - doesn't handle comments but good enough for strict check
3129
+ const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
3130
+ const parsed = parse(cleanContent);
3131
+ return parsed?.compilerOptions?.strict === true;
3132
+ }
3133
+ catch {
3134
+ return undefined;
3135
+ }
3136
+ }
3137
+ /**
3138
+ * Detect TypeScript in project.
3139
+ *
3140
+ * @param projectPath - Project directory path
3141
+ * @param packageJson - Optional pre-loaded package.json
3142
+ * @returns Detection result or null if not detected
3143
+ */
3144
+ function typescriptDetector(projectPath, packageJson) {
3145
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
3146
+ const sources = [];
3147
+ let confidence = 0;
3148
+ let configPath;
3149
+ let version;
3150
+ const deps = collectAllDependencies(pkg);
3151
+ // TypeScript package
3152
+ if (deps['typescript']) {
3153
+ confidence += 50;
3154
+ version = parseVersionString(deps['typescript']);
3155
+ sources.push({ type: 'package.json', field: 'dependencies.typescript' });
3156
+ }
3157
+ // tsconfig.json
3158
+ if (exists(join$1(projectPath, 'tsconfig.json'))) {
3159
+ confidence += 40;
3160
+ configPath = 'tsconfig.json';
3161
+ sources.push({ type: 'config-file', path: 'tsconfig.json' });
3162
+ }
3163
+ // tsconfig.*.json variants
3164
+ const tsconfigVariants = ['tsconfig.build.json', 'tsconfig.lib.json', 'tsconfig.spec.json', 'tsconfig.app.json'];
3165
+ for (const variant of tsconfigVariants) {
3166
+ if (exists(join$1(projectPath, variant))) {
3167
+ confidence += 5;
3168
+ sources.push({ type: 'config-file', path: variant });
3169
+ break;
3170
+ }
3171
+ }
3172
+ // @types packages
3173
+ const typePackages = keys(deps).filter((d) => d.startsWith('@types/'));
3174
+ if (typePackages.length > 0) {
3175
+ confidence += 10;
3176
+ sources.push({ type: 'package.json', field: '@types/* packages' });
3177
+ }
3178
+ if (confidence === 0) {
3179
+ return null;
3180
+ }
3181
+ const strictMode = checkTsConfigStrict(projectPath);
3182
+ return {
3183
+ id: 'typescript',
3184
+ name: 'TypeScript',
3185
+ version,
3186
+ configPath,
3187
+ strictMode,
3188
+ confidence: min(confidence, 100),
3189
+ detectedFrom: sources,
3190
+ };
3191
+ }
3192
+ /**
3193
+ * Detect Flow in project.
3194
+ *
3195
+ * @param projectPath - Project directory path
3196
+ * @param packageJson - Optional pre-loaded package.json
3197
+ * @returns Detection result or null if not detected
3198
+ */
3199
+ function flowDetector(projectPath, packageJson) {
3200
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
3201
+ const sources = [];
3202
+ let confidence = 0;
3203
+ let configPath;
3204
+ let version;
3205
+ const deps = collectAllDependencies(pkg);
3206
+ // flow-bin package
3207
+ if (deps['flow-bin']) {
3208
+ confidence += 60;
3209
+ version = parseVersionString(deps['flow-bin']);
3210
+ sources.push({ type: 'package.json', field: 'dependencies.flow-bin' });
3211
+ }
3212
+ // .flowconfig
3213
+ if (exists(join$1(projectPath, '.flowconfig'))) {
3214
+ confidence += 40;
3215
+ configPath = '.flowconfig';
3216
+ sources.push({ type: 'config-file', path: '.flowconfig' });
3217
+ }
3218
+ // flow-typed directory
3219
+ if (exists(join$1(projectPath, 'flow-typed'))) {
3220
+ confidence += 10;
3221
+ sources.push({ type: 'directory', path: 'flow-typed/' });
3222
+ }
3223
+ // @babel/preset-flow
3224
+ if (deps['@babel/preset-flow']) {
3225
+ confidence += 10;
3226
+ sources.push({ type: 'package.json', field: 'dependencies.@babel/preset-flow' });
3227
+ }
3228
+ if (confidence === 0) {
3229
+ return null;
3230
+ }
3231
+ return {
3232
+ id: 'flow',
3233
+ name: 'Flow',
3234
+ version,
3235
+ configPath,
3236
+ confidence: min(confidence, 100),
3237
+ detectedFrom: sources,
3238
+ };
3239
+ }
3240
+ /**
3241
+ * Check if a file contains JSDoc type annotations.
3242
+ *
3243
+ * @param content - The file content to check.
3244
+ * @returns `true` if the content contains JSDoc type annotations.
3245
+ */
3246
+ function hasJsDocTypes(content) {
3247
+ // Check for JSDoc type annotations
3248
+ return (content.includes('@type {') ||
3249
+ content.includes('@param {') ||
3250
+ content.includes('@returns {') ||
3251
+ content.includes('@typedef') ||
3252
+ content.includes('@template'));
3253
+ }
3254
+ /**
3255
+ * Detect JSDoc type annotations in project.
3256
+ *
3257
+ * @param projectPath - Project directory path
3258
+ * @param packageJson - Optional pre-loaded package.json
3259
+ * @returns Detection result or null if not detected
3260
+ */
3261
+ function jsdocDetector(projectPath, packageJson) {
3262
+ const pkg = packageJson ?? readPackageJsonIfExists(projectPath);
3263
+ const sources = [];
3264
+ let confidence = 0;
3265
+ const deps = collectAllDependencies(pkg);
3266
+ // jsdoc package
3267
+ if (deps['jsdoc']) {
3268
+ confidence += 30;
3269
+ sources.push({ type: 'package.json', field: 'dependencies.jsdoc' });
3270
+ }
3271
+ // typescript with checkJs (JSDoc type checking)
3272
+ if (deps['typescript']) {
3273
+ // Check if checkJs is enabled in tsconfig
3274
+ const tsconfigPath = join$1(projectPath, 'tsconfig.json');
3275
+ const content = readFileIfExists(tsconfigPath);
3276
+ if (content) {
3277
+ try {
3278
+ const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
3279
+ const parsed = parse(cleanContent);
3280
+ if (parsed?.compilerOptions?.checkJs === true || parsed?.compilerOptions?.allowJs === true) {
3281
+ confidence += 30;
3282
+ sources.push({ type: 'config-file', path: 'tsconfig.json (checkJs/allowJs)' });
3283
+ }
3284
+ }
3285
+ catch {
3286
+ // Ignore parsing errors
3287
+ }
3288
+ }
3289
+ }
3290
+ // Check for jsconfig.json (VS Code JS type checking)
3291
+ if (exists(join$1(projectPath, 'jsconfig.json'))) {
3292
+ confidence += 40;
3293
+ sources.push({ type: 'config-file', path: 'jsconfig.json' });
3294
+ }
3295
+ // Sample check for JSDoc annotations in source files
3296
+ const srcDir = join$1(projectPath, 'src');
3297
+ if (exists(srcDir)) {
3298
+ try {
3299
+ const entries = readDirectory(srcDir);
3300
+ const files = entries.filter((e) => e.isFile && (e.name.endsWith('.js') || e.name.endsWith('.mjs'))).map((e) => e.name);
3301
+ for (const file of files.slice(0, 3)) {
3302
+ const content = readFileIfExists(join$1(srcDir, file));
3303
+ if (content && hasJsDocTypes(content)) {
3304
+ confidence += 20;
3305
+ sources.push({ type: 'directory', path: `src/${file} (JSDoc annotations)` });
3306
+ break;
3307
+ }
3308
+ }
3309
+ }
3310
+ catch {
3311
+ // Ignore directory read errors
3312
+ }
3313
+ }
3314
+ if (confidence === 0) {
3315
+ return null;
3316
+ }
3317
+ return {
3318
+ id: 'jsdoc',
3319
+ name: 'JSDoc',
3320
+ confidence: min(confidence, 100),
3321
+ detectedFrom: sources,
3322
+ };
3323
+ }
3324
+ /** All type system detectors */
3325
+ const typeSystemDetectors = [
3326
+ { id: 'typescript', name: 'TypeScript', detect: typescriptDetector },
3327
+ { id: 'flow', name: 'Flow', detect: flowDetector },
3328
+ { id: 'jsdoc', name: 'JSDoc', detect: jsdocDetector },
3329
+ ];
3330
+
3331
+ const techLogger = createScopedLogger('project-scope:tech');
3332
+ /**
3333
+ * Cache for tech detection results.
3334
+ * TTL: 60 seconds (tech stack can change during active development)
3335
+ */
3336
+ const detectAllCache = createCache({ ttl: 60000, maxSize: 50 });
3337
+ /**
3338
+ * Check if the value is a DetectAllOptions object.
3339
+ *
3340
+ * @param value - Value to check
3341
+ * @returns True if value is DetectAllOptions
3342
+ */
3343
+ function isDetectAllOptions(value) {
3344
+ if (typeof value !== 'object' || value === null)
3345
+ return false;
3346
+ // DetectAllOptions has skipCache or packageJson fields specifically
3347
+ // PackageJson never has skipCache field
3348
+ return 'skipCache' in value || 'packageJson' in value;
3349
+ }
3350
+ /**
3351
+ * Run all technology detectors on a project.
3352
+ *
3353
+ * Results are cached for 60 seconds per project path to avoid
3354
+ * redundant file system operations on repeated calls.
3355
+ *
3356
+ * @param projectPath - Path to project directory
3357
+ * @param packageJsonOrOptions - Optional pre-loaded package.json or options object
3358
+ * @returns All detection results organized by category
3359
+ *
3360
+ * @example
3361
+ * ```typescript
3362
+ * import { detectAll } from '@hyperfrontend/project-scope'
3363
+ *
3364
+ * const detections = detectAll('./my-project')
3365
+ *
3366
+ * // Check frontend frameworks
3367
+ * for (const fw of detections.frontendFrameworks) {
3368
+ * console.log(`${fw.name} v${fw.version} (${fw.confidence}% confidence)`)
3369
+ * }
3370
+ *
3371
+ * // Check build tools
3372
+ * console.log('Build tools:', detections.buildTools.map(t => t.name))
3373
+ *
3374
+ * // Check testing frameworks
3375
+ * console.log('Testing:', detections.testingFrameworks.map(t => t.name))
3376
+ * ```
3377
+ */
3378
+ function detectAll(projectPath, packageJsonOrOptions) {
3379
+ // Handle backward-compatible arguments
3380
+ const options = isDetectAllOptions(packageJsonOrOptions) ? packageJsonOrOptions : { packageJson: packageJsonOrOptions };
3381
+ // Check cache first (unless skipCache is true)
3382
+ if (!options.skipCache) {
3383
+ const cached = detectAllCache.get(projectPath);
3384
+ if (cached) {
3385
+ techLogger.debug('Returning cached tech detection results', { projectPath });
3386
+ return cached;
3387
+ }
3388
+ }
3389
+ const pkg = options.packageJson ?? readPackageJsonIfExists(projectPath);
3390
+ techLogger.debug('Running all tech detectors', { projectPath });
3391
+ const result = {
3392
+ buildTools: buildToolDetectors
3393
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3394
+ .filter((r) => r !== null)
3395
+ .sort((a, b) => b.confidence - a.confidence),
3396
+ monorepo: monorepoDetectors
3397
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3398
+ .filter((r) => r !== null)
3399
+ .sort((a, b) => b.confidence - a.confidence),
3400
+ frontendFrameworks: frameworkDetectors
3401
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3402
+ .filter((r) => r !== null)
3403
+ .sort((a, b) => b.confidence - a.confidence),
3404
+ backendFrameworks: backendDetectors
3405
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3406
+ .filter((r) => r !== null)
3407
+ .sort((a, b) => b.confidence - a.confidence),
3408
+ legacyFrameworks: legacyDetectors
3409
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3410
+ .filter((r) => r !== null)
3411
+ .sort((a, b) => b.confidence - a.confidence),
3412
+ testingFrameworks: testingDetectors
3413
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3414
+ .filter((r) => r !== null)
3415
+ .sort((a, b) => b.confidence - a.confidence),
3416
+ typeSystem: typeSystemDetectors
3417
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3418
+ .filter((r) => r !== null)
3419
+ .sort((a, b) => b.confidence - a.confidence),
3420
+ linting: lintingDetectors
3421
+ .map((d) => d.detect(projectPath, pkg ?? undefined))
3422
+ .filter((r) => r !== null)
3423
+ .sort((a, b) => b.confidence - a.confidence),
3424
+ };
3425
+ techLogger.debug('Tech detection complete', {
3426
+ buildTools: result.buildTools.map((t) => t.id),
3427
+ frontendFrameworks: result.frontendFrameworks.map((f) => f.id),
3428
+ backendFrameworks: result.backendFrameworks.map((f) => f.id),
3429
+ legacyFrameworks: result.legacyFrameworks.map((f) => f.id),
3430
+ testingFrameworks: result.testingFrameworks.map((f) => f.id),
3431
+ });
3432
+ // Cache the result
3433
+ detectAllCache.set(projectPath, result);
3434
+ return result;
3435
+ }
3436
+
3437
+ const frameworkLogger = createScopedLogger('project-scope:heuristics:framework');
3438
+ /**
3439
+ * Cache for framework identification results.
3440
+ * TTL: 60 seconds (frameworks are stable but can change during development)
3441
+ */
3442
+ const frameworkIdCache = createCache({ ttl: 60000, maxSize: 50 });
3443
+ /**
3444
+ * Build a human-readable summary string from detected frameworks.
3445
+ *
3446
+ * @param frontend - Frontend framework names
3447
+ * @param backend - Backend framework names
3448
+ * @param testing - Testing framework names
3449
+ * @returns Human-readable summary
3450
+ */
3451
+ function buildSummary(frontend, backend, testing) {
3452
+ const parts = [];
3453
+ if (frontend.length > 0) {
3454
+ parts.push(frontend.join(' + '));
3455
+ }
3456
+ if (backend.length > 0) {
3457
+ if (parts.length > 0) {
3458
+ parts.push('with');
3459
+ }
3460
+ parts.push(backend.join(' + '));
3461
+ }
3462
+ if (testing.length > 0) {
3463
+ if (parts.length > 0) {
3464
+ parts.push('and');
3465
+ }
3466
+ parts.push(testing.join('/'));
3467
+ }
3468
+ return parts.length > 0 ? parts.join(' ') : 'No frameworks detected';
3469
+ }
3470
+ /**
3471
+ * Identify all frameworks in project.
3472
+ *
3473
+ * Runs all technology detectors and aggregates results into a comprehensive
3474
+ * framework identification with confidence scoring.
3475
+ *
3476
+ * @param projectPath - Project directory path
3477
+ * @param options - Identification options
3478
+ * @returns Framework identification result
3479
+ *
3480
+ * @example
3481
+ * ```typescript
3482
+ * import { identifyFrameworks } from '@hyperfrontend/project-scope'
3483
+ *
3484
+ * const result = identifyFrameworks('./my-react-app')
3485
+ * console.log(result.summary) // 'React + Next.js with Jest'
3486
+ * console.log(result.primary?.name) // 'React'
3487
+ * console.log(result.stack.frontend) // ['react', 'nextjs']
3488
+ * console.log(result.stack.testing) // ['jest']
3489
+ * ```
3490
+ */
3491
+ function identifyFrameworks(projectPath, options) {
3492
+ frameworkLogger.debug('Identifying frameworks', { projectPath, minConfidence: options?.minConfidence ?? 10 });
3493
+ const minConfidence = options?.minConfidence ?? 10;
3494
+ const cacheKey = `${projectPath}:${minConfidence}`;
3495
+ if (!options?.skipCache) {
3496
+ const cached = frameworkIdCache.get(cacheKey);
3497
+ if (cached) {
3498
+ frameworkLogger.debug('Returning cached framework identification', { projectPath });
3499
+ return cached;
3500
+ }
3501
+ }
3502
+ const packageJson = readPackageJsonIfExists(projectPath);
3503
+ const detections = detectAll(projectPath, packageJson ?? undefined);
3504
+ const frontendFrameworks = detections.frontendFrameworks
3505
+ .filter((d) => d.confidence >= minConfidence)
3506
+ .map((d) => ({
3507
+ id: d.id,
3508
+ name: d.name,
3509
+ version: d.version,
3510
+ confidence: d.confidence,
3511
+ category: d.category === 'meta-framework' ? 'frontend' : 'frontend',
3512
+ metaFrameworks: d.metaFrameworks?.map((m) => m.id),
3513
+ }));
3514
+ const metaFrameworks = [];
3515
+ for (const detection of detections.frontendFrameworks) {
3516
+ if (detection.category === 'meta-framework' && detection.confidence >= minConfidence) {
3517
+ metaFrameworks.push({
3518
+ id: detection.id,
3519
+ name: detection.name,
3520
+ version: detection.version,
3521
+ confidence: detection.confidence,
3522
+ category: 'frontend',
3523
+ });
3524
+ }
3525
+ if (detection.metaFrameworks) {
3526
+ for (const meta of detection.metaFrameworks) {
3527
+ if (meta.confidence >= minConfidence && !metaFrameworks.some((m) => m.id === meta.id)) {
3528
+ metaFrameworks.push({
3529
+ id: meta.id,
3530
+ name: meta.name,
3531
+ version: meta.version,
3532
+ confidence: meta.confidence,
3533
+ category: 'frontend',
3534
+ });
3535
+ }
3536
+ }
3537
+ }
3538
+ }
3539
+ const backendFrameworks = detections.backendFrameworks
3540
+ .filter((d) => d.confidence >= minConfidence)
3541
+ .map((d) => ({
3542
+ id: d.id,
3543
+ name: d.name,
3544
+ version: d.version,
3545
+ confidence: d.confidence,
3546
+ category: 'backend',
3547
+ }));
3548
+ const testingFrameworks = detections.testingFrameworks
3549
+ .filter((d) => d.confidence >= minConfidence)
3550
+ .map((d) => ({
3551
+ id: d.id,
3552
+ name: d.name,
3553
+ version: d.version,
3554
+ configPath: d.configPath,
3555
+ type: d.type,
3556
+ confidence: d.confidence,
3557
+ }));
3558
+ const stack = {
3559
+ frontend: detections.frontendFrameworks.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3560
+ backend: detections.backendFrameworks.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3561
+ testing: detections.testingFrameworks.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3562
+ build: detections.buildTools.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3563
+ typeSystem: detections.typeSystem.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3564
+ linting: detections.linting.filter((d) => d.confidence >= minConfidence).map((d) => d.id),
3565
+ };
3566
+ const allFrameworks = [...frontendFrameworks, ...backendFrameworks];
3567
+ allFrameworks.sort((a, b) => b.confidence - a.confidence);
3568
+ const primary = allFrameworks[0];
3569
+ const summary = buildSummary(frontendFrameworks.map((f) => f.name), backendFrameworks.map((f) => f.name), testingFrameworks.map((f) => f.name));
3570
+ frameworkLogger.debug('Framework identification complete', {
3571
+ projectPath,
3572
+ primaryFramework: primary?.name ?? 'none',
3573
+ frontendCount: frontendFrameworks.length,
3574
+ backendCount: backendFrameworks.length,
3575
+ testingCount: testingFrameworks.length,
3576
+ metaFrameworkCount: metaFrameworks.length,
3577
+ summary,
3578
+ });
3579
+ const result = {
3580
+ primary,
3581
+ frontend: frontendFrameworks,
3582
+ backend: backendFrameworks,
3583
+ testing: testingFrameworks,
3584
+ metaFrameworks,
3585
+ summary,
3586
+ stack,
3587
+ };
3588
+ frameworkIdCache.set(cacheKey, result);
3589
+ return result;
3590
+ }
3591
+ /**
3592
+ * Clear the framework identification cache.
3593
+ *
3594
+ * Useful for testing or when the project files have changed.
3595
+ */
3596
+ function clearFrameworkIdentificationCache() {
3597
+ frameworkIdCache.clear();
3598
+ }
3599
+ /**
3600
+ * Check if a project uses a specific framework.
3601
+ *
3602
+ * @param projectPath - Project directory path
3603
+ * @param frameworkId - Framework identifier to check
3604
+ * @param minConfidence - Minimum confidence threshold (default: 50)
3605
+ * @returns True if the framework is detected with sufficient confidence
3606
+ */
3607
+ function usesFramework(projectPath, frameworkId, minConfidence = 50) {
3608
+ const identification = identifyFrameworks(projectPath, { minConfidence });
3609
+ const allFrameworks = [...identification.frontend, ...identification.backend, ...identification.metaFrameworks];
3610
+ return allFrameworks.some((f) => f.id === frameworkId && f.confidence >= minConfidence);
3611
+ }
3612
+
3613
+ export { clearFrameworkIdentificationCache, identifyFrameworks, usesFramework };
3614
+ //# sourceMappingURL=index.esm.js.map