@hyperfrontend/versioning 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 (353) hide show
  1. package/ARCHITECTURE.md +593 -0
  2. package/CHANGELOG.md +35 -0
  3. package/FUNDING.md +141 -0
  4. package/LICENSE.md +21 -0
  5. package/README.md +195 -0
  6. package/SECURITY.md +82 -0
  7. package/changelog/compare/diff.d.ts +128 -0
  8. package/changelog/compare/diff.d.ts.map +1 -0
  9. package/changelog/compare/index.cjs.js +628 -0
  10. package/changelog/compare/index.cjs.js.map +1 -0
  11. package/changelog/compare/index.d.ts +4 -0
  12. package/changelog/compare/index.d.ts.map +1 -0
  13. package/changelog/compare/index.esm.js +612 -0
  14. package/changelog/compare/index.esm.js.map +1 -0
  15. package/changelog/compare/is-equal.d.ts +114 -0
  16. package/changelog/compare/is-equal.d.ts.map +1 -0
  17. package/changelog/index.cjs.js +6448 -0
  18. package/changelog/index.cjs.js.map +1 -0
  19. package/changelog/index.d.ts +6 -0
  20. package/changelog/index.d.ts.map +1 -0
  21. package/changelog/index.esm.js +6358 -0
  22. package/changelog/index.esm.js.map +1 -0
  23. package/changelog/models/changelog.d.ts +86 -0
  24. package/changelog/models/changelog.d.ts.map +1 -0
  25. package/changelog/models/commit-ref.d.ts +51 -0
  26. package/changelog/models/commit-ref.d.ts.map +1 -0
  27. package/changelog/models/entry.d.ts +84 -0
  28. package/changelog/models/entry.d.ts.map +1 -0
  29. package/changelog/models/index.cjs.js +2043 -0
  30. package/changelog/models/index.cjs.js.map +1 -0
  31. package/changelog/models/index.d.ts +11 -0
  32. package/changelog/models/index.d.ts.map +1 -0
  33. package/changelog/models/index.esm.js +2026 -0
  34. package/changelog/models/index.esm.js.map +1 -0
  35. package/changelog/models/schema.d.ts +68 -0
  36. package/changelog/models/schema.d.ts.map +1 -0
  37. package/changelog/models/section.d.ts +25 -0
  38. package/changelog/models/section.d.ts.map +1 -0
  39. package/changelog/operations/add-entry.d.ts +56 -0
  40. package/changelog/operations/add-entry.d.ts.map +1 -0
  41. package/changelog/operations/add-item.d.ts +18 -0
  42. package/changelog/operations/add-item.d.ts.map +1 -0
  43. package/changelog/operations/filter-by-predicate.d.ts +81 -0
  44. package/changelog/operations/filter-by-predicate.d.ts.map +1 -0
  45. package/changelog/operations/filter-by-range.d.ts +63 -0
  46. package/changelog/operations/filter-by-range.d.ts.map +1 -0
  47. package/changelog/operations/filter-entries.d.ts +9 -0
  48. package/changelog/operations/filter-entries.d.ts.map +1 -0
  49. package/changelog/operations/index.cjs.js +2455 -0
  50. package/changelog/operations/index.cjs.js.map +1 -0
  51. package/changelog/operations/index.d.ts +15 -0
  52. package/changelog/operations/index.d.ts.map +1 -0
  53. package/changelog/operations/index.esm.js +2411 -0
  54. package/changelog/operations/index.esm.js.map +1 -0
  55. package/changelog/operations/merge.d.ts +88 -0
  56. package/changelog/operations/merge.d.ts.map +1 -0
  57. package/changelog/operations/remove-entry.d.ts +45 -0
  58. package/changelog/operations/remove-entry.d.ts.map +1 -0
  59. package/changelog/operations/remove-section.d.ts +50 -0
  60. package/changelog/operations/remove-section.d.ts.map +1 -0
  61. package/changelog/operations/transform.d.ts +143 -0
  62. package/changelog/operations/transform.d.ts.map +1 -0
  63. package/changelog/parse/index.cjs.js +1282 -0
  64. package/changelog/parse/index.cjs.js.map +1 -0
  65. package/changelog/parse/index.d.ts +5 -0
  66. package/changelog/parse/index.d.ts.map +1 -0
  67. package/changelog/parse/index.esm.js +1275 -0
  68. package/changelog/parse/index.esm.js.map +1 -0
  69. package/changelog/parse/line.d.ts +48 -0
  70. package/changelog/parse/line.d.ts.map +1 -0
  71. package/changelog/parse/parser.d.ts +16 -0
  72. package/changelog/parse/parser.d.ts.map +1 -0
  73. package/changelog/parse/tokenizer.d.ts +49 -0
  74. package/changelog/parse/tokenizer.d.ts.map +1 -0
  75. package/changelog/serialize/index.cjs.js +574 -0
  76. package/changelog/serialize/index.cjs.js.map +1 -0
  77. package/changelog/serialize/index.d.ts +6 -0
  78. package/changelog/serialize/index.d.ts.map +1 -0
  79. package/changelog/serialize/index.esm.js +564 -0
  80. package/changelog/serialize/index.esm.js.map +1 -0
  81. package/changelog/serialize/templates.d.ts +81 -0
  82. package/changelog/serialize/templates.d.ts.map +1 -0
  83. package/changelog/serialize/to-json.d.ts +57 -0
  84. package/changelog/serialize/to-json.d.ts.map +1 -0
  85. package/changelog/serialize/to-string.d.ts +30 -0
  86. package/changelog/serialize/to-string.d.ts.map +1 -0
  87. package/commits/index.cjs.js +648 -0
  88. package/commits/index.cjs.js.map +1 -0
  89. package/commits/index.d.ts +3 -0
  90. package/commits/index.d.ts.map +1 -0
  91. package/commits/index.esm.js +629 -0
  92. package/commits/index.esm.js.map +1 -0
  93. package/commits/models/breaking.d.ts +39 -0
  94. package/commits/models/breaking.d.ts.map +1 -0
  95. package/commits/models/commit-type.d.ts +32 -0
  96. package/commits/models/commit-type.d.ts.map +1 -0
  97. package/commits/models/conventional.d.ts +49 -0
  98. package/commits/models/conventional.d.ts.map +1 -0
  99. package/commits/models/index.cjs.js +207 -0
  100. package/commits/models/index.cjs.js.map +1 -0
  101. package/commits/models/index.d.ts +7 -0
  102. package/commits/models/index.d.ts.map +1 -0
  103. package/commits/models/index.esm.js +193 -0
  104. package/commits/models/index.esm.js.map +1 -0
  105. package/commits/parse/body.d.ts +18 -0
  106. package/commits/parse/body.d.ts.map +1 -0
  107. package/commits/parse/footer.d.ts +16 -0
  108. package/commits/parse/footer.d.ts.map +1 -0
  109. package/commits/parse/header.d.ts +15 -0
  110. package/commits/parse/header.d.ts.map +1 -0
  111. package/commits/parse/index.cjs.js +505 -0
  112. package/commits/parse/index.cjs.js.map +1 -0
  113. package/commits/parse/index.d.ts +5 -0
  114. package/commits/parse/index.d.ts.map +1 -0
  115. package/commits/parse/index.esm.js +499 -0
  116. package/commits/parse/index.esm.js.map +1 -0
  117. package/commits/parse/message.d.ts +17 -0
  118. package/commits/parse/message.d.ts.map +1 -0
  119. package/commits/utils/replace-char.d.ts +19 -0
  120. package/commits/utils/replace-char.d.ts.map +1 -0
  121. package/flow/executor/execute.d.ts +72 -0
  122. package/flow/executor/execute.d.ts.map +1 -0
  123. package/flow/executor/index.cjs.js +4402 -0
  124. package/flow/executor/index.cjs.js.map +1 -0
  125. package/flow/executor/index.d.ts +3 -0
  126. package/flow/executor/index.d.ts.map +1 -0
  127. package/flow/executor/index.esm.js +4398 -0
  128. package/flow/executor/index.esm.js.map +1 -0
  129. package/flow/factory.d.ts +58 -0
  130. package/flow/factory.d.ts.map +1 -0
  131. package/flow/index.cjs.js +8506 -0
  132. package/flow/index.cjs.js.map +1 -0
  133. package/flow/index.d.ts +7 -0
  134. package/flow/index.d.ts.map +1 -0
  135. package/flow/index.esm.js +8451 -0
  136. package/flow/index.esm.js.map +1 -0
  137. package/flow/models/flow.d.ts +130 -0
  138. package/flow/models/flow.d.ts.map +1 -0
  139. package/flow/models/index.cjs.js +285 -0
  140. package/flow/models/index.cjs.js.map +1 -0
  141. package/flow/models/index.d.ts +7 -0
  142. package/flow/models/index.d.ts.map +1 -0
  143. package/flow/models/index.esm.js +268 -0
  144. package/flow/models/index.esm.js.map +1 -0
  145. package/flow/models/step.d.ts +108 -0
  146. package/flow/models/step.d.ts.map +1 -0
  147. package/flow/models/types.d.ts +150 -0
  148. package/flow/models/types.d.ts.map +1 -0
  149. package/flow/presets/conventional.d.ts +59 -0
  150. package/flow/presets/conventional.d.ts.map +1 -0
  151. package/flow/presets/independent.d.ts +61 -0
  152. package/flow/presets/independent.d.ts.map +1 -0
  153. package/flow/presets/index.cjs.js +3903 -0
  154. package/flow/presets/index.cjs.js.map +1 -0
  155. package/flow/presets/index.d.ts +4 -0
  156. package/flow/presets/index.d.ts.map +1 -0
  157. package/flow/presets/index.esm.js +3889 -0
  158. package/flow/presets/index.esm.js.map +1 -0
  159. package/flow/presets/synced.d.ts +65 -0
  160. package/flow/presets/synced.d.ts.map +1 -0
  161. package/flow/steps/analyze-commits.d.ts +19 -0
  162. package/flow/steps/analyze-commits.d.ts.map +1 -0
  163. package/flow/steps/calculate-bump.d.ts +27 -0
  164. package/flow/steps/calculate-bump.d.ts.map +1 -0
  165. package/flow/steps/create-commit.d.ts +16 -0
  166. package/flow/steps/create-commit.d.ts.map +1 -0
  167. package/flow/steps/create-tag.d.ts +22 -0
  168. package/flow/steps/create-tag.d.ts.map +1 -0
  169. package/flow/steps/fetch-registry.d.ts +19 -0
  170. package/flow/steps/fetch-registry.d.ts.map +1 -0
  171. package/flow/steps/generate-changelog.d.ts +25 -0
  172. package/flow/steps/generate-changelog.d.ts.map +1 -0
  173. package/flow/steps/index.cjs.js +3523 -0
  174. package/flow/steps/index.cjs.js.map +1 -0
  175. package/flow/steps/index.d.ts +8 -0
  176. package/flow/steps/index.d.ts.map +1 -0
  177. package/flow/steps/index.esm.js +3504 -0
  178. package/flow/steps/index.esm.js.map +1 -0
  179. package/flow/steps/update-packages.d.ts +25 -0
  180. package/flow/steps/update-packages.d.ts.map +1 -0
  181. package/flow/utils/interpolate.d.ts +11 -0
  182. package/flow/utils/interpolate.d.ts.map +1 -0
  183. package/git/factory.d.ts +233 -0
  184. package/git/factory.d.ts.map +1 -0
  185. package/git/index.cjs.js +2863 -0
  186. package/git/index.cjs.js.map +1 -0
  187. package/git/index.d.ts +5 -0
  188. package/git/index.d.ts.map +1 -0
  189. package/git/index.esm.js +2785 -0
  190. package/git/index.esm.js.map +1 -0
  191. package/git/models/commit.d.ts +129 -0
  192. package/git/models/commit.d.ts.map +1 -0
  193. package/git/models/index.cjs.js +755 -0
  194. package/git/models/index.cjs.js.map +1 -0
  195. package/git/models/index.d.ts +7 -0
  196. package/git/models/index.d.ts.map +1 -0
  197. package/git/models/index.esm.js +729 -0
  198. package/git/models/index.esm.js.map +1 -0
  199. package/git/models/ref.d.ts +120 -0
  200. package/git/models/ref.d.ts.map +1 -0
  201. package/git/models/tag.d.ts +141 -0
  202. package/git/models/tag.d.ts.map +1 -0
  203. package/git/operations/commit.d.ts +97 -0
  204. package/git/operations/commit.d.ts.map +1 -0
  205. package/git/operations/head-info.d.ts +29 -0
  206. package/git/operations/head-info.d.ts.map +1 -0
  207. package/git/operations/index.cjs.js +1954 -0
  208. package/git/operations/index.cjs.js.map +1 -0
  209. package/git/operations/index.d.ts +14 -0
  210. package/git/operations/index.d.ts.map +1 -0
  211. package/git/operations/index.esm.js +1903 -0
  212. package/git/operations/index.esm.js.map +1 -0
  213. package/git/operations/log.d.ts +104 -0
  214. package/git/operations/log.d.ts.map +1 -0
  215. package/git/operations/manage-tags.d.ts +60 -0
  216. package/git/operations/manage-tags.d.ts.map +1 -0
  217. package/git/operations/query-tags.d.ts +88 -0
  218. package/git/operations/query-tags.d.ts.map +1 -0
  219. package/git/operations/stage.d.ts +66 -0
  220. package/git/operations/stage.d.ts.map +1 -0
  221. package/git/operations/status.d.ts +173 -0
  222. package/git/operations/status.d.ts.map +1 -0
  223. package/index.cjs.js +16761 -0
  224. package/index.cjs.js.map +1 -0
  225. package/index.d.ts +102 -0
  226. package/index.d.ts.map +1 -0
  227. package/index.esm.js +16427 -0
  228. package/index.esm.js.map +1 -0
  229. package/package.json +200 -0
  230. package/registry/factory.d.ts +18 -0
  231. package/registry/factory.d.ts.map +1 -0
  232. package/registry/index.cjs.js +543 -0
  233. package/registry/index.cjs.js.map +1 -0
  234. package/registry/index.d.ts +5 -0
  235. package/registry/index.d.ts.map +1 -0
  236. package/registry/index.esm.js +535 -0
  237. package/registry/index.esm.js.map +1 -0
  238. package/registry/models/index.cjs.js +69 -0
  239. package/registry/models/index.cjs.js.map +1 -0
  240. package/registry/models/index.d.ts +6 -0
  241. package/registry/models/index.d.ts.map +1 -0
  242. package/registry/models/index.esm.js +66 -0
  243. package/registry/models/index.esm.js.map +1 -0
  244. package/registry/models/package-info.d.ts +55 -0
  245. package/registry/models/package-info.d.ts.map +1 -0
  246. package/registry/models/registry.d.ts +62 -0
  247. package/registry/models/registry.d.ts.map +1 -0
  248. package/registry/models/version-info.d.ts +67 -0
  249. package/registry/models/version-info.d.ts.map +1 -0
  250. package/registry/npm/cache.d.ts +50 -0
  251. package/registry/npm/cache.d.ts.map +1 -0
  252. package/registry/npm/client.d.ts +30 -0
  253. package/registry/npm/client.d.ts.map +1 -0
  254. package/registry/npm/index.cjs.js +456 -0
  255. package/registry/npm/index.cjs.js.map +1 -0
  256. package/registry/npm/index.d.ts +4 -0
  257. package/registry/npm/index.d.ts.map +1 -0
  258. package/registry/npm/index.esm.js +451 -0
  259. package/registry/npm/index.esm.js.map +1 -0
  260. package/semver/compare/compare.d.ts +100 -0
  261. package/semver/compare/compare.d.ts.map +1 -0
  262. package/semver/compare/index.cjs.js +386 -0
  263. package/semver/compare/index.cjs.js.map +1 -0
  264. package/semver/compare/index.d.ts +3 -0
  265. package/semver/compare/index.d.ts.map +1 -0
  266. package/semver/compare/index.esm.js +370 -0
  267. package/semver/compare/index.esm.js.map +1 -0
  268. package/semver/compare/sort.d.ts +36 -0
  269. package/semver/compare/sort.d.ts.map +1 -0
  270. package/semver/format/index.cjs.js +58 -0
  271. package/semver/format/index.cjs.js.map +1 -0
  272. package/semver/format/index.d.ts +2 -0
  273. package/semver/format/index.d.ts.map +1 -0
  274. package/semver/format/index.esm.js +53 -0
  275. package/semver/format/index.esm.js.map +1 -0
  276. package/semver/format/to-string.d.ts +31 -0
  277. package/semver/format/to-string.d.ts.map +1 -0
  278. package/semver/increment/bump.d.ts +37 -0
  279. package/semver/increment/bump.d.ts.map +1 -0
  280. package/semver/increment/index.cjs.js +223 -0
  281. package/semver/increment/index.cjs.js.map +1 -0
  282. package/semver/increment/index.d.ts +2 -0
  283. package/semver/increment/index.d.ts.map +1 -0
  284. package/semver/increment/index.esm.js +219 -0
  285. package/semver/increment/index.esm.js.map +1 -0
  286. package/semver/index.cjs.js +1499 -0
  287. package/semver/index.cjs.js.map +1 -0
  288. package/semver/index.d.ts +6 -0
  289. package/semver/index.d.ts.map +1 -0
  290. package/semver/index.esm.js +1458 -0
  291. package/semver/index.esm.js.map +1 -0
  292. package/semver/models/index.cjs.js +153 -0
  293. package/semver/models/index.cjs.js.map +1 -0
  294. package/semver/models/index.d.ts +5 -0
  295. package/semver/models/index.d.ts.map +1 -0
  296. package/semver/models/index.esm.js +139 -0
  297. package/semver/models/index.esm.js.map +1 -0
  298. package/semver/models/range.d.ts +83 -0
  299. package/semver/models/range.d.ts.map +1 -0
  300. package/semver/models/version.d.ts +78 -0
  301. package/semver/models/version.d.ts.map +1 -0
  302. package/semver/parse/index.cjs.js +799 -0
  303. package/semver/parse/index.cjs.js.map +1 -0
  304. package/semver/parse/index.d.ts +5 -0
  305. package/semver/parse/index.d.ts.map +1 -0
  306. package/semver/parse/index.esm.js +793 -0
  307. package/semver/parse/index.esm.js.map +1 -0
  308. package/semver/parse/range.d.ts +38 -0
  309. package/semver/parse/range.d.ts.map +1 -0
  310. package/semver/parse/version.d.ts +49 -0
  311. package/semver/parse/version.d.ts.map +1 -0
  312. package/workspace/discovery/changelog-path.d.ts +21 -0
  313. package/workspace/discovery/changelog-path.d.ts.map +1 -0
  314. package/workspace/discovery/dependencies.d.ts +145 -0
  315. package/workspace/discovery/dependencies.d.ts.map +1 -0
  316. package/workspace/discovery/discover-changelogs.d.ts +76 -0
  317. package/workspace/discovery/discover-changelogs.d.ts.map +1 -0
  318. package/workspace/discovery/index.cjs.js +2300 -0
  319. package/workspace/discovery/index.cjs.js.map +1 -0
  320. package/workspace/discovery/index.d.ts +13 -0
  321. package/workspace/discovery/index.d.ts.map +1 -0
  322. package/workspace/discovery/index.esm.js +2283 -0
  323. package/workspace/discovery/index.esm.js.map +1 -0
  324. package/workspace/discovery/packages.d.ts +83 -0
  325. package/workspace/discovery/packages.d.ts.map +1 -0
  326. package/workspace/index.cjs.js +4445 -0
  327. package/workspace/index.cjs.js.map +1 -0
  328. package/workspace/index.d.ts +52 -0
  329. package/workspace/index.d.ts.map +1 -0
  330. package/workspace/index.esm.js +4394 -0
  331. package/workspace/index.esm.js.map +1 -0
  332. package/workspace/models/index.cjs.js +284 -0
  333. package/workspace/models/index.cjs.js.map +1 -0
  334. package/workspace/models/index.d.ts +10 -0
  335. package/workspace/models/index.d.ts.map +1 -0
  336. package/workspace/models/index.esm.js +261 -0
  337. package/workspace/models/index.esm.js.map +1 -0
  338. package/workspace/models/project.d.ts +118 -0
  339. package/workspace/models/project.d.ts.map +1 -0
  340. package/workspace/models/workspace.d.ts +139 -0
  341. package/workspace/models/workspace.d.ts.map +1 -0
  342. package/workspace/operations/batch-update.d.ts +99 -0
  343. package/workspace/operations/batch-update.d.ts.map +1 -0
  344. package/workspace/operations/cascade-bump.d.ts +125 -0
  345. package/workspace/operations/cascade-bump.d.ts.map +1 -0
  346. package/workspace/operations/index.cjs.js +2675 -0
  347. package/workspace/operations/index.cjs.js.map +1 -0
  348. package/workspace/operations/index.d.ts +12 -0
  349. package/workspace/operations/index.d.ts.map +1 -0
  350. package/workspace/operations/index.esm.js +2663 -0
  351. package/workspace/operations/index.esm.js.map +1 -0
  352. package/workspace/operations/validate.d.ts +85 -0
  353. package/workspace/operations/validate.d.ts.map +1 -0
@@ -0,0 +1,4445 @@
1
+ 'use strict';
2
+
3
+ var node_path = require('node:path');
4
+ require('node:util');
5
+ var node_fs = require('node:fs');
6
+ require('node:os');
7
+
8
+ /**
9
+ * Safe copies of Map built-in via factory function.
10
+ *
11
+ * Since constructors cannot be safely captured via Object.assign, this module
12
+ * provides a factory function that uses Reflect.construct internally.
13
+ *
14
+ * These references are captured at module initialization time to protect against
15
+ * prototype pollution attacks. Import only what you need for tree-shaking.
16
+ *
17
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/map
18
+ */
19
+ // Capture references at module initialization time
20
+ const _Map = globalThis.Map;
21
+ const _Reflect$2 = globalThis.Reflect;
22
+ /**
23
+ * (Safe copy) Creates a new Map using the captured Map constructor.
24
+ * Use this instead of `new Map()`.
25
+ *
26
+ * @param iterable - Optional iterable of key-value pairs.
27
+ * @returns A new Map instance.
28
+ */
29
+ const createMap = (iterable) => _Reflect$2.construct(_Map, iterable ? [iterable] : []);
30
+
31
+ /**
32
+ * Safe copies of Console built-in methods.
33
+ *
34
+ * These references are captured at module initialization time to protect against
35
+ * prototype pollution attacks. Import only what you need for tree-shaking.
36
+ *
37
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/console
38
+ */
39
+ // Capture references at module initialization time
40
+ const _console = globalThis.console;
41
+ /**
42
+ * (Safe copy) Outputs a message to the console.
43
+ */
44
+ const log = _console.log.bind(_console);
45
+ /**
46
+ * (Safe copy) Outputs a warning message to the console.
47
+ */
48
+ const warn = _console.warn.bind(_console);
49
+ /**
50
+ * (Safe copy) Outputs an error message to the console.
51
+ */
52
+ const error = _console.error.bind(_console);
53
+ /**
54
+ * (Safe copy) Outputs an informational message to the console.
55
+ */
56
+ const info = _console.info.bind(_console);
57
+ /**
58
+ * (Safe copy) Outputs a debug message to the console.
59
+ */
60
+ const debug = _console.debug.bind(_console);
61
+ /**
62
+ * (Safe copy) Outputs a stack trace to the console.
63
+ */
64
+ _console.trace.bind(_console);
65
+ /**
66
+ * (Safe copy) Displays an interactive listing of the properties of a specified object.
67
+ */
68
+ _console.dir.bind(_console);
69
+ /**
70
+ * (Safe copy) Displays tabular data as a table.
71
+ */
72
+ _console.table.bind(_console);
73
+ /**
74
+ * (Safe copy) Writes an error message to the console if the assertion is false.
75
+ */
76
+ _console.assert.bind(_console);
77
+ /**
78
+ * (Safe copy) Clears the console.
79
+ */
80
+ _console.clear.bind(_console);
81
+ /**
82
+ * (Safe copy) Logs the number of times that this particular call to count() has been called.
83
+ */
84
+ _console.count.bind(_console);
85
+ /**
86
+ * (Safe copy) Resets the counter used with console.count().
87
+ */
88
+ _console.countReset.bind(_console);
89
+ /**
90
+ * (Safe copy) Creates a new inline group in the console.
91
+ */
92
+ _console.group.bind(_console);
93
+ /**
94
+ * (Safe copy) Creates a new inline group in the console that is initially collapsed.
95
+ */
96
+ _console.groupCollapsed.bind(_console);
97
+ /**
98
+ * (Safe copy) Exits the current inline group.
99
+ */
100
+ _console.groupEnd.bind(_console);
101
+ /**
102
+ * (Safe copy) Starts a timer with a name specified as an input parameter.
103
+ */
104
+ _console.time.bind(_console);
105
+ /**
106
+ * (Safe copy) Stops a timer that was previously started.
107
+ */
108
+ _console.timeEnd.bind(_console);
109
+ /**
110
+ * (Safe copy) Logs the current value of a timer that was previously started.
111
+ */
112
+ _console.timeLog.bind(_console);
113
+
114
+ /**
115
+ * Safe copies of Array built-in static methods.
116
+ *
117
+ * These references are captured at module initialization time to protect against
118
+ * prototype pollution attacks. Import only what you need for tree-shaking.
119
+ *
120
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/array
121
+ */
122
+ // Capture references at module initialization time
123
+ const _Array = globalThis.Array;
124
+ /**
125
+ * (Safe copy) Determines whether the passed value is an Array.
126
+ */
127
+ const isArray = _Array.isArray;
128
+
129
+ /**
130
+ * Safe copies of JSON built-in methods.
131
+ *
132
+ * These references are captured at module initialization time to protect against
133
+ * prototype pollution attacks. Import only what you need for tree-shaking.
134
+ *
135
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/json
136
+ */
137
+ // Capture references at module initialization time
138
+ const _JSON = globalThis.JSON;
139
+ /**
140
+ * (Safe copy) Converts a JavaScript Object Notation (JSON) string into an object.
141
+ */
142
+ const parse = _JSON.parse;
143
+ /**
144
+ * (Safe copy) Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
145
+ */
146
+ const stringify = _JSON.stringify;
147
+
148
+ /**
149
+ * Safe copies of Object built-in methods.
150
+ *
151
+ * These references are captured at module initialization time to protect against
152
+ * prototype pollution attacks. Import only what you need for tree-shaking.
153
+ *
154
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/object
155
+ */
156
+ // Capture references at module initialization time
157
+ const _Object = globalThis.Object;
158
+ /**
159
+ * (Safe copy) Prevents modification of existing property attributes and values,
160
+ * and prevents the addition of new properties.
161
+ */
162
+ const freeze = _Object.freeze;
163
+ /**
164
+ * (Safe copy) Returns the names of the enumerable string properties and methods of an object.
165
+ */
166
+ const keys = _Object.keys;
167
+ /**
168
+ * (Safe copy) Returns an array of key/values of the enumerable own properties of an object.
169
+ */
170
+ const entries = _Object.entries;
171
+ /**
172
+ * (Safe copy) Returns an array of values of the enumerable own properties of an object.
173
+ */
174
+ const values = _Object.values;
175
+ /**
176
+ * (Safe copy) Adds one or more properties to an object, and/or modifies attributes of existing properties.
177
+ */
178
+ const defineProperties = _Object.defineProperties;
179
+
180
+ /**
181
+ * Safe copies of Set built-in via factory function.
182
+ *
183
+ * Since constructors cannot be safely captured via Object.assign, this module
184
+ * provides a factory function that uses Reflect.construct internally.
185
+ *
186
+ * These references are captured at module initialization time to protect against
187
+ * prototype pollution attacks. Import only what you need for tree-shaking.
188
+ *
189
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/set
190
+ */
191
+ // Capture references at module initialization time
192
+ const _Set = globalThis.Set;
193
+ const _Reflect$1 = globalThis.Reflect;
194
+ /**
195
+ * (Safe copy) Creates a new Set using the captured Set constructor.
196
+ * Use this instead of `new Set()`.
197
+ *
198
+ * @param iterable - Optional iterable of values.
199
+ * @returns A new Set instance.
200
+ */
201
+ const createSet = (iterable) => _Reflect$1.construct(_Set, iterable ? [iterable] : []);
202
+
203
+ const registeredClasses = [];
204
+
205
+ /**
206
+ * Returns the data type of the target.
207
+ * Uses native `typeof` operator, however, makes distinction between `null`, `array`, and `object`.
208
+ * Also, when classes are registered via `registerClass`, it checks if objects are instance of any known registered class.
209
+ *
210
+ * @param target - The target to get the data type of.
211
+ * @returns The data type of the target.
212
+ */
213
+ const getType = (target) => {
214
+ if (target === null)
215
+ return 'null';
216
+ const nativeDataType = typeof target;
217
+ if (nativeDataType === 'object') {
218
+ if (isArray(target))
219
+ return 'array';
220
+ for (const registeredClass of registeredClasses) {
221
+ if (target instanceof registeredClass)
222
+ return registeredClass.name;
223
+ }
224
+ }
225
+ return nativeDataType;
226
+ };
227
+
228
+ /**
229
+ * Safe copies of Error built-ins via factory functions.
230
+ *
231
+ * Since constructors cannot be safely captured via Object.assign, this module
232
+ * provides factory functions that use Reflect.construct internally.
233
+ *
234
+ * These references are captured at module initialization time to protect against
235
+ * prototype pollution attacks. Import only what you need for tree-shaking.
236
+ *
237
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/error
238
+ */
239
+ // Capture references at module initialization time
240
+ const _Error = globalThis.Error;
241
+ const _Reflect = globalThis.Reflect;
242
+ /**
243
+ * (Safe copy) Creates a new Error using the captured Error constructor.
244
+ * Use this instead of `new Error()`.
245
+ *
246
+ * @param message - Optional error message.
247
+ * @param options - Optional error options.
248
+ * @returns A new Error instance.
249
+ */
250
+ const createError = (message, options) => _Reflect.construct(_Error, [message, options]);
251
+
252
+ /**
253
+ * Safe copies of Math built-in methods.
254
+ *
255
+ * These references are captured at module initialization time to protect against
256
+ * prototype pollution attacks. Import only what you need for tree-shaking.
257
+ *
258
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
259
+ */
260
+ // Capture references at module initialization time
261
+ const _Math = globalThis.Math;
262
+ // ============================================================================
263
+ // Min/Max
264
+ // ============================================================================
265
+ /**
266
+ * (Safe copy) Returns the larger of zero or more numbers.
267
+ */
268
+ const max = _Math.max;
269
+
270
+ /* eslint-disable @typescript-eslint/no-explicit-any */
271
+ /**
272
+ * Creates a wrapper function that only executes the wrapped function if the condition function returns true.
273
+ *
274
+ * @param func - The function to be conditionally executed.
275
+ * @param conditionFunc - A function that returns a boolean, determining if `func` should be executed.
276
+ * @returns A wrapped version of `func` that executes conditionally.
277
+ */
278
+ function createConditionalExecutionFunction(func, conditionFunc) {
279
+ return function (...args) {
280
+ if (conditionFunc()) {
281
+ return func(...args);
282
+ }
283
+ };
284
+ }
285
+
286
+ /* eslint-disable @typescript-eslint/no-explicit-any */
287
+ /**
288
+ * Creates a wrapper function that silently ignores any errors thrown by the wrapped void function.
289
+ * This function is specifically for wrapping functions that do not return a value (void functions).
290
+ * Exceptions are swallowed without any logging or handling.
291
+ *
292
+ * @param func - The void function to be wrapped.
293
+ * @returns A wrapped version of the input function that ignores errors.
294
+ */
295
+ function createErrorIgnoringFunction(func) {
296
+ return function (...args) {
297
+ try {
298
+ func(...args);
299
+ }
300
+ catch {
301
+ // Deliberately swallowing/ignoring the exception
302
+ }
303
+ };
304
+ }
305
+
306
+ /* eslint-disable @typescript-eslint/no-unused-vars */
307
+ /**
308
+ * A no-operation function (noop) that does nothing regardless of the arguments passed.
309
+ * It is designed to be as permissive as possible in its typing without using the `Function` keyword.
310
+ *
311
+ * @param args - Any arguments passed to the function (ignored)
312
+ */
313
+ const noop = (...args) => {
314
+ // Intentionally does nothing
315
+ };
316
+
317
+ const logLevels = ['none', 'error', 'warn', 'log', 'info', 'debug'];
318
+ const priority = {
319
+ error: 4,
320
+ warn: 3,
321
+ log: 2,
322
+ info: 1,
323
+ debug: 0,
324
+ };
325
+ /**
326
+ * Validates whether a given string is a valid log level.
327
+ *
328
+ * @param level - The log level to validate
329
+ * @returns True if the level is valid, false otherwise
330
+ */
331
+ function isValidLogLevel(level) {
332
+ return logLevels.includes(level);
333
+ }
334
+ /**
335
+ * Creates a log level configuration manager for controlling logging behavior.
336
+ * Provides methods to get, set, and evaluate log levels based on priority.
337
+ *
338
+ * @param level - The initial log level (defaults to 'error')
339
+ * @returns A configuration object with log level management methods
340
+ * @throws {Error} When the provided level is not a valid log level
341
+ */
342
+ function createLogLevelConfig(level = 'error') {
343
+ if (!isValidLogLevel(level)) {
344
+ throw createError('Cannot create log level configuration with a valid default log level');
345
+ }
346
+ const state = { level };
347
+ const getLogLevel = () => state.level;
348
+ const setLogLevel = (level) => {
349
+ if (!isValidLogLevel(level)) {
350
+ throw createError(`Cannot set value '${level}' level. Expected levels are ${logLevels}.`);
351
+ }
352
+ state.level = level;
353
+ };
354
+ const shouldLog = (level) => {
355
+ if (state.level === 'none' || level === 'none' || !isValidLogLevel(level)) {
356
+ return false;
357
+ }
358
+ return priority[level] >= priority[state.level];
359
+ };
360
+ return freeze({
361
+ getLogLevel,
362
+ setLogLevel,
363
+ shouldLog,
364
+ });
365
+ }
366
+
367
+ /**
368
+ * Creates a logger instance with configurable log level filtering.
369
+ * Each log function is wrapped to respect the current log level setting.
370
+ *
371
+ * @param error - Function to handle error-level logs (required)
372
+ * @param warn - Function to handle warning-level logs (optional, defaults to noop)
373
+ * @param log - Function to handle standard logs (optional, defaults to noop)
374
+ * @param info - Function to handle info-level logs (optional, defaults to noop)
375
+ * @param debug - Function to handle debug-level logs (optional, defaults to noop)
376
+ * @returns A frozen logger object with log methods and level control
377
+ * @throws {ErrorLevelFn} When any provided log function is invalid
378
+ */
379
+ function createLogger(error, warn = noop, log = noop, info = noop, debug = noop) {
380
+ if (notValidLogFn(error)) {
381
+ throw createError(notFnMsg('error'));
382
+ }
383
+ if (notValidLogFn(warn)) {
384
+ throw createError(notFnMsg('warn'));
385
+ }
386
+ if (notValidLogFn(log)) {
387
+ throw createError(notFnMsg('log'));
388
+ }
389
+ if (notValidLogFn(info)) {
390
+ throw createError(notFnMsg('info'));
391
+ }
392
+ if (notValidLogFn(debug)) {
393
+ throw createError(notFnMsg('debug'));
394
+ }
395
+ const { setLogLevel, getLogLevel, shouldLog } = createLogLevelConfig();
396
+ const wrapLogFn = (fn, level) => {
397
+ if (fn === noop)
398
+ return fn;
399
+ const condition = () => shouldLog(level);
400
+ return createConditionalExecutionFunction(createErrorIgnoringFunction(fn), condition);
401
+ };
402
+ return freeze({
403
+ error: wrapLogFn(error, 'error'),
404
+ warn: wrapLogFn(warn, 'warn'),
405
+ log: wrapLogFn(log, 'log'),
406
+ info: wrapLogFn(info, 'info'),
407
+ debug: wrapLogFn(debug, 'debug'),
408
+ setLogLevel,
409
+ getLogLevel,
410
+ });
411
+ }
412
+ /**
413
+ * Validates whether a given value is a valid log function.
414
+ *
415
+ * @param fn - The value to validate
416
+ * @returns True if the value is not a function (invalid), false if it is valid
417
+ */
418
+ function notValidLogFn(fn) {
419
+ return getType(fn) !== 'function' && fn !== noop;
420
+ }
421
+ /**
422
+ * Generates an error message for invalid log function parameters.
423
+ *
424
+ * @param label - The name of the log function that failed validation
425
+ * @returns A formatted error message string
426
+ */
427
+ function notFnMsg(label) {
428
+ return `Cannot create a logger when ${label} is not a function`;
429
+ }
430
+
431
+ createLogger(error, warn, log, info, debug);
432
+
433
+ /**
434
+ * Global log level registry.
435
+ * Tracks all created scoped loggers to allow global log level changes.
436
+ */
437
+ const loggerRegistry = createSet();
438
+ /** Redacted placeholder for sensitive values */
439
+ const REDACTED = '[REDACTED]';
440
+ /**
441
+ * Patterns that indicate a sensitive key name.
442
+ * Keys containing these patterns will have their values sanitized.
443
+ */
444
+ const SENSITIVE_KEY_PATTERNS = [
445
+ /token/i,
446
+ /key/i,
447
+ /password/i,
448
+ /secret/i,
449
+ /credential/i,
450
+ /auth/i,
451
+ /bearer/i,
452
+ /api[_-]?key/i,
453
+ /private/i,
454
+ /passphrase/i,
455
+ ];
456
+ /**
457
+ * Checks if a key name indicates sensitive data.
458
+ *
459
+ * @param key - Key name to check
460
+ * @returns True if the key indicates sensitive data
461
+ */
462
+ function isSensitiveKey(key) {
463
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
464
+ }
465
+ /**
466
+ * Sanitizes an object by replacing sensitive values with REDACTED.
467
+ * This function recursively processes nested objects and arrays.
468
+ *
469
+ * @param obj - Object to sanitize
470
+ * @returns New object with sensitive values redacted
471
+ */
472
+ function sanitize(obj) {
473
+ if (obj === null || obj === undefined) {
474
+ return obj;
475
+ }
476
+ if (isArray(obj)) {
477
+ return obj.map((item) => sanitize(item));
478
+ }
479
+ if (typeof obj === 'object') {
480
+ const result = {};
481
+ for (const [key, value] of entries(obj)) {
482
+ if (isSensitiveKey(key)) {
483
+ result[key] = REDACTED;
484
+ }
485
+ else if (typeof value === 'object' && value !== null) {
486
+ result[key] = sanitize(value);
487
+ }
488
+ else {
489
+ result[key] = value;
490
+ }
491
+ }
492
+ return result;
493
+ }
494
+ return obj;
495
+ }
496
+ /**
497
+ * Formats a log message with optional metadata.
498
+ *
499
+ * @param namespace - Logger namespace prefix
500
+ * @param message - Log message
501
+ * @param meta - Optional metadata object
502
+ * @returns Formatted log string
503
+ */
504
+ function formatMessage(namespace, message, meta) {
505
+ const prefix = `[${namespace}]`;
506
+ if (meta && keys(meta).length > 0) {
507
+ const sanitizedMeta = sanitize(meta);
508
+ return `${prefix} ${message} ${stringify(sanitizedMeta)}`;
509
+ }
510
+ return `${prefix} ${message}`;
511
+ }
512
+ /**
513
+ * Creates a scoped logger with namespace prefix and optional secret sanitization.
514
+ * All log messages will be prefixed with [namespace] and sensitive metadata
515
+ * values will be automatically redacted.
516
+ *
517
+ * @param namespace - Logger namespace (e.g., 'project-scope', 'analyze')
518
+ * @param options - Logger configuration options
519
+ * @returns A configured scoped logger instance
520
+ *
521
+ * @example
522
+ * ```typescript
523
+ * const logger = createScopedLogger('project-scope')
524
+ * logger.setLogLevel('debug')
525
+ *
526
+ * // Basic logging
527
+ * logger.info('Starting analysis', { path: './project' })
528
+ *
529
+ * // Sensitive data is automatically redacted
530
+ * logger.debug('Config loaded', { apiKey: 'secret123' })
531
+ * // Output: [project-scope] Config loaded {"apiKey":"[REDACTED]"}
532
+ * ```
533
+ */
534
+ function createScopedLogger(namespace, options = {}) {
535
+ const { level = 'error', sanitizeSecrets = true } = options;
536
+ // Create wrapper functions that add namespace prefix and sanitization
537
+ const createLogFn = (baseFn) => (message, meta) => {
538
+ const processedMeta = sanitizeSecrets && meta ? sanitize(meta) : meta;
539
+ baseFn(formatMessage(namespace, message, processedMeta));
540
+ };
541
+ // Create base logger with wrapped functions
542
+ const baseLogger = createLogger(createLogFn(error), createLogFn(warn), createLogFn(log), createLogFn(info), createLogFn(debug));
543
+ // Set initial log level (use global override if set)
544
+ baseLogger.setLogLevel(level);
545
+ const scopedLogger = freeze({
546
+ error: (message, meta) => baseLogger.error(message, meta),
547
+ warn: (message, meta) => baseLogger.warn(message, meta),
548
+ log: (message, meta) => baseLogger.log(message, meta),
549
+ info: (message, meta) => baseLogger.info(message, meta),
550
+ debug: (message, meta) => baseLogger.debug(message, meta),
551
+ setLogLevel: baseLogger.setLogLevel,
552
+ getLogLevel: baseLogger.getLogLevel,
553
+ });
554
+ // Register logger for global level management
555
+ loggerRegistry.add(scopedLogger);
556
+ return scopedLogger;
557
+ }
558
+ /**
559
+ * Default logger instance for the project-scope library.
560
+ * Use this for general logging within the library.
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * import { logger } from '@hyperfrontend/project-scope/core'
565
+ *
566
+ * logger.setLogLevel('debug')
567
+ * logger.debug('Analyzing project', { path: './src' })
568
+ * ```
569
+ */
570
+ createScopedLogger('project-scope');
571
+
572
+ const fsLogger = createScopedLogger('project-scope:fs');
573
+ /**
574
+ * Create a file system error with code and context.
575
+ *
576
+ * @param message - The error message describing what went wrong
577
+ * @param code - The category code for this type of filesystem failure
578
+ * @param context - Additional context including path, operation, and cause
579
+ * @returns A configured Error object with code and context properties
580
+ */
581
+ function createFileSystemError(message, code, context) {
582
+ const error = createError(message);
583
+ defineProperties(error, {
584
+ code: { value: code, enumerable: true },
585
+ context: { value: context, enumerable: true },
586
+ });
587
+ return error;
588
+ }
589
+ /**
590
+ * Read file contents as string.
591
+ *
592
+ * @param filePath - Path to file
593
+ * @param encoding - File encoding (default: utf-8)
594
+ * @returns File contents as string
595
+ * @throws {Error} If file doesn't exist or can't be read
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * import { readFileContent } from '@hyperfrontend/project-scope'
600
+ *
601
+ * const content = readFileContent('./package.json')
602
+ * console.log(content) // JSON string
603
+ * ```
604
+ */
605
+ function readFileContent(filePath, encoding = 'utf-8') {
606
+ if (!node_fs.existsSync(filePath)) {
607
+ fsLogger.debug('File not found', { path: filePath });
608
+ throw createFileSystemError(`File not found: ${filePath}`, 'FS_NOT_FOUND', { path: filePath, operation: 'read' });
609
+ }
610
+ try {
611
+ return node_fs.readFileSync(filePath, { encoding });
612
+ }
613
+ catch (error) {
614
+ fsLogger.warn('Failed to read file', { path: filePath });
615
+ throw createFileSystemError(`Failed to read file: ${filePath}`, 'FS_READ_ERROR', { path: filePath, operation: 'read', cause: error });
616
+ }
617
+ }
618
+ /**
619
+ * Read file if exists, return null otherwise.
620
+ *
621
+ * @param filePath - Path to file
622
+ * @param encoding - File encoding (default: utf-8)
623
+ * @returns File contents or null if file doesn't exist
624
+ */
625
+ function readFileIfExists(filePath, encoding = 'utf-8') {
626
+ if (!node_fs.existsSync(filePath)) {
627
+ return null;
628
+ }
629
+ try {
630
+ return node_fs.readFileSync(filePath, { encoding });
631
+ }
632
+ catch {
633
+ return null;
634
+ }
635
+ }
636
+
637
+ const fsWriteLogger = createScopedLogger('project-scope:fs:write');
638
+ /**
639
+ * Ensure directory exists, create recursively if not.
640
+ *
641
+ * @param dirPath - Directory path to ensure exists
642
+ */
643
+ function ensureDir(dirPath) {
644
+ if (!node_fs.existsSync(dirPath)) {
645
+ fsWriteLogger.debug('Creating directory recursively', { path: dirPath });
646
+ node_fs.mkdirSync(dirPath, { recursive: true });
647
+ }
648
+ }
649
+ /**
650
+ * Write string content to file.
651
+ * Creates parent directories if needed.
652
+ *
653
+ * @param filePath - Absolute or relative path to the target file
654
+ * @param content - String content to write to the file
655
+ * @param options - Optional write configuration (encoding, mode)
656
+ * @throws {Error} If write fails
657
+ *
658
+ * @example
659
+ * ```typescript
660
+ * import { writeFileContent } from '@hyperfrontend/project-scope'
661
+ *
662
+ * // Write a simple text file
663
+ * writeFileContent('./output/data.txt', 'Hello, World!')
664
+ *
665
+ * // Write with custom encoding
666
+ * writeFileContent('./output/data.txt', 'Content', { encoding: 'utf-8' })
667
+ * ```
668
+ */
669
+ function writeFileContent(filePath, content, options) {
670
+ try {
671
+ fsWriteLogger.debug('Writing file content', { path: filePath, size: content.length, encoding: options?.encoding ?? 'utf-8' });
672
+ ensureDir(node_path.dirname(filePath));
673
+ node_fs.writeFileSync(filePath, content, {
674
+ encoding: options?.encoding ?? 'utf-8',
675
+ mode: options?.mode,
676
+ });
677
+ fsWriteLogger.debug('File written successfully', { path: filePath });
678
+ }
679
+ catch (error) {
680
+ fsWriteLogger.warn('Failed to write file', { path: filePath, error: error instanceof Error ? error.message : String(error) });
681
+ throw createFileSystemError(`Failed to write file: ${filePath}`, 'FS_WRITE_ERROR', { path: filePath, operation: 'write', cause: error });
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Get file stats with error handling.
687
+ *
688
+ * @param filePath - Path to file
689
+ * @param followSymlinks - Whether to follow symlinks (default: true)
690
+ * @returns File stats or null if path doesn't exist
691
+ */
692
+ function getFileStat(filePath, followSymlinks = true) {
693
+ if (!node_fs.existsSync(filePath)) {
694
+ return null;
695
+ }
696
+ try {
697
+ const stat = followSymlinks ? node_fs.statSync(filePath) : node_fs.lstatSync(filePath);
698
+ return {
699
+ isFile: stat.isFile(),
700
+ isDirectory: stat.isDirectory(),
701
+ isSymlink: stat.isSymbolicLink(),
702
+ size: stat.size,
703
+ created: stat.birthtime,
704
+ modified: stat.mtime,
705
+ accessed: stat.atime,
706
+ mode: stat.mode,
707
+ };
708
+ }
709
+ catch {
710
+ return null;
711
+ }
712
+ }
713
+ /**
714
+ * Check if path is a directory.
715
+ *
716
+ * @param dirPath - Path to check
717
+ * @returns True if path is a directory
718
+ */
719
+ function isDirectory(dirPath) {
720
+ const stats = getFileStat(dirPath);
721
+ return stats?.isDirectory ?? false;
722
+ }
723
+ /**
724
+ * Check if path exists.
725
+ *
726
+ * @param filePath - Path to check
727
+ * @returns True if path exists
728
+ */
729
+ function exists(filePath) {
730
+ return node_fs.existsSync(filePath);
731
+ }
732
+
733
+ const fsDirLogger = createScopedLogger('project-scope:fs:dir');
734
+ /**
735
+ * List immediate contents of a directory.
736
+ *
737
+ * @param dirPath - Absolute or relative path to the directory
738
+ * @returns Array of entries with metadata for each file/directory
739
+ * @throws {Error} If directory doesn't exist or isn't a directory
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * import { readDirectory } from '@hyperfrontend/project-scope'
744
+ *
745
+ * const entries = readDirectory('./src')
746
+ * for (const entry of entries) {
747
+ * console.log(entry.name, entry.isFile ? 'file' : 'directory')
748
+ * }
749
+ * ```
750
+ */
751
+ function readDirectory(dirPath) {
752
+ fsDirLogger.debug('Reading directory', { path: dirPath });
753
+ if (!node_fs.existsSync(dirPath)) {
754
+ fsDirLogger.debug('Directory not found', { path: dirPath });
755
+ throw createFileSystemError(`Directory not found: ${dirPath}`, 'FS_NOT_FOUND', { path: dirPath, operation: 'readdir' });
756
+ }
757
+ if (!isDirectory(dirPath)) {
758
+ fsDirLogger.debug('Path is not a directory', { path: dirPath });
759
+ throw createFileSystemError(`Not a directory: ${dirPath}`, 'FS_NOT_A_DIRECTORY', { path: dirPath, operation: 'readdir' });
760
+ }
761
+ try {
762
+ const entries = node_fs.readdirSync(dirPath, { withFileTypes: true });
763
+ fsDirLogger.debug('Directory read complete', { path: dirPath, entryCount: entries.length });
764
+ return entries.map((entry) => ({
765
+ name: entry.name,
766
+ path: node_path.join(dirPath, entry.name),
767
+ isFile: entry.isFile(),
768
+ isDirectory: entry.isDirectory(),
769
+ isSymlink: entry.isSymbolicLink(),
770
+ }));
771
+ }
772
+ catch (error) {
773
+ fsDirLogger.warn('Failed to read directory', { path: dirPath, error: error instanceof Error ? error.message : String(error) });
774
+ throw createFileSystemError(`Failed to read directory: ${dirPath}`, 'FS_READ_ERROR', {
775
+ path: dirPath,
776
+ operation: 'readdir',
777
+ cause: error,
778
+ });
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Join path segments.
784
+ * Uses platform-specific separators (e.g., / or \).
785
+ *
786
+ * @param paths - Path segments to join
787
+ * @returns Joined path
788
+ */
789
+ function join(...paths) {
790
+ return node_path.join(...paths);
791
+ }
792
+
793
+ const fsTraversalLogger = createScopedLogger('project-scope:fs:traversal');
794
+ /**
795
+ * Generic upward directory traversal.
796
+ * Name avoids similarity to fs.readdir/fs.readdirSync.
797
+ *
798
+ * @param startPath - Starting directory
799
+ * @param predicate - Function to test each directory
800
+ * @returns First matching directory or null
801
+ */
802
+ function traverseUpward(startPath, predicate) {
803
+ fsTraversalLogger.debug('Starting upward traversal', { startPath });
804
+ let currentPath = node_path.resolve(startPath);
805
+ const rootPath = node_path.parse(currentPath).root;
806
+ while (currentPath !== rootPath) {
807
+ if (predicate(currentPath)) {
808
+ fsTraversalLogger.debug('Upward traversal found match', { startPath, foundPath: currentPath });
809
+ return currentPath;
810
+ }
811
+ currentPath = node_path.dirname(currentPath);
812
+ }
813
+ // Check root directory
814
+ if (predicate(rootPath)) {
815
+ fsTraversalLogger.debug('Upward traversal found match at root', { startPath, foundPath: rootPath });
816
+ return rootPath;
817
+ }
818
+ fsTraversalLogger.debug('Upward traversal found no match', { startPath });
819
+ return null;
820
+ }
821
+ /**
822
+ * Find directory containing any of the specified marker files.
823
+ *
824
+ * @param startPath - Starting directory
825
+ * @param markers - Array of marker file names to search for
826
+ * @returns First directory containing any marker, or null
827
+ */
828
+ function locateByMarkers(startPath, markers) {
829
+ fsTraversalLogger.debug('Locating by markers', { startPath, markers });
830
+ const result = traverseUpward(startPath, (dir) => markers.some((marker) => exists(join(dir, marker))));
831
+ if (result) {
832
+ fsTraversalLogger.debug('Found directory with marker', { startPath, foundPath: result });
833
+ }
834
+ return result;
835
+ }
836
+ /**
837
+ * Find directory where predicate returns true, starting from given path.
838
+ *
839
+ * @param startPath - Starting directory
840
+ * @param test - Function to test if directory matches criteria
841
+ * @returns Matching directory path or null
842
+ */
843
+ function findUpwardWhere(startPath, test) {
844
+ fsTraversalLogger.debug('Finding upward where condition met', { startPath });
845
+ return traverseUpward(startPath, test);
846
+ }
847
+
848
+ /**
849
+ * Create a structured error with code and optional context.
850
+ *
851
+ * @param message - The human-readable error message
852
+ * @param code - The machine-readable error code for programmatic handling
853
+ * @param context - Additional contextual information about the error
854
+ * @returns Structured error instance with code and context properties
855
+ *
856
+ * @example
857
+ * ```typescript
858
+ * import { createStructuredError } from '@hyperfrontend/project-scope'
859
+ *
860
+ * throw createStructuredError(
861
+ * 'Configuration file not found',
862
+ * 'CONFIG_NOT_FOUND',
863
+ * { path: './config.json', searched: ['./config.json', './settings.json'] }
864
+ * )
865
+ * ```
866
+ */
867
+ function createStructuredError(message, code, context) {
868
+ const error = createError(message);
869
+ error.code = code;
870
+ error.context = context ?? {};
871
+ return error;
872
+ }
873
+ /**
874
+ * Create a configuration-related error.
875
+ *
876
+ * @param message - The human-readable error message
877
+ * @param code - The machine-readable error code for programmatic handling
878
+ * @param context - Additional contextual information (e.g., file path, config key)
879
+ * @returns Structured error instance tagged with type 'config'
880
+ */
881
+ function createConfigError(message, code, context) {
882
+ return createStructuredError(message, code, { ...context, type: 'config' });
883
+ }
884
+
885
+ const packageLogger = createScopedLogger('project-scope:project:package');
886
+ /**
887
+ * Verifies that a value is an object with only string values,
888
+ * used for validating dependency maps and script definitions.
889
+ *
890
+ * @param value - Value to check
891
+ * @returns True if value is a record of strings
892
+ */
893
+ function isStringRecord(value) {
894
+ if (typeof value !== 'object' || value === null)
895
+ return false;
896
+ return values(value).every((v) => typeof v === 'string');
897
+ }
898
+ /**
899
+ * Extracts and normalizes the workspaces field from package.json,
900
+ * supporting both array format and object with packages array.
901
+ *
902
+ * @param value - Raw workspaces value from package.json
903
+ * @returns Normalized workspace patterns or undefined if invalid
904
+ */
905
+ function parseWorkspaces(value) {
906
+ if (isArray(value) && value.every((v) => typeof v === 'string')) {
907
+ return value;
908
+ }
909
+ if (typeof value === 'object' && value !== null) {
910
+ const obj = value;
911
+ if (isArray(obj['packages'])) {
912
+ return { packages: obj['packages'] };
913
+ }
914
+ }
915
+ return undefined;
916
+ }
917
+ /**
918
+ * Validate and normalize package.json data.
919
+ *
920
+ * @param data - Raw parsed data
921
+ * @returns Validated package.json
922
+ */
923
+ function validatePackageJson(data) {
924
+ if (typeof data !== 'object' || data === null) {
925
+ throw createError('package.json must be an object');
926
+ }
927
+ const pkg = data;
928
+ return {
929
+ name: typeof pkg['name'] === 'string' ? pkg['name'] : undefined,
930
+ version: typeof pkg['version'] === 'string' ? pkg['version'] : undefined,
931
+ description: typeof pkg['description'] === 'string' ? pkg['description'] : undefined,
932
+ main: typeof pkg['main'] === 'string' ? pkg['main'] : undefined,
933
+ module: typeof pkg['module'] === 'string' ? pkg['module'] : undefined,
934
+ browser: typeof pkg['browser'] === 'string' ? pkg['browser'] : undefined,
935
+ types: typeof pkg['types'] === 'string' ? pkg['types'] : undefined,
936
+ bin: typeof pkg['bin'] === 'string' || isStringRecord(pkg['bin']) ? pkg['bin'] : undefined,
937
+ scripts: isStringRecord(pkg['scripts']) ? pkg['scripts'] : undefined,
938
+ dependencies: isStringRecord(pkg['dependencies']) ? pkg['dependencies'] : undefined,
939
+ devDependencies: isStringRecord(pkg['devDependencies']) ? pkg['devDependencies'] : undefined,
940
+ peerDependencies: isStringRecord(pkg['peerDependencies']) ? pkg['peerDependencies'] : undefined,
941
+ optionalDependencies: isStringRecord(pkg['optionalDependencies']) ? pkg['optionalDependencies'] : undefined,
942
+ workspaces: parseWorkspaces(pkg['workspaces']),
943
+ exports: typeof pkg['exports'] === 'object' ? pkg['exports'] : undefined,
944
+ engines: isStringRecord(pkg['engines']) ? pkg['engines'] : undefined,
945
+ ...pkg,
946
+ };
947
+ }
948
+ /**
949
+ * Reads and parses package.json from a directory, validating
950
+ * the structure and normalizing fields to the PackageJson interface.
951
+ *
952
+ * @param projectPath - Project directory path or path to package.json
953
+ * @returns Parsed package.json
954
+ * @throws {Error} Error if file doesn't exist or is invalid
955
+ */
956
+ function readPackageJson(projectPath) {
957
+ const packageJsonPath = projectPath.endsWith('package.json') ? projectPath : node_path.join(projectPath, 'package.json');
958
+ packageLogger.debug('Reading package.json', { path: packageJsonPath });
959
+ const content = readFileContent(packageJsonPath);
960
+ try {
961
+ const data = parse(content);
962
+ const validated = validatePackageJson(data);
963
+ packageLogger.debug('Package.json read successfully', { path: packageJsonPath, name: validated.name });
964
+ return validated;
965
+ }
966
+ catch (error) {
967
+ packageLogger.warn('Failed to parse package.json', {
968
+ path: packageJsonPath,
969
+ error: error instanceof Error ? error.message : String(error),
970
+ });
971
+ throw createConfigError(`Failed to parse package.json: ${packageJsonPath}`, 'CONFIG_PARSE_ERROR', {
972
+ filePath: packageJsonPath,
973
+ cause: error,
974
+ });
975
+ }
976
+ }
977
+ /**
978
+ * Attempts to read and parse package.json if it exists,
979
+ * returning null on missing file or parse failure.
980
+ *
981
+ * @param projectPath - Project directory path or path to package.json
982
+ * @returns Parsed package.json or null if not found
983
+ */
984
+ function readPackageJsonIfExists(projectPath) {
985
+ const packageJsonPath = projectPath.endsWith('package.json') ? projectPath : node_path.join(projectPath, 'package.json');
986
+ const content = readFileIfExists(packageJsonPath);
987
+ if (!content) {
988
+ packageLogger.debug('Package.json not found', { path: packageJsonPath });
989
+ return null;
990
+ }
991
+ try {
992
+ const validated = validatePackageJson(parse(content));
993
+ packageLogger.debug('Package.json loaded', { path: packageJsonPath, name: validated.name });
994
+ return validated;
995
+ }
996
+ catch {
997
+ packageLogger.debug('Failed to parse package.json, returning null', { path: packageJsonPath });
998
+ return null;
999
+ }
1000
+ }
1001
+ /**
1002
+ * Find nearest package.json by walking up the directory tree.
1003
+ *
1004
+ * @param startPath - Starting path
1005
+ * @returns Path to directory containing package.json, or null if not found
1006
+ */
1007
+ function findNearestPackageJson(startPath) {
1008
+ return locateByMarkers(startPath, ['package.json']);
1009
+ }
1010
+
1011
+ createScopedLogger('project-scope:heuristics:deps');
1012
+
1013
+ /**
1014
+ * Global registry of all caches for bulk operations.
1015
+ */
1016
+ const cacheRegistry = createSet();
1017
+ /**
1018
+ * Create a cache with optional TTL and size limits.
1019
+ *
1020
+ * The cache provides a simple key-value store with:
1021
+ * - Optional TTL (time-to-live) for automatic expiration
1022
+ * - Optional maxSize for limiting cache size with FIFO eviction
1023
+ * - Lazy expiration (entries are checked on access)
1024
+ *
1025
+ * @param options - Cache configuration options
1026
+ * @returns Cache instance
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * // Basic cache
1031
+ * const cache = createCache<string, number>()
1032
+ * cache.set('answer', 42)
1033
+ * cache.get('answer') // 42
1034
+ *
1035
+ * // Cache with TTL (expires after 60 seconds)
1036
+ * const ttlCache = createCache<string, object>({ ttl: 60000 })
1037
+ *
1038
+ * // Cache with max size (evicts oldest when full)
1039
+ * const lruCache = createCache<string, object>({ maxSize: 100 })
1040
+ *
1041
+ * // Combined options
1042
+ * const configCache = createCache<string, object>({
1043
+ * ttl: 30000,
1044
+ * maxSize: 50
1045
+ * })
1046
+ * ```
1047
+ */
1048
+ function createCache(options) {
1049
+ const { ttl, maxSize } = options ?? {};
1050
+ const store = createMap();
1051
+ // Track insertion order for FIFO eviction
1052
+ const insertionOrder = [];
1053
+ /**
1054
+ * Check if an entry is expired.
1055
+ *
1056
+ * @param entry - Cache entry to check
1057
+ * @returns True if entry is expired
1058
+ */
1059
+ function isExpired(entry) {
1060
+ if (ttl === undefined)
1061
+ return false;
1062
+ // eslint-disable-next-line workspace/no-unsafe-builtin-methods -- Date.now() is needed for Jest fake timers compatibility
1063
+ return Date.now() - entry.timestamp > ttl;
1064
+ }
1065
+ /**
1066
+ * Evict oldest entries to make room for new ones.
1067
+ */
1068
+ function evictIfNeeded() {
1069
+ if (maxSize === undefined)
1070
+ return;
1071
+ while (store.size >= maxSize && insertionOrder.length > 0) {
1072
+ const oldestKey = insertionOrder.shift();
1073
+ if (oldestKey !== undefined) {
1074
+ store.delete(oldestKey);
1075
+ }
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Remove key from insertion order tracking.
1080
+ *
1081
+ * @param key - Key to remove from order tracking
1082
+ */
1083
+ function removeFromOrder(key) {
1084
+ const index = insertionOrder.indexOf(key);
1085
+ if (index !== -1) {
1086
+ insertionOrder.splice(index, 1);
1087
+ }
1088
+ }
1089
+ const cache = {
1090
+ get(key) {
1091
+ const entry = store.get(key);
1092
+ if (!entry)
1093
+ return undefined;
1094
+ if (isExpired(entry)) {
1095
+ store.delete(key);
1096
+ removeFromOrder(key);
1097
+ return undefined;
1098
+ }
1099
+ return entry.value;
1100
+ },
1101
+ set(key, value) {
1102
+ // If key exists, remove from order first
1103
+ if (store.has(key)) {
1104
+ removeFromOrder(key);
1105
+ }
1106
+ else {
1107
+ // Evict if needed before adding new entry
1108
+ evictIfNeeded();
1109
+ }
1110
+ // eslint-disable-next-line workspace/no-unsafe-builtin-methods -- Date.now() is needed for Jest fake timers compatibility
1111
+ store.set(key, { value, timestamp: Date.now() });
1112
+ insertionOrder.push(key);
1113
+ },
1114
+ has(key) {
1115
+ const entry = store.get(key);
1116
+ if (!entry)
1117
+ return false;
1118
+ if (isExpired(entry)) {
1119
+ store.delete(key);
1120
+ removeFromOrder(key);
1121
+ return false;
1122
+ }
1123
+ return true;
1124
+ },
1125
+ delete(key) {
1126
+ removeFromOrder(key);
1127
+ return store.delete(key);
1128
+ },
1129
+ clear() {
1130
+ store.clear();
1131
+ insertionOrder.length = 0;
1132
+ },
1133
+ size() {
1134
+ return store.size;
1135
+ },
1136
+ keys() {
1137
+ return [...insertionOrder];
1138
+ },
1139
+ };
1140
+ // Register cache for global operations
1141
+ cacheRegistry.add(cache);
1142
+ return freeze(cache);
1143
+ }
1144
+
1145
+ /**
1146
+ * Pattern matching utilities with ReDoS protection.
1147
+ * Uses character-by-character matching instead of regex where possible.
1148
+ */
1149
+ /**
1150
+ * Match path against glob pattern using safe character iteration.
1151
+ * Avoids regex to prevent ReDoS attacks.
1152
+ *
1153
+ * Supported patterns:
1154
+ * - * matches any characters except /
1155
+ * - ** matches any characters including /
1156
+ * - ? matches exactly one character except /
1157
+ * - {a,b,c} matches any of the alternatives
1158
+ *
1159
+ * @param path - The filesystem path to test against the pattern
1160
+ * @param pattern - The glob pattern to match against
1161
+ * @returns True if path matches pattern
1162
+ *
1163
+ * @example
1164
+ * ```typescript
1165
+ * import { matchGlobPattern } from '@hyperfrontend/project-scope'
1166
+ *
1167
+ * matchGlobPattern('src/utils/helper.ts', '\*\*\/*.ts') // true
1168
+ * matchGlobPattern('test.spec.ts', '\*.spec.ts') // true
1169
+ * matchGlobPattern('config.json', '\*.{json,yaml}') // true
1170
+ * matchGlobPattern('src/index.ts', 'src/\*.ts') // true
1171
+ * ```
1172
+ */
1173
+ function matchGlobPattern(path, pattern) {
1174
+ return matchSegments(path.split('/'), pattern.split('/'), 0, 0);
1175
+ }
1176
+ /**
1177
+ * Internal recursive function to match path segments against pattern segments.
1178
+ *
1179
+ * @param pathParts - Array of path segments split by '/'
1180
+ * @param patternParts - Array of pattern segments split by '/'
1181
+ * @param pathIdx - Current index in pathParts being examined
1182
+ * @param patternIdx - Current index in patternParts being examined
1183
+ * @returns True if remaining segments match
1184
+ */
1185
+ function matchSegments(pathParts, patternParts, pathIdx, patternIdx) {
1186
+ // Base cases
1187
+ if (pathIdx === pathParts.length && patternIdx === patternParts.length) {
1188
+ return true; // Both exhausted = match
1189
+ }
1190
+ if (patternIdx >= patternParts.length) {
1191
+ return false; // Pattern exhausted but path remains
1192
+ }
1193
+ const patternPart = patternParts[patternIdx];
1194
+ // Handle ** (globstar) - matches zero or more directories
1195
+ if (patternPart === '**') {
1196
+ // Try matching rest of pattern against current position and all future positions
1197
+ for (let i = pathIdx; i <= pathParts.length; i++) {
1198
+ if (matchSegments(pathParts, patternParts, i, patternIdx + 1)) {
1199
+ return true;
1200
+ }
1201
+ }
1202
+ return false;
1203
+ }
1204
+ if (pathIdx >= pathParts.length) {
1205
+ return false; // Path exhausted but pattern remains (and it's not **)
1206
+ }
1207
+ const pathPart = pathParts[pathIdx];
1208
+ // Match current segment
1209
+ if (matchSegment(pathPart, patternPart)) {
1210
+ return matchSegments(pathParts, patternParts, pathIdx + 1, patternIdx + 1);
1211
+ }
1212
+ return false;
1213
+ }
1214
+ /**
1215
+ * Match a single path segment against a pattern segment.
1216
+ * Handles *, ?, and {a,b,c} patterns.
1217
+ *
1218
+ * @param text - The path segment text to match
1219
+ * @param pattern - The pattern segment to match against
1220
+ * @returns True if the text matches the pattern
1221
+ */
1222
+ function matchSegment(text, pattern) {
1223
+ let textIdx = 0;
1224
+ let patternIdx = 0;
1225
+ while (patternIdx < pattern.length) {
1226
+ const char = pattern[patternIdx];
1227
+ if (char === '*') {
1228
+ // * matches zero or more characters
1229
+ patternIdx++;
1230
+ if (patternIdx === pattern.length) {
1231
+ return true; // * at end matches rest of string
1232
+ }
1233
+ // Try matching rest of pattern at each position in text
1234
+ for (let i = textIdx; i <= text.length; i++) {
1235
+ if (matchSegmentFrom(text, i, pattern, patternIdx)) {
1236
+ return true;
1237
+ }
1238
+ }
1239
+ return false;
1240
+ }
1241
+ else if (char === '?') {
1242
+ // ? matches exactly one character
1243
+ if (textIdx >= text.length) {
1244
+ return false;
1245
+ }
1246
+ textIdx++;
1247
+ patternIdx++;
1248
+ }
1249
+ else if (char === '{') {
1250
+ // {a,b,c} matches any alternative
1251
+ const closeIdx = findClosingBrace(pattern, patternIdx);
1252
+ if (closeIdx === -1) {
1253
+ // Unmatched brace, treat as literal
1254
+ if (textIdx >= text.length || text[textIdx] !== char) {
1255
+ return false;
1256
+ }
1257
+ textIdx++;
1258
+ patternIdx++;
1259
+ }
1260
+ else {
1261
+ const alternatives = extractAlternatives(pattern.slice(patternIdx + 1, closeIdx));
1262
+ for (const alt of alternatives) {
1263
+ if (matchSegmentFrom(text, textIdx, text.slice(0, textIdx) + alt + pattern.slice(closeIdx + 1), textIdx)) {
1264
+ return true;
1265
+ }
1266
+ }
1267
+ return false;
1268
+ }
1269
+ }
1270
+ else {
1271
+ // Literal character
1272
+ if (textIdx >= text.length || text[textIdx] !== char) {
1273
+ return false;
1274
+ }
1275
+ textIdx++;
1276
+ patternIdx++;
1277
+ }
1278
+ }
1279
+ return textIdx === text.length;
1280
+ }
1281
+ /**
1282
+ * Helper to match from a specific position.
1283
+ *
1284
+ * @param text - The full text being matched
1285
+ * @param textIdx - The starting index in text to match from
1286
+ * @param pattern - The full pattern being matched
1287
+ * @param patternIdx - The starting index in pattern to match from
1288
+ * @returns True if the text matches the pattern from the given positions
1289
+ */
1290
+ function matchSegmentFrom(text, textIdx, pattern, patternIdx) {
1291
+ const remainingText = text.slice(textIdx);
1292
+ const remainingPattern = pattern.slice(patternIdx);
1293
+ return matchSegment(remainingText, remainingPattern);
1294
+ }
1295
+ /**
1296
+ * Find closing brace for {a,b,c} pattern.
1297
+ *
1298
+ * @param pattern - The pattern string to search within
1299
+ * @param startIdx - The index of the opening brace
1300
+ * @returns The index of the matching closing brace, or -1 if not found
1301
+ */
1302
+ function findClosingBrace(pattern, startIdx) {
1303
+ let depth = 0;
1304
+ for (let i = startIdx; i < pattern.length; i++) {
1305
+ if (pattern[i] === '{') {
1306
+ depth++;
1307
+ }
1308
+ else if (pattern[i] === '}') {
1309
+ depth--;
1310
+ if (depth === 0) {
1311
+ return i;
1312
+ }
1313
+ }
1314
+ }
1315
+ return -1;
1316
+ }
1317
+ /**
1318
+ * Extract alternatives from {a,b,c} pattern content.
1319
+ *
1320
+ * @param content - The content between braces (without the braces themselves)
1321
+ * @returns Array of alternative strings split by commas at depth 0
1322
+ */
1323
+ function extractAlternatives(content) {
1324
+ const alternatives = [];
1325
+ let current = '';
1326
+ let depth = 0;
1327
+ for (let i = 0; i < content.length; i++) {
1328
+ const char = content[i];
1329
+ if (char === '{') {
1330
+ depth++;
1331
+ current += char;
1332
+ }
1333
+ else if (char === '}') {
1334
+ depth--;
1335
+ current += char;
1336
+ }
1337
+ else if (char === ',' && depth === 0) {
1338
+ alternatives.push(current);
1339
+ current = '';
1340
+ }
1341
+ else {
1342
+ current += char;
1343
+ }
1344
+ }
1345
+ if (current) {
1346
+ alternatives.push(current);
1347
+ }
1348
+ return alternatives;
1349
+ }
1350
+
1351
+ const walkLogger = createScopedLogger('project-scope:project:walk');
1352
+ /**
1353
+ * Reads .gitignore file from the given directory and extracts
1354
+ * non-comment patterns for use in file traversal filtering.
1355
+ *
1356
+ * @param startPath - Directory containing the .gitignore file
1357
+ * @returns Array of gitignore patterns
1358
+ */
1359
+ function loadGitignorePatterns(startPath) {
1360
+ const patterns = [];
1361
+ const gitignorePath = node_path.join(startPath, '.gitignore');
1362
+ const content = readFileIfExists(gitignorePath);
1363
+ if (content) {
1364
+ const lines = content.split('\n');
1365
+ for (const line of lines) {
1366
+ const trimmed = line.trim();
1367
+ if (trimmed && !trimmed.startsWith('#')) {
1368
+ patterns.push(trimmed);
1369
+ }
1370
+ }
1371
+ }
1372
+ return patterns;
1373
+ }
1374
+ /**
1375
+ * Evaluates whether a relative path should be ignored based on
1376
+ * a list of gitignore-style patterns.
1377
+ *
1378
+ * @param relativePath - Path relative to the root directory
1379
+ * @param patterns - Array of gitignore-style patterns to test
1380
+ * @returns True if the path matches any ignore pattern
1381
+ */
1382
+ function matchesIgnorePattern(relativePath, patterns) {
1383
+ for (const pattern of patterns) {
1384
+ if (matchPattern(relativePath, pattern)) {
1385
+ return true;
1386
+ }
1387
+ }
1388
+ return false;
1389
+ }
1390
+ /**
1391
+ * Tests if the given path matches a gitignore-style pattern,
1392
+ * supporting negation patterns with '!' prefix.
1393
+ * Uses safe character-by-character matching to prevent ReDoS attacks.
1394
+ *
1395
+ * @param path - File or directory path to test
1396
+ * @param pattern - Gitignore-style pattern (may include wildcards)
1397
+ * @returns True if the path matches the pattern (or doesn't match if negated)
1398
+ */
1399
+ function matchPattern(path, pattern) {
1400
+ const normalizedPattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
1401
+ const isNegation = normalizedPattern.startsWith('!');
1402
+ const actualPattern = isNegation ? normalizedPattern.slice(1) : normalizedPattern;
1403
+ const matchesFullPath = matchGlobPattern(path, actualPattern) || matchGlobPattern(path, `**/${actualPattern}`);
1404
+ const matchesSegment = path.split('/').some((segment) => matchGlobPattern(segment, actualPattern));
1405
+ const matches = matchesFullPath || matchesSegment;
1406
+ return isNegation ? !matches : matches;
1407
+ }
1408
+ /**
1409
+ * Traverses a directory tree synchronously, calling a visitor function
1410
+ * for each file and directory encountered. Supports depth limiting,
1411
+ * hidden file filtering, and gitignore pattern matching.
1412
+ *
1413
+ * @param startPath - Root directory to begin traversal
1414
+ * @param visitor - Callback function invoked for each file system entry
1415
+ * @param options - Configuration for traversal behavior
1416
+ */
1417
+ function walkDirectory(startPath, visitor, options) {
1418
+ walkLogger.debug('Starting directory walk', {
1419
+ startPath,
1420
+ maxDepth: options?.maxDepth ?? -1,
1421
+ includeHidden: options?.includeHidden ?? false,
1422
+ respectGitignore: options?.respectGitignore ?? true,
1423
+ ignorePatterns: options?.ignorePatterns?.length ?? 0,
1424
+ });
1425
+ const maxDepth = options?.maxDepth ?? -1;
1426
+ const includeHidden = options?.includeHidden ?? false;
1427
+ const ignorePatterns = options?.ignorePatterns ?? [];
1428
+ const respectGitignore = options?.respectGitignore ?? true;
1429
+ const gitignorePatterns = respectGitignore ? loadGitignorePatterns(startPath) : [];
1430
+ const allIgnorePatterns = [...ignorePatterns, ...gitignorePatterns];
1431
+ if (gitignorePatterns.length > 0) {
1432
+ walkLogger.debug('Loaded gitignore patterns', { count: gitignorePatterns.length });
1433
+ }
1434
+ /**
1435
+ * Recursively walks directory entries, applying visitor to each.
1436
+ *
1437
+ * @param currentPath - Absolute path to current directory
1438
+ * @param relativePath - Path relative to the starting directory
1439
+ * @param depth - Current recursion depth
1440
+ * @returns False to stop walking, true to continue
1441
+ */
1442
+ function walk(currentPath, relativePath, depth) {
1443
+ if (maxDepth !== -1 && depth > maxDepth) {
1444
+ return true;
1445
+ }
1446
+ let entries;
1447
+ try {
1448
+ entries = readDirectory(currentPath);
1449
+ }
1450
+ catch {
1451
+ return true;
1452
+ }
1453
+ for (const entry of entries) {
1454
+ if (!includeHidden && entry.name.startsWith('.')) {
1455
+ continue;
1456
+ }
1457
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1458
+ if (matchesIgnorePattern(entryRelativePath, allIgnorePatterns)) {
1459
+ continue;
1460
+ }
1461
+ const walkEntry = {
1462
+ name: entry.name,
1463
+ path: entry.path,
1464
+ relativePath: entryRelativePath,
1465
+ isFile: entry.isFile,
1466
+ isDirectory: entry.isDirectory,
1467
+ isSymlink: entry.isSymlink,
1468
+ depth,
1469
+ };
1470
+ const result = visitor(walkEntry);
1471
+ if (result === 'stop') {
1472
+ return false;
1473
+ }
1474
+ if (result === 'skip') {
1475
+ continue;
1476
+ }
1477
+ if (entry.isDirectory) {
1478
+ const shouldContinue = walk(entry.path, entryRelativePath, depth + 1);
1479
+ if (!shouldContinue) {
1480
+ return false;
1481
+ }
1482
+ }
1483
+ }
1484
+ return true;
1485
+ }
1486
+ walk(startPath, '', 0);
1487
+ walkLogger.debug('Directory walk complete', { startPath });
1488
+ }
1489
+
1490
+ const searchLogger = createScopedLogger('project-scope:project:search');
1491
+ /**
1492
+ * Tests if a path matches at least one pattern from an array of globs,
1493
+ * enabling flexible multi-pattern file filtering.
1494
+ * Uses safe character-by-character matching to prevent ReDoS attacks.
1495
+ *
1496
+ * @param path - File path to test
1497
+ * @param patterns - Array of glob patterns
1498
+ * @returns True if path matches any pattern
1499
+ */
1500
+ function matchesPatterns(path, patterns) {
1501
+ return patterns.some((pattern) => matchGlobPattern(path, pattern));
1502
+ }
1503
+ /**
1504
+ * Searches a directory tree for files matching one or more glob patterns,
1505
+ * returning relative or absolute paths based on options.
1506
+ *
1507
+ * @param startPath - Root directory to begin the search
1508
+ * @param patterns - Glob patterns (e.g., '*.ts', '**\/*.json') to filter files
1509
+ * @param options - Configuration for search behavior
1510
+ * @returns List of relative file paths that match the patterns
1511
+ *
1512
+ * @example
1513
+ * ```typescript
1514
+ * import { findFiles } from '@hyperfrontend/project-scope'
1515
+ *
1516
+ * // Find all TypeScript files
1517
+ * const tsFiles = findFiles('./src', '\*\*\/*.ts')
1518
+ *
1519
+ * // Find multiple file types
1520
+ * const configFiles = findFiles('./', ['\*.json', '\*.yaml', '\*.yml'])
1521
+ *
1522
+ * // Limit results and get absolute paths
1523
+ * const first10 = findFiles('./src', '\*\*\/*.ts', {
1524
+ * maxResults: 10,
1525
+ * absolutePaths: true
1526
+ * })
1527
+ * ```
1528
+ */
1529
+ function findFiles(startPath, patterns, options) {
1530
+ const normalizedPatterns = isArray(patterns) ? patterns : [patterns];
1531
+ searchLogger.debug('Finding files', { startPath, patterns: normalizedPatterns, maxResults: options?.maxResults });
1532
+ const results = [];
1533
+ const maxResults = options?.maxResults ?? Infinity;
1534
+ walkDirectory(startPath, (entry) => {
1535
+ if (results.length >= maxResults) {
1536
+ return 'stop';
1537
+ }
1538
+ if (!entry.isFile) {
1539
+ return undefined;
1540
+ }
1541
+ if (matchesPatterns(entry.relativePath, normalizedPatterns)) {
1542
+ results.push(options?.absolutePaths ? entry.path : entry.relativePath);
1543
+ }
1544
+ return undefined;
1545
+ }, options);
1546
+ searchLogger.debug('File search complete', { startPath, matchCount: results.length });
1547
+ return results;
1548
+ }
1549
+
1550
+ createScopedLogger('project-scope:heuristics:entry-points');
1551
+ /**
1552
+ * Cache for entry point discovery results.
1553
+ * TTL: 60 seconds (entry points are relatively stable)
1554
+ */
1555
+ createCache({ ttl: 60000, maxSize: 50 });
1556
+
1557
+ createScopedLogger('project-scope:tech');
1558
+ /**
1559
+ * Cache for tech detection results.
1560
+ * TTL: 60 seconds (tech stack can change during active development)
1561
+ */
1562
+ createCache({ ttl: 60000, maxSize: 50 });
1563
+
1564
+ createScopedLogger('project-scope:heuristics:project-type');
1565
+
1566
+ const rootLogger = createScopedLogger('project-scope:root');
1567
+ /**
1568
+ * Files indicating workspace/monorepo root.
1569
+ */
1570
+ const WORKSPACE_MARKERS = ['nx.json', 'turbo.json', 'lerna.json', 'pnpm-workspace.yaml', 'rush.json'];
1571
+ /**
1572
+ * Find workspace root (monorepo root).
1573
+ * Searches up for workspace markers like nx.json, turbo.json, etc.
1574
+ *
1575
+ * @param startPath - Starting path
1576
+ * @returns Workspace root path or null
1577
+ *
1578
+ * @example
1579
+ * ```typescript
1580
+ * import { findWorkspaceRoot } from '@hyperfrontend/project-scope'
1581
+ *
1582
+ * const root = findWorkspaceRoot('./libs/my-lib')
1583
+ * if (root) {
1584
+ * console.log('Monorepo root:', root) // e.g., '/home/user/my-monorepo'
1585
+ * }
1586
+ * ```
1587
+ */
1588
+ function findWorkspaceRoot(startPath) {
1589
+ rootLogger.debug('Finding workspace root', { startPath });
1590
+ const byMarker = locateByMarkers(startPath, WORKSPACE_MARKERS);
1591
+ if (byMarker) {
1592
+ rootLogger.debug('Found workspace root by marker', { root: byMarker });
1593
+ return byMarker;
1594
+ }
1595
+ const byWorkspaces = findUpwardWhere(startPath, (dir) => {
1596
+ const pkg = readPackageJsonIfExists(dir);
1597
+ return pkg?.workspaces !== undefined;
1598
+ });
1599
+ if (byWorkspaces) {
1600
+ rootLogger.debug('Found workspace root by workspaces field', { root: byWorkspaces });
1601
+ return byWorkspaces;
1602
+ }
1603
+ const byPackage = findNearestPackageJson(startPath);
1604
+ if (byPackage) {
1605
+ rootLogger.debug('Found workspace root by package.json', { root: byPackage });
1606
+ }
1607
+ else {
1608
+ rootLogger.debug('Workspace root not found');
1609
+ }
1610
+ return byPackage;
1611
+ }
1612
+
1613
+ createScopedLogger('project-scope:nx');
1614
+
1615
+ createScopedLogger('project-scope:nx:devkit');
1616
+
1617
+ createScopedLogger('project-scope:nx:config');
1618
+
1619
+ createScopedLogger('project-scope:config');
1620
+ /**
1621
+ * Cache for config detection results.
1622
+ * TTL: 30 seconds (configs can change frequently during setup)
1623
+ */
1624
+ createCache({ ttl: 30000, maxSize: 50 });
1625
+
1626
+ /**
1627
+ * Safe copies of Number built-in methods and constants.
1628
+ *
1629
+ * These references are captured at module initialization time to protect against
1630
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1631
+ *
1632
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/number
1633
+ */
1634
+ // Capture references at module initialization time
1635
+ const _parseInt = globalThis.parseInt;
1636
+ const _isNaN = globalThis.isNaN;
1637
+ // ============================================================================
1638
+ // Parsing
1639
+ // ============================================================================
1640
+ /**
1641
+ * (Safe copy) Parses a string and returns an integer.
1642
+ */
1643
+ const parseInt = _parseInt;
1644
+ // ============================================================================
1645
+ // Global Type Checking (legacy, less strict)
1646
+ // ============================================================================
1647
+ /**
1648
+ * (Safe copy) Global isNaN function (coerces to number first, less strict than Number.isNaN).
1649
+ */
1650
+ const globalIsNaN = _isNaN;
1651
+
1652
+ /** Logger for analysis operations */
1653
+ createScopedLogger('project-scope:analyze');
1654
+
1655
+ /** Logger for CLI operations */
1656
+ createScopedLogger('project-scope:cli');
1657
+
1658
+ createScopedLogger('project-scope:encoding');
1659
+
1660
+ createScopedLogger('project-scope:encoding:convert');
1661
+
1662
+ createScopedLogger('project-scope:heuristics:framework');
1663
+ /**
1664
+ * Cache for framework identification results.
1665
+ * TTL: 60 seconds (frameworks are stable but can change during development)
1666
+ */
1667
+ createCache({ ttl: 60000, maxSize: 50 });
1668
+
1669
+ createScopedLogger('project-scope:vfs:tree');
1670
+
1671
+ createScopedLogger('project-scope:vfs:factory');
1672
+
1673
+ createScopedLogger('project-scope:vfs');
1674
+
1675
+ createScopedLogger('project-scope:vfs:diff');
1676
+
1677
+ /**
1678
+ * Dependency Graph
1679
+ *
1680
+ * Builds and analyzes dependency relationships between workspace projects.
1681
+ * Provides functions for traversing the dependency graph and determining
1682
+ * build/release order.
1683
+ */
1684
+ /**
1685
+ * Finds internal dependencies in a package.json.
1686
+ * Returns names of workspace packages that this package depends on.
1687
+ *
1688
+ * @param packageJson - Parsed package.json content
1689
+ * @param workspacePackageNames - Set of all package names in the workspace
1690
+ * @returns Array of internal dependency names
1691
+ *
1692
+ * @example
1693
+ * ```typescript
1694
+ * const internalDeps = findInternalDependencies(packageJson, allPackageNames)
1695
+ * // ['@scope/lib-a', '@scope/lib-b']
1696
+ * ```
1697
+ */
1698
+ function findInternalDependencies(packageJson, workspacePackageNames) {
1699
+ const internal = [];
1700
+ const allDeps = {
1701
+ ...packageJson.dependencies,
1702
+ ...packageJson.devDependencies,
1703
+ ...packageJson.peerDependencies,
1704
+ ...packageJson.optionalDependencies,
1705
+ };
1706
+ for (const depName of keys(allDeps)) {
1707
+ if (workspacePackageNames.has(depName)) {
1708
+ internal.push(depName);
1709
+ }
1710
+ }
1711
+ return internal;
1712
+ }
1713
+ /**
1714
+ * Finds internal dependencies with type information.
1715
+ *
1716
+ * @param packageName - Name of the package being analyzed
1717
+ * @param packageJson - Parsed package.json content
1718
+ * @param workspacePackageNames - Set of all package names in the workspace
1719
+ * @returns Array of dependency edges with type information
1720
+ */
1721
+ function findInternalDependenciesWithTypes(packageName, packageJson, workspacePackageNames) {
1722
+ const edges = [];
1723
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
1724
+ for (const depType of depTypes) {
1725
+ const deps = packageJson[depType];
1726
+ if (deps) {
1727
+ for (const [depName, versionRange] of entries(deps)) {
1728
+ if (workspacePackageNames.has(depName)) {
1729
+ edges.push({
1730
+ from: packageName,
1731
+ to: depName,
1732
+ type: depType,
1733
+ versionRange,
1734
+ });
1735
+ }
1736
+ }
1737
+ }
1738
+ }
1739
+ return edges;
1740
+ }
1741
+ /**
1742
+ * Builds a complete dependency graph from a list of projects.
1743
+ *
1744
+ * @param projects - List of projects to analyze
1745
+ * @returns Dependency graph analysis result
1746
+ *
1747
+ * @example
1748
+ * ```typescript
1749
+ * import { buildDependencyGraph, discoverPackages } from '@hyperfrontend/versioning'
1750
+ *
1751
+ * const { projects } = discoverPackages()
1752
+ * const analysis = buildDependencyGraph(projects)
1753
+ *
1754
+ * // Get packages that depend on 'lib-utils'
1755
+ * const dependents = analysis.dependencyGraph.get('lib-utils') ?? []
1756
+ *
1757
+ * // Get packages in topological order for building
1758
+ * const buildOrder = getTopologicalOrder(analysis)
1759
+ * ```
1760
+ */
1761
+ function buildDependencyGraph(projects) {
1762
+ const packageNames = createSet(projects.map((p) => p.name));
1763
+ const edges = [];
1764
+ // Collect all edges
1765
+ for (const project of projects) {
1766
+ const projectEdges = findInternalDependenciesWithTypes(project.name, project.packageJson, packageNames);
1767
+ edges.push(...projectEdges);
1768
+ }
1769
+ // Build forward graph (dependency -> dependents)
1770
+ const dependencyGraph = createMap();
1771
+ for (const name of packageNames) {
1772
+ dependencyGraph.set(name, []);
1773
+ }
1774
+ for (const edge of edges) {
1775
+ const dependents = dependencyGraph.get(edge.to);
1776
+ if (dependents) {
1777
+ dependents.push(edge.from);
1778
+ }
1779
+ }
1780
+ // Build reverse graph (package -> dependencies)
1781
+ const reverseDependencyGraph = createMap();
1782
+ for (const name of packageNames) {
1783
+ reverseDependencyGraph.set(name, []);
1784
+ }
1785
+ for (const edge of edges) {
1786
+ const deps = reverseDependencyGraph.get(edge.from);
1787
+ if (deps) {
1788
+ deps.push(edge.to);
1789
+ }
1790
+ }
1791
+ // Find leaf packages (no dependents)
1792
+ const leafPackages = [];
1793
+ for (const [name, dependents] of dependencyGraph) {
1794
+ if (dependents.length === 0) {
1795
+ leafPackages.push(name);
1796
+ }
1797
+ }
1798
+ // Find root packages (no dependencies)
1799
+ const rootPackages = [];
1800
+ for (const [name, deps] of reverseDependencyGraph) {
1801
+ if (deps.length === 0) {
1802
+ rootPackages.push(name);
1803
+ }
1804
+ }
1805
+ // Detect circular dependencies
1806
+ const { hasCircular, cycles } = detectCircularDependencies(reverseDependencyGraph);
1807
+ // Convert to readonly maps
1808
+ const readonlyDependencyGraph = createMap();
1809
+ for (const [key, value] of dependencyGraph) {
1810
+ readonlyDependencyGraph.set(key, [...value]);
1811
+ }
1812
+ const readonlyReverseDependencyGraph = createMap();
1813
+ for (const [key, value] of reverseDependencyGraph) {
1814
+ readonlyReverseDependencyGraph.set(key, [...value]);
1815
+ }
1816
+ return {
1817
+ dependencyGraph: readonlyDependencyGraph,
1818
+ reverseDependencyGraph: readonlyReverseDependencyGraph,
1819
+ edges,
1820
+ leafPackages,
1821
+ rootPackages,
1822
+ hasCircularDependencies: hasCircular,
1823
+ circularDependencies: cycles,
1824
+ };
1825
+ }
1826
+ /**
1827
+ * Detects circular dependencies in the graph using DFS.
1828
+ *
1829
+ * @param reverseDependencyGraph - Map of package to its dependencies
1830
+ * @returns Detection result with cycle information
1831
+ */
1832
+ function detectCircularDependencies(reverseDependencyGraph) {
1833
+ const cycles = [];
1834
+ const visited = createSet();
1835
+ const recursionStack = createSet();
1836
+ const path = [];
1837
+ /**
1838
+ * Depth-first search to detect cycles.
1839
+ *
1840
+ * @param node - Current node being visited
1841
+ * @returns True if a cycle was found
1842
+ */
1843
+ function dfs(node) {
1844
+ visited.add(node);
1845
+ recursionStack.add(node);
1846
+ path.push(node);
1847
+ const deps = reverseDependencyGraph.get(node) ?? [];
1848
+ for (const dep of deps) {
1849
+ if (!visited.has(dep)) {
1850
+ if (dfs(dep)) {
1851
+ return true;
1852
+ }
1853
+ }
1854
+ else if (recursionStack.has(dep)) {
1855
+ // Found a cycle
1856
+ const cycleStart = path.indexOf(dep);
1857
+ const cycle = path.slice(cycleStart);
1858
+ cycle.push(dep); // Close the cycle
1859
+ cycles.push(cycle);
1860
+ }
1861
+ }
1862
+ path.pop();
1863
+ recursionStack.delete(node);
1864
+ return false;
1865
+ }
1866
+ for (const node of reverseDependencyGraph.keys()) {
1867
+ if (!visited.has(node)) {
1868
+ dfs(node);
1869
+ }
1870
+ }
1871
+ return {
1872
+ hasCircular: cycles.length > 0,
1873
+ cycles,
1874
+ };
1875
+ }
1876
+ /**
1877
+ * Gets a topological ordering of packages for building.
1878
+ * Packages with no dependencies come first.
1879
+ *
1880
+ * @param analysis - Dependency graph analysis result
1881
+ * @returns Array of package names in build order
1882
+ * @throws {Error} If circular dependencies exist
1883
+ *
1884
+ * @example
1885
+ * ```typescript
1886
+ * const buildOrder = getTopologicalOrder(analysis)
1887
+ * for (const pkg of buildOrder) {
1888
+ * await build(pkg)
1889
+ * }
1890
+ * ```
1891
+ */
1892
+ function getTopologicalOrder(analysis) {
1893
+ if (analysis.hasCircularDependencies) {
1894
+ throw createError(`Circular dependencies detected: ${analysis.circularDependencies.map((c) => c.join(' -> ')).join(', ')}`);
1895
+ }
1896
+ const result = [];
1897
+ const inDegree = createMap();
1898
+ const adjacency = createMap();
1899
+ // Initialize
1900
+ for (const [name, deps] of analysis.reverseDependencyGraph) {
1901
+ inDegree.set(name, deps.length);
1902
+ adjacency.set(name, []);
1903
+ }
1904
+ // Build adjacency list (dependency -> dependents)
1905
+ for (const [name, deps] of analysis.reverseDependencyGraph) {
1906
+ for (const dep of deps) {
1907
+ const adj = adjacency.get(dep);
1908
+ if (adj) {
1909
+ adj.push(name);
1910
+ }
1911
+ }
1912
+ }
1913
+ // Kahn's algorithm
1914
+ const queue = [...analysis.rootPackages];
1915
+ while (queue.length > 0) {
1916
+ const node = queue.shift();
1917
+ if (node === undefined) {
1918
+ break;
1919
+ }
1920
+ result.push(node);
1921
+ const dependents = adjacency.get(node) ?? [];
1922
+ for (const dependent of dependents) {
1923
+ const degree = inDegree.get(dependent) ?? 0;
1924
+ inDegree.set(dependent, degree - 1);
1925
+ if (degree - 1 === 0) {
1926
+ queue.push(dependent);
1927
+ }
1928
+ }
1929
+ }
1930
+ return result;
1931
+ }
1932
+ /**
1933
+ * Gets all transitive dependents of a package (direct and indirect).
1934
+ *
1935
+ * @param workspace - The workspace containing projects
1936
+ * @param packageName - Name of the package to analyze
1937
+ * @returns Set of all packages that depend on this package
1938
+ *
1939
+ * @example
1940
+ * ```typescript
1941
+ * // If lib-a depends on lib-utils and app-main depends on lib-a
1942
+ * // Then getTransitiveDependents('lib-utils') returns ['lib-a', 'app-main']
1943
+ * ```
1944
+ */
1945
+ function getTransitiveDependents(workspace, packageName) {
1946
+ const dependents = createSet();
1947
+ const queue = [packageName];
1948
+ while (queue.length > 0) {
1949
+ const current = queue.shift();
1950
+ if (current === undefined) {
1951
+ break;
1952
+ }
1953
+ const directDependents = workspace.dependencyGraph.get(current) ?? [];
1954
+ for (const dep of directDependents) {
1955
+ if (!dependents.has(dep)) {
1956
+ dependents.add(dep);
1957
+ queue.push(dep);
1958
+ }
1959
+ }
1960
+ }
1961
+ return dependents;
1962
+ }
1963
+ /**
1964
+ * Gets all transitive dependencies of a package (direct and indirect).
1965
+ *
1966
+ * @param workspace - The workspace containing projects
1967
+ * @param packageName - Name of the package to analyze
1968
+ * @returns Set of all packages this package depends on
1969
+ */
1970
+ function getTransitiveDependencies(workspace, packageName) {
1971
+ const dependencies = createSet();
1972
+ const queue = [packageName];
1973
+ while (queue.length > 0) {
1974
+ const current = queue.shift();
1975
+ if (current === undefined) {
1976
+ break;
1977
+ }
1978
+ const directDeps = workspace.reverseDependencyGraph.get(current) ?? [];
1979
+ for (const dep of directDeps) {
1980
+ if (!dependencies.has(dep)) {
1981
+ dependencies.add(dep);
1982
+ queue.push(dep);
1983
+ }
1984
+ }
1985
+ }
1986
+ return dependencies;
1987
+ }
1988
+ /**
1989
+ * Checks if package A transitively depends on package B.
1990
+ *
1991
+ * @param workspace - The workspace containing projects
1992
+ * @param packageA - Name of the potentially dependent package
1993
+ * @param packageB - Name of the potential dependency
1994
+ * @returns True if packageA transitively depends on packageB
1995
+ */
1996
+ function transitivelyDependsOn(workspace, packageA, packageB) {
1997
+ const deps = getTransitiveDependencies(workspace, packageA);
1998
+ return deps.has(packageB);
1999
+ }
2000
+
2001
+ /**
2002
+ * Project Model
2003
+ *
2004
+ * Represents a single project/package within a workspace.
2005
+ * Contains package.json data, paths, and dependency information.
2006
+ */
2007
+ /**
2008
+ * Creates a new Project object.
2009
+ *
2010
+ * @param options - Project properties
2011
+ * @returns A new Project object
2012
+ */
2013
+ function createProject(options) {
2014
+ const isPrivate = options.packageJson['private'] === true;
2015
+ const publishable = !isPrivate && options.name !== undefined && options.version !== undefined;
2016
+ return {
2017
+ name: options.name,
2018
+ version: options.version,
2019
+ path: options.path,
2020
+ packageJsonPath: options.packageJsonPath,
2021
+ packageJson: options.packageJson,
2022
+ changelogPath: options.changelogPath ?? null,
2023
+ internalDependencies: options.internalDependencies ?? [],
2024
+ internalDependents: options.internalDependents ?? [],
2025
+ publishable,
2026
+ private: isPrivate,
2027
+ };
2028
+ }
2029
+ /**
2030
+ * Checks if a project is publishable (public and has name/version).
2031
+ *
2032
+ * @param project - The project to check
2033
+ * @returns True if the project can be published
2034
+ */
2035
+ function isPublishable(project) {
2036
+ return project.publishable;
2037
+ }
2038
+ /**
2039
+ * Checks if a project is private.
2040
+ *
2041
+ * @param project - The project to check
2042
+ * @returns True if the project is marked as private
2043
+ */
2044
+ function isPrivate(project) {
2045
+ return project.private;
2046
+ }
2047
+ /**
2048
+ * Checks if a project has a changelog file.
2049
+ *
2050
+ * @param project - The project to check
2051
+ * @returns True if changelog exists
2052
+ */
2053
+ function hasChangelog$1(project) {
2054
+ return project.changelogPath !== null;
2055
+ }
2056
+ /**
2057
+ * Checks if a project has any internal dependencies.
2058
+ *
2059
+ * @param project - The project to check
2060
+ * @returns True if project depends on other workspace packages
2061
+ */
2062
+ function hasInternalDependencies(project) {
2063
+ return project.internalDependencies.length > 0;
2064
+ }
2065
+ /**
2066
+ * Checks if a project has any internal dependents.
2067
+ *
2068
+ * @param project - The project to check
2069
+ * @returns True if other workspace packages depend on this project
2070
+ */
2071
+ function hasInternalDependents(project) {
2072
+ return project.internalDependents.length > 0;
2073
+ }
2074
+ /**
2075
+ * Gets the dependency count (internal dependencies).
2076
+ *
2077
+ * @param project - Project instance to analyze
2078
+ * @returns Number of internal dependencies
2079
+ */
2080
+ function getDependencyCount(project) {
2081
+ return project.internalDependencies.length;
2082
+ }
2083
+ /**
2084
+ * Gets the dependent count (packages that depend on this one).
2085
+ *
2086
+ * @param project - Project instance to analyze
2087
+ * @returns Number of internal dependents
2088
+ */
2089
+ function getDependentCount(project) {
2090
+ return project.internalDependents.length;
2091
+ }
2092
+ /**
2093
+ * Creates a copy of a project with updated internal dependents.
2094
+ *
2095
+ * @param project - The project to update
2096
+ * @param dependents - New list of internal dependents
2097
+ * @returns A new Project with updated dependents
2098
+ */
2099
+ function withDependents(project, dependents) {
2100
+ return {
2101
+ ...project,
2102
+ internalDependents: dependents,
2103
+ };
2104
+ }
2105
+ /**
2106
+ * Creates a copy of a project with an added internal dependent.
2107
+ *
2108
+ * @param project - The project to update
2109
+ * @param dependent - Name of the dependent to add
2110
+ * @returns A new Project with the added dependent
2111
+ */
2112
+ function addDependent(project, dependent) {
2113
+ if (project.internalDependents.includes(dependent)) {
2114
+ return project;
2115
+ }
2116
+ return {
2117
+ ...project,
2118
+ internalDependents: [...project.internalDependents, dependent],
2119
+ };
2120
+ }
2121
+
2122
+ /**
2123
+ * Workspace Model
2124
+ *
2125
+ * Represents a monorepo workspace with multiple projects.
2126
+ * Used for package discovery, dependency tracking, and coordinated versioning.
2127
+ */
2128
+ /**
2129
+ * Default workspace discovery patterns.
2130
+ */
2131
+ const DEFAULT_PATTERNS = [
2132
+ 'libs/*/package.json',
2133
+ 'apps/*/package.json',
2134
+ 'packages/*/package.json',
2135
+ 'tools/*/package.json',
2136
+ 'plugins/*/package.json',
2137
+ ];
2138
+ /**
2139
+ * Default exclusion patterns.
2140
+ */
2141
+ const DEFAULT_EXCLUDE = ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/.git/**'];
2142
+ /**
2143
+ * Default workspace configuration.
2144
+ */
2145
+ const DEFAULT_WORKSPACE_CONFIG = {
2146
+ patterns: DEFAULT_PATTERNS,
2147
+ exclude: DEFAULT_EXCLUDE,
2148
+ includeChangelogs: true,
2149
+ trackDependencies: true,
2150
+ };
2151
+ /**
2152
+ * Creates a new workspace configuration by merging with defaults.
2153
+ *
2154
+ * @param options - Partial configuration options
2155
+ * @returns Complete workspace configuration
2156
+ */
2157
+ function createWorkspaceConfig(options) {
2158
+ return {
2159
+ patterns: options?.patterns ?? DEFAULT_PATTERNS,
2160
+ exclude: options?.exclude ?? DEFAULT_EXCLUDE,
2161
+ includeChangelogs: options?.includeChangelogs ?? true,
2162
+ trackDependencies: options?.trackDependencies ?? true,
2163
+ };
2164
+ }
2165
+ /**
2166
+ * Creates a new workspace object.
2167
+ *
2168
+ * @param options - Workspace properties
2169
+ * @param options.root - Absolute path to workspace root directory
2170
+ * @param options.type - Type of workspace (nx, turbo, etc.)
2171
+ * @param options.projects - Map of project names to project objects
2172
+ * @param options.config - Configuration used for workspace discovery
2173
+ * @param options.dependencyGraph - Map of package names to their dependents
2174
+ * @param options.reverseDependencyGraph - Map of package names to their dependencies
2175
+ * @returns A new Workspace object
2176
+ */
2177
+ function createWorkspace(options) {
2178
+ const projectList = [...options.projects.values()].sort((a, b) => a.name.localeCompare(b.name));
2179
+ return {
2180
+ root: options.root,
2181
+ type: options.type,
2182
+ projects: options.projects,
2183
+ projectList,
2184
+ config: options.config,
2185
+ dependencyGraph: options.dependencyGraph,
2186
+ reverseDependencyGraph: options.reverseDependencyGraph,
2187
+ };
2188
+ }
2189
+ /**
2190
+ * Gets a project by name from the workspace.
2191
+ *
2192
+ * @param workspace - Workspace to search in
2193
+ * @param projectName - Identifier of the project to retrieve
2194
+ * @returns The project or undefined if not found
2195
+ */
2196
+ function getProject(workspace, projectName) {
2197
+ return workspace.projects.get(projectName);
2198
+ }
2199
+ /**
2200
+ * Checks if a project exists in the workspace.
2201
+ *
2202
+ * @param workspace - Workspace to search in
2203
+ * @param projectName - Identifier of the project to check
2204
+ * @returns True if the project exists
2205
+ */
2206
+ function hasProject(workspace, projectName) {
2207
+ return workspace.projects.has(projectName);
2208
+ }
2209
+ /**
2210
+ * Gets all project names in the workspace.
2211
+ *
2212
+ * @param workspace - Workspace to retrieve project names from
2213
+ * @returns Array of project names
2214
+ */
2215
+ function getProjectNames(workspace) {
2216
+ return workspace.projectList.map((p) => p.name);
2217
+ }
2218
+ /**
2219
+ * Gets the count of projects in the workspace.
2220
+ *
2221
+ * @param workspace - Workspace to count projects in
2222
+ * @returns Number of projects
2223
+ */
2224
+ function getProjectCount(workspace) {
2225
+ return workspace.projects.size;
2226
+ }
2227
+ /**
2228
+ * Gets projects that depend on the given project.
2229
+ *
2230
+ * @param workspace - Workspace containing the dependency graph
2231
+ * @param projectName - Name of the dependency
2232
+ * @returns Array of dependent project names
2233
+ */
2234
+ function getDependents(workspace, projectName) {
2235
+ return workspace.dependencyGraph.get(projectName) ?? [];
2236
+ }
2237
+ /**
2238
+ * Gets projects that the given project depends on.
2239
+ *
2240
+ * @param workspace - Workspace containing the dependency graph
2241
+ * @param projectName - Identifier of the project to look up
2242
+ * @returns Array of dependency project names
2243
+ */
2244
+ function getDependencies(workspace, projectName) {
2245
+ return workspace.reverseDependencyGraph.get(projectName) ?? [];
2246
+ }
2247
+ /**
2248
+ * Checks if projectA depends on projectB.
2249
+ *
2250
+ * @param workspace - Workspace containing the dependency graph
2251
+ * @param projectA - Name of the potentially dependent project
2252
+ * @param projectB - Name of the potential dependency
2253
+ * @returns True if projectA depends on projectB
2254
+ */
2255
+ function dependsOn(workspace, projectA, projectB) {
2256
+ const deps = getDependencies(workspace, projectA);
2257
+ return deps.includes(projectB);
2258
+ }
2259
+
2260
+ /**
2261
+ * Changelog Discovery
2262
+ *
2263
+ * Discovers CHANGELOG.md files within workspace projects.
2264
+ */
2265
+ /**
2266
+ * Common changelog file names in priority order.
2267
+ */
2268
+ const CHANGELOG_NAMES = ['CHANGELOG.md', 'Changelog.md', 'changelog.md', 'HISTORY.md', 'CHANGES.md'];
2269
+ /**
2270
+ * Finds changelog files for a list of packages.
2271
+ * Returns a map of project path to changelog absolute path.
2272
+ *
2273
+ * @param workspaceRoot - Workspace root path
2274
+ * @param packages - List of packages to find changelogs for
2275
+ * @returns Map of project path to changelog path
2276
+ */
2277
+ function findChangelogs(workspaceRoot, packages) {
2278
+ const result = createMap();
2279
+ for (const pkg of packages) {
2280
+ const changelogPath = findProjectChangelog(pkg.path);
2281
+ if (changelogPath) {
2282
+ result.set(pkg.path, changelogPath);
2283
+ }
2284
+ }
2285
+ return result;
2286
+ }
2287
+ /**
2288
+ * Finds the changelog file for a single project.
2289
+ * Checks for common changelog names in order of priority.
2290
+ *
2291
+ * @param projectPath - Path to project directory
2292
+ * @returns Absolute path to changelog or null if not found
2293
+ *
2294
+ * @example
2295
+ * ```typescript
2296
+ * import { findProjectChangelog } from '@hyperfrontend/versioning'
2297
+ *
2298
+ * const changelogPath = findProjectChangelog('./libs/my-lib')
2299
+ * if (changelogPath) {
2300
+ * console.log('Found changelog:', changelogPath)
2301
+ * }
2302
+ * ```
2303
+ */
2304
+ function findProjectChangelog(projectPath) {
2305
+ for (const name of CHANGELOG_NAMES) {
2306
+ const changelogPath = node_path.join(projectPath, name);
2307
+ if (exists(changelogPath)) {
2308
+ return changelogPath;
2309
+ }
2310
+ }
2311
+ return null;
2312
+ }
2313
+ /**
2314
+ * Discovers all changelog files within a workspace.
2315
+ *
2316
+ * @param workspaceRoot - Workspace root path
2317
+ * @param patterns - Glob patterns for finding changelogs (default: all CHANGELOGs)
2318
+ * @returns Array of discovered changelog information
2319
+ *
2320
+ * @example
2321
+ * ```typescript
2322
+ * import { discoverAllChangelogs } from '@hyperfrontend/versioning'
2323
+ *
2324
+ * const changelogs = discoverAllChangelogs('/path/to/workspace')
2325
+ * for (const changelog of changelogs) {
2326
+ * console.log(`${changelog.projectPath} -> ${changelog.path}`)
2327
+ * }
2328
+ * ```
2329
+ */
2330
+ function discoverAllChangelogs(workspaceRoot, patterns = ['**/CHANGELOG.md', '**/Changelog.md', '**/changelog.md']) {
2331
+ const results = [];
2332
+ const files = findFiles(workspaceRoot, [...patterns], {
2333
+ ignorePatterns: ['**/node_modules/**', '**/dist/**'],
2334
+ absolutePaths: false,
2335
+ });
2336
+ for (const relativePath of files) {
2337
+ const absolutePath = node_path.join(workspaceRoot, relativePath);
2338
+ const projectPath = node_path.dirname(absolutePath);
2339
+ const filename = relativePath.split('/').pop() ?? 'CHANGELOG.md';
2340
+ results.push({
2341
+ path: absolutePath,
2342
+ relativePath,
2343
+ projectPath,
2344
+ filename,
2345
+ });
2346
+ }
2347
+ return results;
2348
+ }
2349
+
2350
+ /**
2351
+ * Package Discovery
2352
+ *
2353
+ * Discovers packages within a workspace by finding package.json files
2354
+ * and extracting relevant metadata. Uses project-scope for file operations.
2355
+ */
2356
+ /**
2357
+ * Discovers all packages within a workspace.
2358
+ * Finds package.json files, parses them, and optionally discovers
2359
+ * changelogs and internal dependencies.
2360
+ *
2361
+ * @param options - Discovery options
2362
+ * @returns Discovery result with all found packages
2363
+ * @throws {Error} If workspace root cannot be found
2364
+ *
2365
+ * @example
2366
+ * ```typescript
2367
+ * import { discoverPackages } from '@hyperfrontend/versioning'
2368
+ *
2369
+ * // Discover all packages in current workspace
2370
+ * const result = discoverPackages()
2371
+ *
2372
+ * // Discover with custom patterns
2373
+ * const result = discoverPackages({
2374
+ * patterns: ['packages/*\/package.json'],
2375
+ * includeChangelogs: true
2376
+ * })
2377
+ *
2378
+ * // Access discovered projects
2379
+ * for (const project of result.projects) {
2380
+ * console.log(`${project.name}@${project.version}`)
2381
+ * }
2382
+ * ```
2383
+ */
2384
+ function discoverPackages(options = {}) {
2385
+ // Resolve workspace root
2386
+ const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot(process.cwd());
2387
+ if (!workspaceRoot) {
2388
+ throw createError('Could not find workspace root. Ensure you are in a monorepo with nx.json, turbo.json, or workspaces field.');
2389
+ }
2390
+ // Build configuration
2391
+ const config = {
2392
+ patterns: options.patterns ?? DEFAULT_WORKSPACE_CONFIG.patterns,
2393
+ exclude: options.exclude ?? DEFAULT_WORKSPACE_CONFIG.exclude,
2394
+ includeChangelogs: options.includeChangelogs ?? DEFAULT_WORKSPACE_CONFIG.includeChangelogs,
2395
+ trackDependencies: options.trackDependencies ?? DEFAULT_WORKSPACE_CONFIG.trackDependencies,
2396
+ };
2397
+ // Find all package.json files
2398
+ const packageJsonPaths = findPackageJsonFiles(workspaceRoot, config);
2399
+ // Parse package.json files
2400
+ const rawPackages = parsePackageJsonFiles(workspaceRoot, packageJsonPaths);
2401
+ // Collect all package names for internal dependency detection
2402
+ const packageNames = createSet(rawPackages.map((p) => p.name));
2403
+ // Find changelogs if requested
2404
+ const changelogMap = config.includeChangelogs ? findChangelogs(workspaceRoot, rawPackages) : createMap();
2405
+ // Build projects with changelog paths
2406
+ const rawWithChangelogs = rawPackages.map((pkg) => ({
2407
+ ...pkg,
2408
+ changelogPath: changelogMap.get(pkg.path) ?? null,
2409
+ }));
2410
+ // Calculate internal dependencies
2411
+ const projects = config.trackDependencies
2412
+ ? buildProjectsWithDependencies(rawWithChangelogs, packageNames)
2413
+ : rawWithChangelogs.map((pkg) => createProject(pkg));
2414
+ // Build project map
2415
+ const projectMap = createMap();
2416
+ for (const project of projects) {
2417
+ projectMap.set(project.name, project);
2418
+ }
2419
+ return {
2420
+ projects,
2421
+ projectMap,
2422
+ packageNames,
2423
+ workspaceRoot,
2424
+ config,
2425
+ };
2426
+ }
2427
+ /**
2428
+ * Finds all package.json files matching the configured patterns.
2429
+ *
2430
+ * @param workspaceRoot - Root directory to search from
2431
+ * @param config - Workspace configuration
2432
+ * @returns Array of relative paths to package.json files
2433
+ */
2434
+ function findPackageJsonFiles(workspaceRoot, config) {
2435
+ const patterns = [...config.patterns];
2436
+ const excludePatterns = [...config.exclude];
2437
+ return findFiles(workspaceRoot, patterns, {
2438
+ ignorePatterns: excludePatterns,
2439
+ });
2440
+ }
2441
+ /**
2442
+ * Parses package.json files and extracts metadata.
2443
+ *
2444
+ * @param workspaceRoot - Workspace root path
2445
+ * @param packageJsonPaths - Relative paths to package.json files
2446
+ * @returns Array of raw package info objects
2447
+ */
2448
+ function parsePackageJsonFiles(workspaceRoot, packageJsonPaths) {
2449
+ const packages = [];
2450
+ for (const relativePath of packageJsonPaths) {
2451
+ const absolutePath = node_path.join(workspaceRoot, relativePath);
2452
+ const projectPath = node_path.dirname(absolutePath);
2453
+ try {
2454
+ const packageJson = readPackageJson(absolutePath);
2455
+ // Skip packages without a name
2456
+ if (!packageJson.name) {
2457
+ continue;
2458
+ }
2459
+ packages.push({
2460
+ name: packageJson.name,
2461
+ version: packageJson.version ?? '0.0.0',
2462
+ path: projectPath,
2463
+ packageJsonPath: absolutePath,
2464
+ packageJson,
2465
+ changelogPath: null,
2466
+ });
2467
+ }
2468
+ catch {
2469
+ // Skip packages that can't be parsed
2470
+ continue;
2471
+ }
2472
+ }
2473
+ return packages;
2474
+ }
2475
+ /**
2476
+ * Builds projects with internal dependency information.
2477
+ *
2478
+ * @param rawPackages - Raw package info objects
2479
+ * @param packageNames - Set of all package names
2480
+ * @returns Array of Project objects with dependencies populated
2481
+ */
2482
+ function buildProjectsWithDependencies(rawPackages, packageNames) {
2483
+ // First pass: create projects with dependencies
2484
+ const projectsWithDeps = [];
2485
+ for (const pkg of rawPackages) {
2486
+ const internalDeps = findInternalDependencies(pkg.packageJson, packageNames);
2487
+ projectsWithDeps.push({
2488
+ ...pkg,
2489
+ internalDependencies: internalDeps,
2490
+ });
2491
+ }
2492
+ // Build dependency -> dependents map
2493
+ const dependentsMap = createMap();
2494
+ for (const pkg of projectsWithDeps) {
2495
+ for (const dep of pkg.internalDependencies) {
2496
+ const existing = dependentsMap.get(dep) ?? [];
2497
+ existing.push(pkg.name);
2498
+ dependentsMap.set(dep, existing);
2499
+ }
2500
+ }
2501
+ // Second pass: add dependents to each project
2502
+ return projectsWithDeps.map((pkg) => {
2503
+ const dependents = dependentsMap.get(pkg.name) ?? [];
2504
+ return createProject({
2505
+ ...pkg,
2506
+ internalDependents: dependents,
2507
+ });
2508
+ });
2509
+ }
2510
+ /**
2511
+ * Discovers a single project by path.
2512
+ *
2513
+ * @param projectPath - Path to project directory or package.json
2514
+ * @returns The discovered project or null if not found
2515
+ */
2516
+ function discoverProject(projectPath) {
2517
+ const packageJsonPath = projectPath.endsWith('package.json') ? projectPath : node_path.join(projectPath, 'package.json');
2518
+ const projectDir = projectPath.endsWith('package.json') ? node_path.dirname(projectPath) : projectPath;
2519
+ try {
2520
+ const packageJson = readPackageJson(packageJsonPath);
2521
+ if (!packageJson.name) {
2522
+ return null;
2523
+ }
2524
+ return createProject({
2525
+ name: packageJson.name,
2526
+ version: packageJson.version ?? '0.0.0',
2527
+ path: projectDir,
2528
+ packageJsonPath,
2529
+ packageJson,
2530
+ changelogPath: null,
2531
+ });
2532
+ }
2533
+ catch {
2534
+ return null;
2535
+ }
2536
+ }
2537
+ /**
2538
+ * Discovers a project by name within a workspace.
2539
+ *
2540
+ * @param projectName - Name of the project to find
2541
+ * @param options - Discovery options
2542
+ * @returns The project or null if not found
2543
+ */
2544
+ function discoverProjectByName(projectName, options = {}) {
2545
+ const result = discoverPackages(options);
2546
+ return result.projectMap.get(projectName) ?? null;
2547
+ }
2548
+
2549
+ /**
2550
+ * Changelog Path Utilities
2551
+ *
2552
+ * Functions for checking changelog existence and resolving expected paths.
2553
+ */
2554
+ /**
2555
+ * Checks if a project has a changelog file.
2556
+ *
2557
+ * @param projectPath - Directory containing the project to check
2558
+ * @returns True if changelog exists
2559
+ */
2560
+ function hasChangelog(projectPath) {
2561
+ return findProjectChangelog(projectPath) !== null;
2562
+ }
2563
+ /**
2564
+ * Gets the expected changelog path for a project.
2565
+ * Returns the standard CHANGELOG.md path regardless of whether it exists.
2566
+ *
2567
+ * @param projectPath - Directory containing the project files
2568
+ * @returns Absolute path to CHANGELOG.md in the project directory
2569
+ */
2570
+ function getExpectedChangelogPath(projectPath) {
2571
+ return node_path.join(projectPath, 'CHANGELOG.md');
2572
+ }
2573
+
2574
+ /**
2575
+ * Converts a SemVer to its canonical string representation.
2576
+ *
2577
+ * @param version - The version to format
2578
+ * @returns The version string (e.g., "1.2.3-alpha.1+build.123")
2579
+ */
2580
+ function format(version) {
2581
+ let result = `${version.major}.${version.minor}.${version.patch}`;
2582
+ if (version.prerelease.length > 0) {
2583
+ result += '-' + version.prerelease.join('.');
2584
+ }
2585
+ if (version.build.length > 0) {
2586
+ result += '+' + version.build.join('.');
2587
+ }
2588
+ return result;
2589
+ }
2590
+
2591
+ /**
2592
+ * Creates a new SemVer object.
2593
+ *
2594
+ * @param options - Version components
2595
+ * @returns A new SemVer object
2596
+ */
2597
+ function createSemVer(options) {
2598
+ return {
2599
+ major: options.major,
2600
+ minor: options.minor,
2601
+ patch: options.patch,
2602
+ prerelease: options.prerelease ?? [],
2603
+ build: options.build ?? [],
2604
+ raw: options.raw,
2605
+ };
2606
+ }
2607
+
2608
+ /**
2609
+ * Increments a version based on the bump type.
2610
+ *
2611
+ * @param version - The version to increment
2612
+ * @param type - The type of bump (major, minor, patch, etc.)
2613
+ * @param prereleaseId - Optional prerelease identifier for prerelease bumps
2614
+ * @returns A new incremented SemVer
2615
+ *
2616
+ * @example
2617
+ * increment(parseVersion('1.2.3'), 'minor') // 1.3.0
2618
+ * increment(parseVersion('1.2.3'), 'major') // 2.0.0
2619
+ * increment(parseVersion('1.2.3'), 'prerelease', 'alpha') // 1.2.4-alpha.0
2620
+ */
2621
+ function increment(version, type, prereleaseId) {
2622
+ switch (type) {
2623
+ case 'major':
2624
+ return createSemVer({
2625
+ major: version.major + 1,
2626
+ minor: 0,
2627
+ patch: 0,
2628
+ prerelease: [],
2629
+ build: [],
2630
+ });
2631
+ case 'minor':
2632
+ return createSemVer({
2633
+ major: version.major,
2634
+ minor: version.minor + 1,
2635
+ patch: 0,
2636
+ prerelease: [],
2637
+ build: [],
2638
+ });
2639
+ case 'patch':
2640
+ // If version has prerelease, just remove it (1.2.3-alpha -> 1.2.3)
2641
+ if (version.prerelease.length > 0) {
2642
+ return createSemVer({
2643
+ major: version.major,
2644
+ minor: version.minor,
2645
+ patch: version.patch,
2646
+ prerelease: [],
2647
+ build: [],
2648
+ });
2649
+ }
2650
+ return createSemVer({
2651
+ major: version.major,
2652
+ minor: version.minor,
2653
+ patch: version.patch + 1,
2654
+ prerelease: [],
2655
+ build: [],
2656
+ });
2657
+ case 'premajor':
2658
+ return createSemVer({
2659
+ major: version.major + 1,
2660
+ minor: 0,
2661
+ patch: 0,
2662
+ prerelease: [prereleaseId ?? 'alpha', '0'],
2663
+ build: [],
2664
+ });
2665
+ case 'preminor':
2666
+ return createSemVer({
2667
+ major: version.major,
2668
+ minor: version.minor + 1,
2669
+ patch: 0,
2670
+ prerelease: [prereleaseId ?? 'alpha', '0'],
2671
+ build: [],
2672
+ });
2673
+ case 'prepatch':
2674
+ return createSemVer({
2675
+ major: version.major,
2676
+ minor: version.minor,
2677
+ patch: version.patch + 1,
2678
+ prerelease: [prereleaseId ?? 'alpha', '0'],
2679
+ build: [],
2680
+ });
2681
+ case 'prerelease':
2682
+ return incrementPrerelease(version, prereleaseId);
2683
+ case 'none':
2684
+ default:
2685
+ return version;
2686
+ }
2687
+ }
2688
+ /**
2689
+ * Increments the prerelease portion of a version.
2690
+ *
2691
+ * @param version - The version to increment
2692
+ * @param id - Optional prerelease identifier
2693
+ * @returns A new version with incremented prerelease
2694
+ */
2695
+ function incrementPrerelease(version, id) {
2696
+ const prerelease = [...version.prerelease];
2697
+ if (prerelease.length === 0) {
2698
+ // No existing prerelease - start at patch+1 with id.0
2699
+ return createSemVer({
2700
+ major: version.major,
2701
+ minor: version.minor,
2702
+ patch: version.patch + 1,
2703
+ prerelease: [id ?? 'alpha', '0'],
2704
+ build: [],
2705
+ });
2706
+ }
2707
+ // Check if the last identifier is numeric
2708
+ const lastIdx = prerelease.length - 1;
2709
+ const last = prerelease[lastIdx];
2710
+ const lastNum = parseInt(last, 10);
2711
+ if (!globalIsNaN(lastNum)) {
2712
+ // Increment the numeric part
2713
+ prerelease[lastIdx] = String(lastNum + 1);
2714
+ }
2715
+ else {
2716
+ // Append .0
2717
+ prerelease.push('0');
2718
+ }
2719
+ // If a different id is specified, replace the base identifier
2720
+ if (id && prerelease.length > 0 && prerelease[0] !== id) {
2721
+ prerelease[0] = id;
2722
+ // Reset numeric part
2723
+ if (prerelease.length > 1) {
2724
+ prerelease[prerelease.length - 1] = '0';
2725
+ }
2726
+ }
2727
+ return createSemVer({
2728
+ major: version.major,
2729
+ minor: version.minor,
2730
+ patch: version.patch,
2731
+ prerelease,
2732
+ build: [],
2733
+ });
2734
+ }
2735
+
2736
+ /**
2737
+ * Maximum version string length to prevent memory exhaustion.
2738
+ */
2739
+ const MAX_VERSION_LENGTH = 256;
2740
+ /**
2741
+ * Parses a semantic version string.
2742
+ *
2743
+ * Accepts versions in the format: MAJOR.MINOR.PATCH[-prerelease][+build]
2744
+ * Optional leading 'v' or '=' prefixes are stripped.
2745
+ *
2746
+ * @param input - The version string to parse
2747
+ * @returns A ParseVersionResult with the parsed version or error
2748
+ *
2749
+ * @example
2750
+ * parseVersion('1.2.3') // { success: true, version: { major: 1, minor: 2, patch: 3, ... } }
2751
+ * parseVersion('v1.0.0-alpha.1+build.123') // { success: true, ... }
2752
+ * parseVersion('invalid') // { success: false, error: '...' }
2753
+ */
2754
+ function parseVersion(input) {
2755
+ // Input validation
2756
+ if (!input || typeof input !== 'string') {
2757
+ return { success: false, error: 'Version string is required' };
2758
+ }
2759
+ if (input.length > MAX_VERSION_LENGTH) {
2760
+ return { success: false, error: `Version string exceeds maximum length of ${MAX_VERSION_LENGTH}` };
2761
+ }
2762
+ // Strip leading whitespace
2763
+ let pos = 0;
2764
+ while (pos < input.length && isWhitespace(input.charCodeAt(pos))) {
2765
+ pos++;
2766
+ }
2767
+ // Strip trailing whitespace
2768
+ let end = input.length;
2769
+ while (end > pos && isWhitespace(input.charCodeAt(end - 1))) {
2770
+ end--;
2771
+ }
2772
+ // Strip optional leading 'v' or '='
2773
+ if (pos < end) {
2774
+ const code = input.charCodeAt(pos);
2775
+ if (code === 118 || code === 86) {
2776
+ // 'v' or 'V'
2777
+ pos++;
2778
+ }
2779
+ else if (code === 61) {
2780
+ // '='
2781
+ pos++;
2782
+ }
2783
+ }
2784
+ // Parse major version
2785
+ const majorResult = parseNumericIdentifier(input, pos, end);
2786
+ if (!majorResult.success) {
2787
+ return { success: false, error: majorResult.error ?? 'Invalid major version' };
2788
+ }
2789
+ pos = majorResult.endPos;
2790
+ // Expect dot
2791
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
2792
+ // '.'
2793
+ return { success: false, error: 'Expected "." after major version' };
2794
+ }
2795
+ pos++;
2796
+ // Parse minor version
2797
+ const minorResult = parseNumericIdentifier(input, pos, end);
2798
+ if (!minorResult.success) {
2799
+ return { success: false, error: minorResult.error ?? 'Invalid minor version' };
2800
+ }
2801
+ pos = minorResult.endPos;
2802
+ // Expect dot
2803
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
2804
+ // '.'
2805
+ return { success: false, error: 'Expected "." after minor version' };
2806
+ }
2807
+ pos++;
2808
+ // Parse patch version
2809
+ const patchResult = parseNumericIdentifier(input, pos, end);
2810
+ if (!patchResult.success) {
2811
+ return { success: false, error: patchResult.error ?? 'Invalid patch version' };
2812
+ }
2813
+ pos = patchResult.endPos;
2814
+ // Parse optional prerelease
2815
+ const prerelease = [];
2816
+ if (pos < end && input.charCodeAt(pos) === 45) {
2817
+ // '-'
2818
+ pos++;
2819
+ const prereleaseResult = parseIdentifiers(input, pos, end, [43]); // Stop at '+'
2820
+ if (!prereleaseResult.success) {
2821
+ return { success: false, error: prereleaseResult.error ?? 'Invalid prerelease' };
2822
+ }
2823
+ prerelease.push(...prereleaseResult.identifiers);
2824
+ pos = prereleaseResult.endPos;
2825
+ }
2826
+ // Parse optional build metadata
2827
+ const build = [];
2828
+ if (pos < end && input.charCodeAt(pos) === 43) {
2829
+ // '+'
2830
+ pos++;
2831
+ const buildResult = parseIdentifiers(input, pos, end, []);
2832
+ if (!buildResult.success) {
2833
+ return { success: false, error: buildResult.error ?? 'Invalid build metadata' };
2834
+ }
2835
+ build.push(...buildResult.identifiers);
2836
+ pos = buildResult.endPos;
2837
+ }
2838
+ // Check for trailing characters
2839
+ if (pos < end) {
2840
+ return { success: false, error: `Unexpected character at position ${pos}: "${input[pos]}"` };
2841
+ }
2842
+ return {
2843
+ success: true,
2844
+ version: createSemVer({
2845
+ major: majorResult.value,
2846
+ minor: minorResult.value,
2847
+ patch: patchResult.value,
2848
+ prerelease,
2849
+ build,
2850
+ raw: input,
2851
+ }),
2852
+ };
2853
+ }
2854
+ /**
2855
+ * Parses a numeric identifier (non-negative integer, no leading zeros except for "0").
2856
+ *
2857
+ * @param input - Input string to parse
2858
+ * @param start - Start position in the input
2859
+ * @param end - End position in the input
2860
+ * @returns Numeric parsing result
2861
+ */
2862
+ function parseNumericIdentifier(input, start, end) {
2863
+ if (start >= end) {
2864
+ return { success: false, value: 0, endPos: start, error: 'Expected numeric identifier' };
2865
+ }
2866
+ let pos = start;
2867
+ const firstCode = input.charCodeAt(pos);
2868
+ // Must start with a digit
2869
+ if (!isDigit(firstCode)) {
2870
+ return { success: false, value: 0, endPos: pos, error: 'Expected digit' };
2871
+ }
2872
+ // Check for leading zero (only "0" is valid, not "01", "007", etc.)
2873
+ if (firstCode === 48 && pos + 1 < end && isDigit(input.charCodeAt(pos + 1))) {
2874
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier cannot have leading zeros' };
2875
+ }
2876
+ // Consume digits
2877
+ let value = 0;
2878
+ while (pos < end && isDigit(input.charCodeAt(pos))) {
2879
+ value = value * 10 + (input.charCodeAt(pos) - 48);
2880
+ pos++;
2881
+ // Prevent overflow
2882
+ if (value > Number.MAX_SAFE_INTEGER) {
2883
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier is too large' };
2884
+ }
2885
+ }
2886
+ return { success: true, value, endPos: pos };
2887
+ }
2888
+ /**
2889
+ * Parses dot-separated identifiers (for prerelease/build).
2890
+ *
2891
+ * @param input - Input string to parse
2892
+ * @param start - Start position in the input
2893
+ * @param end - End position in the input
2894
+ * @param stopCodes - Character codes that signal end of identifiers
2895
+ * @returns Identifiers parsing result
2896
+ */
2897
+ function parseIdentifiers(input, start, end, stopCodes) {
2898
+ const identifiers = [];
2899
+ let pos = start;
2900
+ while (pos < end) {
2901
+ // Check for stop characters
2902
+ if (stopCodes.includes(input.charCodeAt(pos))) {
2903
+ break;
2904
+ }
2905
+ // Parse one identifier
2906
+ const identStart = pos;
2907
+ while (pos < end) {
2908
+ const code = input.charCodeAt(pos);
2909
+ // Stop at dot or stop characters
2910
+ if (code === 46 || stopCodes.includes(code)) {
2911
+ break;
2912
+ }
2913
+ // Must be alphanumeric or hyphen
2914
+ if (!isAlphanumeric(code) && code !== 45) {
2915
+ return { success: false, identifiers: [], endPos: pos, error: `Invalid character in identifier: "${input[pos]}"` };
2916
+ }
2917
+ pos++;
2918
+ }
2919
+ // Empty identifier is not allowed
2920
+ if (pos === identStart) {
2921
+ return { success: false, identifiers: [], endPos: pos, error: 'Empty identifier' };
2922
+ }
2923
+ identifiers.push(input.slice(identStart, pos));
2924
+ // Consume dot separator
2925
+ if (pos < end && input.charCodeAt(pos) === 46) {
2926
+ pos++;
2927
+ // Dot at end is invalid
2928
+ if (pos >= end || stopCodes.includes(input.charCodeAt(pos))) {
2929
+ return { success: false, identifiers: [], endPos: pos, error: 'Identifier expected after dot' };
2930
+ }
2931
+ }
2932
+ }
2933
+ return { success: true, identifiers, endPos: pos };
2934
+ }
2935
+ /**
2936
+ * Checks if a character code is a digit (0-9).
2937
+ *
2938
+ * @param code - Character code to check
2939
+ * @returns True if the code represents a digit
2940
+ */
2941
+ function isDigit(code) {
2942
+ return code >= 48 && code <= 57;
2943
+ }
2944
+ /**
2945
+ * Checks if a character code is alphanumeric or hyphen.
2946
+ *
2947
+ * @param code - Character code to check
2948
+ * @returns True if the code represents an alphanumeric character
2949
+ */
2950
+ function isAlphanumeric(code) {
2951
+ return ((code >= 48 && code <= 57) || // 0-9
2952
+ (code >= 65 && code <= 90) || // A-Z
2953
+ (code >= 97 && code <= 122) // a-z
2954
+ );
2955
+ }
2956
+ /**
2957
+ * Checks if a character code is whitespace.
2958
+ *
2959
+ * @param code - Character code to check
2960
+ * @returns True if the code represents whitespace
2961
+ */
2962
+ function isWhitespace(code) {
2963
+ return code === 32 || code === 9 || code === 10 || code === 13;
2964
+ }
2965
+
2966
+ /**
2967
+ * Cascade Bump
2968
+ *
2969
+ * Calculates which packages need version bumps when dependencies change.
2970
+ * Implements cascade versioning for monorepos where dependents may need
2971
+ * to be bumped when their dependencies are updated.
2972
+ */
2973
+ /**
2974
+ * Default cascade bump options.
2975
+ */
2976
+ const DEFAULT_CASCADE_OPTIONS = {
2977
+ cascadeBumpType: 'patch',
2978
+ includeDevDependencies: false,
2979
+ includePeerDependencies: true,
2980
+ prereleaseId: 'alpha',
2981
+ };
2982
+ /**
2983
+ * Calculates cascade bumps for a workspace given direct bumps.
2984
+ *
2985
+ * When packages are directly bumped (e.g., due to commits), their dependents
2986
+ * may also need version bumps. This function calculates all affected packages.
2987
+ *
2988
+ * @param workspace - Workspace containing projects and dependency graph
2989
+ * @param directBumps - Packages with direct changes
2990
+ * @param options - Configuration for cascade bump calculation
2991
+ * @returns Cascade bump result
2992
+ *
2993
+ * @example
2994
+ * ```typescript
2995
+ * import { calculateCascadeBumps } from '@hyperfrontend/versioning'
2996
+ *
2997
+ * // If lib-utils is getting a minor bump
2998
+ * const result = calculateCascadeBumps(workspace, [
2999
+ * { name: 'lib-utils', bumpType: 'minor' }
3000
+ * ])
3001
+ *
3002
+ * // result.bumps includes lib-utils and all packages that depend on it
3003
+ * for (const bump of result.bumps) {
3004
+ * console.log(`${bump.name}: ${bump.currentVersion} -> ${bump.nextVersion}`)
3005
+ * }
3006
+ * ```
3007
+ */
3008
+ function calculateCascadeBumps(workspace, directBumps, options = {}) {
3009
+ const opts = { ...DEFAULT_CASCADE_OPTIONS, ...options };
3010
+ const directBumpMap = createMap(directBumps.map((b) => [b.name, b]));
3011
+ const allBumps = createMap();
3012
+ // Process direct bumps first
3013
+ for (const input of directBumps) {
3014
+ const project = workspace.projects.get(input.name);
3015
+ if (!project) {
3016
+ continue;
3017
+ }
3018
+ const planned = createPlannedBump(project, input.bumpType, 'direct', [], opts.prereleaseId);
3019
+ allBumps.set(input.name, planned);
3020
+ }
3021
+ // Calculate cascade bumps
3022
+ const processed = createSet();
3023
+ const queue = [...directBumps.map((b) => b.name)];
3024
+ while (queue.length > 0) {
3025
+ const current = queue.shift();
3026
+ if (current === undefined || processed.has(current)) {
3027
+ continue;
3028
+ }
3029
+ processed.add(current);
3030
+ // Get dependents
3031
+ const dependents = getTransitiveDependents(workspace, current);
3032
+ for (const depName of dependents) {
3033
+ // Skip if already has a direct bump
3034
+ if (directBumpMap.has(depName)) {
3035
+ continue;
3036
+ }
3037
+ // Skip if already planned
3038
+ if (allBumps.has(depName)) {
3039
+ // Update triggered by list
3040
+ const existing = allBumps.get(depName);
3041
+ if (existing && !existing.triggeredBy.includes(current)) {
3042
+ allBumps.set(depName, {
3043
+ ...existing,
3044
+ triggeredBy: [...existing.triggeredBy, current],
3045
+ });
3046
+ }
3047
+ continue;
3048
+ }
3049
+ const project = workspace.projects.get(depName);
3050
+ if (!project) {
3051
+ continue;
3052
+ }
3053
+ // Check if we should include this dependent based on dependency type
3054
+ if (!shouldCascade(workspace, depName, current, opts)) {
3055
+ continue;
3056
+ }
3057
+ const planned = createPlannedBump(project, opts.cascadeBumpType, 'cascade', [current], opts.prereleaseId);
3058
+ allBumps.set(depName, planned);
3059
+ // Continue cascading from this dependent
3060
+ queue.push(depName);
3061
+ }
3062
+ }
3063
+ // Convert to arrays and categorize
3064
+ const bumps = [...allBumps.values()];
3065
+ const directBumpsArray = bumps.filter((b) => b.reason === 'direct');
3066
+ const cascadeBumpsArray = bumps.filter((b) => b.reason === 'cascade');
3067
+ // Sort bumps by name for consistent output
3068
+ bumps.sort((a, b) => a.name.localeCompare(b.name));
3069
+ return {
3070
+ bumps,
3071
+ directBumps: directBumpsArray,
3072
+ cascadeBumps: cascadeBumpsArray,
3073
+ totalAffected: bumps.length,
3074
+ };
3075
+ }
3076
+ /**
3077
+ * Determines if a cascade should propagate based on dependency type.
3078
+ *
3079
+ * @param workspace - Workspace containing the project
3080
+ * @param dependent - Name of the dependent package
3081
+ * @param dependency - Name of the dependency being bumped
3082
+ * @param opts - Cascade bump options
3083
+ * @returns True if the bump should cascade to this dependent
3084
+ */
3085
+ function shouldCascade(workspace, dependent, dependency, opts) {
3086
+ const project = workspace.projects.get(dependent);
3087
+ if (!project) {
3088
+ return false;
3089
+ }
3090
+ const pkg = project.packageJson;
3091
+ // Check production dependencies (always cascades)
3092
+ if (pkg.dependencies?.[dependency]) {
3093
+ return true;
3094
+ }
3095
+ // Check dev dependencies
3096
+ if (opts.includeDevDependencies && pkg.devDependencies?.[dependency]) {
3097
+ return true;
3098
+ }
3099
+ // Check peer dependencies
3100
+ if (opts.includePeerDependencies && pkg.peerDependencies?.[dependency]) {
3101
+ return true;
3102
+ }
3103
+ return false;
3104
+ }
3105
+ /**
3106
+ * Creates a planned bump for a project.
3107
+ *
3108
+ * @param project - Project to create bump plan for
3109
+ * @param bumpType - Type of version bump to apply
3110
+ * @param reason - Reason for the bump (direct, cascade, or sync)
3111
+ * @param triggeredBy - List of packages that triggered this bump
3112
+ * @param prereleaseId - Optional prerelease identifier
3113
+ * @returns Planned bump object with version information
3114
+ */
3115
+ function createPlannedBump(project, bumpType, reason, triggeredBy, prereleaseId) {
3116
+ const parseResult = parseVersion(project.version);
3117
+ if (!parseResult.success || !parseResult.version) {
3118
+ throw createError(`Invalid version for ${project.name}: ${project.version}`);
3119
+ }
3120
+ const next = computeNextVersion(parseResult.version, bumpType, prereleaseId);
3121
+ return {
3122
+ name: project.name,
3123
+ currentVersion: project.version,
3124
+ nextVersion: format(next),
3125
+ bumpType,
3126
+ reason,
3127
+ triggeredBy,
3128
+ };
3129
+ }
3130
+ /**
3131
+ * Computes the next version based on bump type.
3132
+ *
3133
+ * @param current - Current semantic version
3134
+ * @param bumpType - Type of version bump to apply
3135
+ * @param prereleaseId - Optional prerelease identifier
3136
+ * @returns New semantic version after bump
3137
+ */
3138
+ function computeNextVersion(current, bumpType, prereleaseId) {
3139
+ if (bumpType === 'none') {
3140
+ return current;
3141
+ }
3142
+ return increment(current, bumpType, prereleaseId);
3143
+ }
3144
+ /**
3145
+ * Calculates cascade bumps starting from a single package.
3146
+ *
3147
+ * @param workspace - Workspace containing projects and dependency graph
3148
+ * @param packageName - Package with direct changes
3149
+ * @param bumpType - Type of bump for the direct change
3150
+ * @param options - Configuration for cascade behavior
3151
+ * @returns Cascade bump result
3152
+ */
3153
+ function calculateCascadeBumpsFromPackage(workspace, packageName, bumpType, options = {}) {
3154
+ return calculateCascadeBumps(workspace, [{ name: packageName, bumpType }], options);
3155
+ }
3156
+ /**
3157
+ * Gets a summary of the cascade bump calculation.
3158
+ *
3159
+ * @param result - Result object from cascade bump calculation
3160
+ * @returns Human-readable summary
3161
+ */
3162
+ function summarizeCascadeBumps(result) {
3163
+ if (result.totalAffected === 0) {
3164
+ return 'No packages affected';
3165
+ }
3166
+ const lines = [];
3167
+ lines.push(`${result.totalAffected} package(s) affected:`);
3168
+ lines.push(` - ${result.directBumps.length} direct bump(s)`);
3169
+ lines.push(` - ${result.cascadeBumps.length} cascade bump(s)`);
3170
+ lines.push('');
3171
+ lines.push('Planned bumps:');
3172
+ for (const bump of result.bumps) {
3173
+ const suffix = bump.reason === 'cascade' ? ` (triggered by ${bump.triggeredBy.join(', ')})` : '';
3174
+ lines.push(` ${bump.name}: ${bump.currentVersion} -> ${bump.nextVersion} [${bump.bumpType}]${suffix}`);
3175
+ }
3176
+ return lines.join('\n');
3177
+ }
3178
+
3179
+ /**
3180
+ * Batch Update
3181
+ *
3182
+ * Utilities for updating multiple packages at once.
3183
+ * Supports updating versions, dependencies, and other package.json fields.
3184
+ */
3185
+ /**
3186
+ * Default batch update options.
3187
+ */
3188
+ const DEFAULT_BATCH_UPDATE_OPTIONS = {
3189
+ dryRun: false,
3190
+ updateDependencyReferences: true,
3191
+ };
3192
+ /**
3193
+ * Applies planned bumps to the workspace.
3194
+ * Updates package.json version fields for all affected packages.
3195
+ *
3196
+ * @param workspace - Workspace containing projects to update
3197
+ * @param bumps - Planned version bumps
3198
+ * @param options - Update options
3199
+ * @returns Batch update result
3200
+ *
3201
+ * @example
3202
+ * ```typescript
3203
+ * import { applyBumps, calculateCascadeBumps } from '@hyperfrontend/versioning'
3204
+ *
3205
+ * const cascadeResult = calculateCascadeBumps(workspace, directBumps)
3206
+ * const updateResult = applyBumps(workspace, cascadeResult.bumps)
3207
+ *
3208
+ * if (updateResult.success) {
3209
+ * console.log(`Updated ${updateResult.updated.length} packages`)
3210
+ * } else {
3211
+ * console.error('Some updates failed:', updateResult.failed)
3212
+ * }
3213
+ * ```
3214
+ */
3215
+ function applyBumps(workspace, bumps, options = {}) {
3216
+ const opts = { ...DEFAULT_BATCH_UPDATE_OPTIONS, ...options };
3217
+ const updated = [];
3218
+ const failed = [];
3219
+ // Build a map for dependency reference updates
3220
+ const versionUpdates = createMap();
3221
+ for (const bump of bumps) {
3222
+ versionUpdates.set(bump.name, bump.nextVersion);
3223
+ }
3224
+ for (const bump of bumps) {
3225
+ const project = workspace.projects.get(bump.name);
3226
+ if (!project) {
3227
+ failed.push({
3228
+ name: bump.name,
3229
+ packageJsonPath: '',
3230
+ error: 'Project not found in workspace',
3231
+ });
3232
+ continue;
3233
+ }
3234
+ try {
3235
+ if (!opts.dryRun) {
3236
+ updatePackageVersion(project.packageJsonPath, bump.nextVersion);
3237
+ }
3238
+ updated.push({
3239
+ name: bump.name,
3240
+ packageJsonPath: project.packageJsonPath,
3241
+ previousVersion: bump.currentVersion,
3242
+ newVersion: bump.nextVersion,
3243
+ });
3244
+ }
3245
+ catch (error) {
3246
+ failed.push({
3247
+ name: bump.name,
3248
+ packageJsonPath: project.packageJsonPath,
3249
+ error: error instanceof Error ? error.message : String(error),
3250
+ });
3251
+ }
3252
+ }
3253
+ // Update dependency references if requested
3254
+ if (opts.updateDependencyReferences && !opts.dryRun) {
3255
+ for (const project of workspace.projectList) {
3256
+ try {
3257
+ updateDependencyReferences(project.packageJsonPath, versionUpdates);
3258
+ }
3259
+ catch {
3260
+ // Dependency reference updates are best-effort
3261
+ }
3262
+ }
3263
+ }
3264
+ return {
3265
+ updated,
3266
+ failed,
3267
+ total: bumps.length,
3268
+ success: failed.length === 0,
3269
+ };
3270
+ }
3271
+ /**
3272
+ * Updates the version field in a package.json file.
3273
+ *
3274
+ * @param packageJsonPath - Path to package.json
3275
+ * @param newVersion - New version string
3276
+ */
3277
+ function updatePackageVersion(packageJsonPath, newVersion) {
3278
+ const content = readFileContent(packageJsonPath);
3279
+ const pkg = parse(content);
3280
+ pkg.version = newVersion;
3281
+ const formatted = stringify(pkg, null, 2) + '\n';
3282
+ writeFileContent(packageJsonPath, formatted);
3283
+ }
3284
+ /**
3285
+ * Updates dependency version references in a package.json file.
3286
+ *
3287
+ * @param packageJsonPath - Path to package.json
3288
+ * @param versionUpdates - Map of package name to new version
3289
+ */
3290
+ function updateDependencyReferences(packageJsonPath, versionUpdates) {
3291
+ const content = readFileContent(packageJsonPath);
3292
+ const pkg = parse(content);
3293
+ let modified = false;
3294
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
3295
+ for (const depType of depTypes) {
3296
+ const deps = pkg[depType];
3297
+ if (deps) {
3298
+ for (const [name, newVersion] of versionUpdates) {
3299
+ if (deps[name]) {
3300
+ // Preserve version prefix (^, ~, etc.) or use exact version
3301
+ const currentRange = deps[name];
3302
+ const prefix = extractVersionPrefix(currentRange);
3303
+ deps[name] = prefix + newVersion;
3304
+ modified = true;
3305
+ }
3306
+ }
3307
+ }
3308
+ }
3309
+ if (modified) {
3310
+ const formatted = stringify(pkg, null, 2) + '\n';
3311
+ writeFileContent(packageJsonPath, formatted);
3312
+ }
3313
+ }
3314
+ /**
3315
+ * Extracts the version prefix from a version range.
3316
+ *
3317
+ * @param versionRange - Version range string
3318
+ * @returns The prefix (^, ~, >=, etc.) or empty string
3319
+ */
3320
+ function extractVersionPrefix(versionRange) {
3321
+ if (versionRange.startsWith('^'))
3322
+ return '^';
3323
+ if (versionRange.startsWith('~'))
3324
+ return '~';
3325
+ if (versionRange.startsWith('>='))
3326
+ return '>=';
3327
+ if (versionRange.startsWith('>'))
3328
+ return '>';
3329
+ if (versionRange.startsWith('<='))
3330
+ return '<=';
3331
+ if (versionRange.startsWith('<'))
3332
+ return '<';
3333
+ if (versionRange.startsWith('='))
3334
+ return '=';
3335
+ return '';
3336
+ }
3337
+ /**
3338
+ * Updates a package.json file using a VFS Tree.
3339
+ *
3340
+ * @param tree - Virtual file system tree
3341
+ * @param packageJsonPath - Relative path to package.json
3342
+ * @param newVersion - New version string
3343
+ */
3344
+ function updatePackageVersionInTree(tree, packageJsonPath, newVersion) {
3345
+ const content = tree.read(packageJsonPath, 'utf-8');
3346
+ if (!content) {
3347
+ throw createError(`Could not read ${packageJsonPath}`);
3348
+ }
3349
+ const pkg = parse(content);
3350
+ pkg.version = newVersion;
3351
+ const formatted = stringify(pkg, null, 2) + '\n';
3352
+ tree.write(packageJsonPath, formatted);
3353
+ }
3354
+ /**
3355
+ * Creates a summary of the batch update result.
3356
+ *
3357
+ * @param result - Result object from batch update operation
3358
+ * @returns Human-readable summary
3359
+ */
3360
+ function summarizeBatchUpdate(result) {
3361
+ const lines = [];
3362
+ if (result.success) {
3363
+ lines.push(`Successfully updated ${result.updated.length} package(s)`);
3364
+ }
3365
+ else {
3366
+ lines.push(`Updated ${result.updated.length}/${result.total} package(s)`);
3367
+ lines.push(`Failed: ${result.failed.length} package(s)`);
3368
+ }
3369
+ if (result.updated.length > 0) {
3370
+ lines.push('');
3371
+ lines.push('Updated packages:');
3372
+ for (const pkg of result.updated) {
3373
+ lines.push(` ${pkg.name}: ${pkg.previousVersion} -> ${pkg.newVersion}`);
3374
+ }
3375
+ }
3376
+ if (result.failed.length > 0) {
3377
+ lines.push('');
3378
+ lines.push('Failed packages:');
3379
+ for (const pkg of result.failed) {
3380
+ lines.push(` ${pkg.name}: ${pkg.error}`);
3381
+ }
3382
+ }
3383
+ return lines.join('\n');
3384
+ }
3385
+
3386
+ /**
3387
+ * Compares two semantic versions.
3388
+ *
3389
+ * @param a - First version
3390
+ * @param b - Second version
3391
+ * @returns -1 if a < b, 0 if a == b, 1 if a > b
3392
+ *
3393
+ * @example
3394
+ * compare(parseVersion('1.0.0'), parseVersion('2.0.0')) // -1
3395
+ * compare(parseVersion('1.0.0'), parseVersion('1.0.0')) // 0
3396
+ * compare(parseVersion('2.0.0'), parseVersion('1.0.0')) // 1
3397
+ */
3398
+ function compare(a, b) {
3399
+ // Compare major, minor, patch
3400
+ if (a.major !== b.major) {
3401
+ return a.major < b.major ? -1 : 1;
3402
+ }
3403
+ if (a.minor !== b.minor) {
3404
+ return a.minor < b.minor ? -1 : 1;
3405
+ }
3406
+ if (a.patch !== b.patch) {
3407
+ return a.patch < b.patch ? -1 : 1;
3408
+ }
3409
+ // Compare prerelease
3410
+ // Version with prerelease has lower precedence than release
3411
+ if (a.prerelease.length === 0 && b.prerelease.length > 0) {
3412
+ return 1; // a is release, b is prerelease -> a > b
3413
+ }
3414
+ if (a.prerelease.length > 0 && b.prerelease.length === 0) {
3415
+ return -1; // a is prerelease, b is release -> a < b
3416
+ }
3417
+ // Both have prerelease - compare identifiers
3418
+ const maxLen = max(a.prerelease.length, b.prerelease.length);
3419
+ for (let i = 0; i < maxLen; i++) {
3420
+ const aId = a.prerelease[i];
3421
+ const bId = b.prerelease[i];
3422
+ // Shorter prerelease array has lower precedence
3423
+ if (aId === undefined && bId !== undefined) {
3424
+ return -1;
3425
+ }
3426
+ if (aId !== undefined && bId === undefined) {
3427
+ return 1;
3428
+ }
3429
+ if (aId === undefined || bId === undefined) {
3430
+ continue;
3431
+ }
3432
+ // Compare identifiers
3433
+ const cmp = compareIdentifiers(aId, bId);
3434
+ if (cmp !== 0) {
3435
+ return cmp;
3436
+ }
3437
+ }
3438
+ return 0;
3439
+ }
3440
+ /**
3441
+ * Checks if a version satisfies a comparator.
3442
+ *
3443
+ * @param version - Version to check
3444
+ * @param comparator - Comparator to test against
3445
+ * @returns True if version satisfies the comparator
3446
+ */
3447
+ function satisfiesComparator(version, comparator) {
3448
+ const cmp = compare(version, comparator.version);
3449
+ switch (comparator.operator) {
3450
+ case '=':
3451
+ return cmp === 0;
3452
+ case '>':
3453
+ return cmp === 1;
3454
+ case '>=':
3455
+ return cmp >= 0;
3456
+ case '<':
3457
+ return cmp === -1;
3458
+ case '<=':
3459
+ return cmp <= 0;
3460
+ case '^':
3461
+ case '~':
3462
+ // These should have been expanded during parsing
3463
+ // If we encounter them here, treat as >=
3464
+ return cmp >= 0;
3465
+ default:
3466
+ return false;
3467
+ }
3468
+ }
3469
+ /**
3470
+ * Checks if a version satisfies a range.
3471
+ *
3472
+ * @param version - Version to check
3473
+ * @param range - Range to test against
3474
+ * @returns True if version satisfies the range
3475
+ *
3476
+ * @example
3477
+ * satisfies(parseVersion('1.2.3'), parseRange('^1.0.0')) // true
3478
+ * satisfies(parseVersion('2.0.0'), parseRange('^1.0.0')) // false
3479
+ */
3480
+ function satisfies(version, range) {
3481
+ // Empty range matches any
3482
+ if (range.sets.length === 0) {
3483
+ return true;
3484
+ }
3485
+ // OR logic: at least one set must be satisfied
3486
+ for (const set of range.sets) {
3487
+ // AND logic: all comparators in set must be satisfied
3488
+ let allSatisfied = true;
3489
+ // Empty comparator set matches any
3490
+ if (set.comparators.length === 0) {
3491
+ return true;
3492
+ }
3493
+ for (const comp of set.comparators) {
3494
+ if (!satisfiesComparator(version, comp)) {
3495
+ allSatisfied = false;
3496
+ break;
3497
+ }
3498
+ }
3499
+ if (allSatisfied) {
3500
+ return true;
3501
+ }
3502
+ }
3503
+ return false;
3504
+ }
3505
+ // ============================================================================
3506
+ // Internal helpers
3507
+ // ============================================================================
3508
+ /**
3509
+ * Compares two prerelease identifiers.
3510
+ * Numeric identifiers have lower precedence than alphanumeric.
3511
+ * Numeric identifiers are compared numerically.
3512
+ * Alphanumeric identifiers are compared lexically.
3513
+ *
3514
+ * @param a - First prerelease identifier
3515
+ * @param b - Second prerelease identifier
3516
+ * @returns -1 if a < b, 0 if equal, 1 if a > b
3517
+ */
3518
+ function compareIdentifiers(a, b) {
3519
+ const aIsNumeric = isNumeric(a);
3520
+ const bIsNumeric = isNumeric(b);
3521
+ // Numeric identifiers have lower precedence
3522
+ if (aIsNumeric && !bIsNumeric) {
3523
+ return -1;
3524
+ }
3525
+ if (!aIsNumeric && bIsNumeric) {
3526
+ return 1;
3527
+ }
3528
+ // Both numeric - compare as numbers
3529
+ if (aIsNumeric && bIsNumeric) {
3530
+ const aNum = parseInt(a, 10);
3531
+ const bNum = parseInt(b, 10);
3532
+ if (aNum < bNum)
3533
+ return -1;
3534
+ if (aNum > bNum)
3535
+ return 1;
3536
+ return 0;
3537
+ }
3538
+ // Both alphanumeric - compare lexically
3539
+ if (a < b)
3540
+ return -1;
3541
+ if (a > b)
3542
+ return 1;
3543
+ return 0;
3544
+ }
3545
+ /**
3546
+ * Checks if a string consists only of digits.
3547
+ *
3548
+ * @param str - String to check for numeric content
3549
+ * @returns True if string contains only digits
3550
+ */
3551
+ function isNumeric(str) {
3552
+ if (str.length === 0)
3553
+ return false;
3554
+ for (let i = 0; i < str.length; i++) {
3555
+ const code = str.charCodeAt(i);
3556
+ if (code < 48 || code > 57) {
3557
+ return false;
3558
+ }
3559
+ }
3560
+ return true;
3561
+ }
3562
+
3563
+ /**
3564
+ * Creates a new Comparator.
3565
+ *
3566
+ * @param operator - The comparison operator
3567
+ * @param version - The version to compare against
3568
+ * @returns A new Comparator
3569
+ */
3570
+ function createComparator(operator, version) {
3571
+ return { operator, version };
3572
+ }
3573
+ /**
3574
+ * Creates a new ComparatorSet.
3575
+ *
3576
+ * @param comparators - Array of comparators (AND logic)
3577
+ * @returns A new ComparatorSet
3578
+ */
3579
+ function createComparatorSet(comparators) {
3580
+ return { comparators };
3581
+ }
3582
+ /**
3583
+ * Creates a new Range.
3584
+ *
3585
+ * @param sets - Array of comparator sets (OR logic)
3586
+ * @param raw - Original raw string
3587
+ * @returns A new Range
3588
+ */
3589
+ function createRange(sets, raw) {
3590
+ return { sets, raw };
3591
+ }
3592
+
3593
+ /**
3594
+ * Maximum range string length.
3595
+ */
3596
+ const MAX_RANGE_LENGTH = 1024;
3597
+ /**
3598
+ * Parses a semver range string.
3599
+ *
3600
+ * Supports:
3601
+ * - Exact: 1.2.3, =1.2.3
3602
+ * - Comparators: >1.0.0, >=1.0.0, <2.0.0, <=2.0.0
3603
+ * - Caret: ^1.2.3 (compatible with version)
3604
+ * - Tilde: ~1.2.3 (approximately equivalent)
3605
+ * - X-ranges: 1.x, 1.2.x, *
3606
+ * - Hyphen ranges: 1.0.0 - 2.0.0
3607
+ * - OR: 1.0.0 || 2.0.0
3608
+ * - AND: >=1.0.0 <2.0.0
3609
+ *
3610
+ * @param input - The range string to parse
3611
+ * @returns A ParseRangeResult with the parsed range or error
3612
+ */
3613
+ function parseRange(input) {
3614
+ if (!input || typeof input !== 'string') {
3615
+ return { success: false, error: 'Range string is required' };
3616
+ }
3617
+ if (input.length > MAX_RANGE_LENGTH) {
3618
+ return { success: false, error: `Range string exceeds maximum length of ${MAX_RANGE_LENGTH}` };
3619
+ }
3620
+ // Trim whitespace
3621
+ const trimmed = input.trim();
3622
+ // Handle wildcard/any
3623
+ if (trimmed === '' || trimmed === '*' || trimmed.toLowerCase() === 'x') {
3624
+ return { success: true, range: createRange([], input) };
3625
+ }
3626
+ // Split by || for OR logic
3627
+ const orParts = splitByOr(trimmed);
3628
+ const sets = [];
3629
+ for (const part of orParts) {
3630
+ const setResult = parseComparatorSet(part.trim());
3631
+ if (!setResult.success) {
3632
+ return { success: false, error: setResult.error };
3633
+ }
3634
+ if (setResult.set) {
3635
+ sets.push(setResult.set);
3636
+ }
3637
+ }
3638
+ return { success: true, range: createRange(sets, input) };
3639
+ }
3640
+ /**
3641
+ * Splits a string by || delimiter, respecting nesting.
3642
+ *
3643
+ * @param input - Range string containing OR groups
3644
+ * @returns Array of OR-separated parts
3645
+ */
3646
+ function splitByOr(input) {
3647
+ const parts = [];
3648
+ let current = '';
3649
+ let pos = 0;
3650
+ while (pos < input.length) {
3651
+ if (input[pos] === '|' && pos + 1 < input.length && input[pos + 1] === '|') {
3652
+ parts.push(current);
3653
+ current = '';
3654
+ pos += 2;
3655
+ }
3656
+ else {
3657
+ current += input[pos];
3658
+ pos++;
3659
+ }
3660
+ }
3661
+ parts.push(current);
3662
+ return parts;
3663
+ }
3664
+ /**
3665
+ * Parses a single comparator set (space-separated comparators = AND logic).
3666
+ *
3667
+ * @param input - Comparator set string
3668
+ * @returns Parsed set result
3669
+ */
3670
+ function parseComparatorSet(input) {
3671
+ if (!input || input.trim() === '') {
3672
+ return { success: true }; // Empty set matches any
3673
+ }
3674
+ const trimmed = input.trim();
3675
+ // Check for hyphen range: "1.0.0 - 2.0.0"
3676
+ const hyphenMatch = parseHyphenRange(trimmed);
3677
+ if (hyphenMatch.isHyphenRange) {
3678
+ if (!hyphenMatch.success) {
3679
+ return { success: false, error: hyphenMatch.error };
3680
+ }
3681
+ return { success: true, set: hyphenMatch.set };
3682
+ }
3683
+ // Split by whitespace for AND logic
3684
+ const tokens = splitByWhitespace(trimmed);
3685
+ const comparators = [];
3686
+ for (const token of tokens) {
3687
+ const compResult = parseSingleComparator(token);
3688
+ if (!compResult.success) {
3689
+ return { success: false, error: compResult.error };
3690
+ }
3691
+ if (compResult.comparators) {
3692
+ comparators.push(...compResult.comparators);
3693
+ }
3694
+ }
3695
+ if (comparators.length === 0) {
3696
+ return { success: true }; // Empty matches any
3697
+ }
3698
+ return { success: true, set: createComparatorSet(comparators) };
3699
+ }
3700
+ /**
3701
+ * Checks for and parses hyphen ranges like "1.0.0 - 2.0.0".
3702
+ *
3703
+ * @param input - Potential hyphen range string
3704
+ * @returns Hyphen range parsing result
3705
+ */
3706
+ function parseHyphenRange(input) {
3707
+ // Look for " - " (space-hyphen-space)
3708
+ let hyphenPos = -1;
3709
+ for (let i = 0; i < input.length - 2; i++) {
3710
+ if (input[i] === ' ' && input[i + 1] === '-' && input[i + 2] === ' ') {
3711
+ hyphenPos = i;
3712
+ break;
3713
+ }
3714
+ }
3715
+ if (hyphenPos === -1) {
3716
+ return { isHyphenRange: false, success: true };
3717
+ }
3718
+ const leftPart = input.slice(0, hyphenPos).trim();
3719
+ const rightPart = input.slice(hyphenPos + 3).trim();
3720
+ const leftVersion = parseSimpleVersion(leftPart);
3721
+ if (!leftVersion) {
3722
+ return { isHyphenRange: true, success: false, error: `Invalid left side of hyphen range: "${leftPart}"` };
3723
+ }
3724
+ const rightVersion = parseSimpleVersion(rightPart);
3725
+ if (!rightVersion) {
3726
+ return { isHyphenRange: true, success: false, error: `Invalid right side of hyphen range: "${rightPart}"` };
3727
+ }
3728
+ // Hyphen range: >=left <=right
3729
+ const comparators = [createComparator('>=', leftVersion), createComparator('<=', rightVersion)];
3730
+ return {
3731
+ isHyphenRange: true,
3732
+ success: true,
3733
+ set: createComparatorSet(comparators),
3734
+ };
3735
+ }
3736
+ /**
3737
+ * Splits by whitespace.
3738
+ *
3739
+ * @param input - String to split
3740
+ * @returns Array of whitespace-separated tokens
3741
+ */
3742
+ function splitByWhitespace(input) {
3743
+ const tokens = [];
3744
+ let current = '';
3745
+ for (const char of input) {
3746
+ if (char === ' ' || char === '\t') {
3747
+ if (current) {
3748
+ tokens.push(current);
3749
+ current = '';
3750
+ }
3751
+ }
3752
+ else {
3753
+ current += char;
3754
+ }
3755
+ }
3756
+ if (current) {
3757
+ tokens.push(current);
3758
+ }
3759
+ return tokens;
3760
+ }
3761
+ /**
3762
+ * Parses a single comparator token (e.g., ">=1.0.0", "^1.2.3", "~1.0").
3763
+ *
3764
+ * @param token - Comparator token to parse
3765
+ * @returns Parsed comparator result
3766
+ */
3767
+ function parseSingleComparator(token) {
3768
+ let pos = 0;
3769
+ let operator = '=';
3770
+ // Parse operator
3771
+ if (token[pos] === '^') {
3772
+ operator = '^';
3773
+ pos++;
3774
+ }
3775
+ else if (token[pos] === '~') {
3776
+ operator = '~';
3777
+ pos++;
3778
+ }
3779
+ else if (token[pos] === '>') {
3780
+ if (token[pos + 1] === '=') {
3781
+ operator = '>=';
3782
+ pos += 2;
3783
+ }
3784
+ else {
3785
+ operator = '>';
3786
+ pos++;
3787
+ }
3788
+ }
3789
+ else if (token[pos] === '<') {
3790
+ if (token[pos + 1] === '=') {
3791
+ operator = '<=';
3792
+ pos += 2;
3793
+ }
3794
+ else {
3795
+ operator = '<';
3796
+ pos++;
3797
+ }
3798
+ }
3799
+ else if (token[pos] === '=') {
3800
+ operator = '=';
3801
+ pos++;
3802
+ }
3803
+ const versionPart = token.slice(pos);
3804
+ // Handle wildcards: *, x, X
3805
+ if (versionPart === '*' || versionPart.toLowerCase() === 'x') {
3806
+ // Wildcard matches any - return empty (will be handled as match-all)
3807
+ return { success: true, comparators: [] };
3808
+ }
3809
+ // Handle x-ranges: 1.x, 1.2.x
3810
+ if (versionPart.includes('x') || versionPart.includes('X') || versionPart.includes('*')) {
3811
+ return parseXRange(versionPart);
3812
+ }
3813
+ // Parse version
3814
+ const version = parseSimpleVersion(versionPart);
3815
+ if (!version) {
3816
+ return { success: false, error: `Invalid version in comparator: "${versionPart}"` };
3817
+ }
3818
+ // For caret and tilde, expand to range
3819
+ if (operator === '^') {
3820
+ return expandCaretRange(version);
3821
+ }
3822
+ if (operator === '~') {
3823
+ return expandTildeRange(version);
3824
+ }
3825
+ return { success: true, comparators: [createComparator(operator, version)] };
3826
+ }
3827
+ /**
3828
+ * Parses x-ranges like 1.x, 1.2.x, etc.
3829
+ *
3830
+ * @param input - X-range string to parse
3831
+ * @param _operator - Range operator (unused but kept for interface consistency)
3832
+ * @returns Comparator result
3833
+ */
3834
+ function parseXRange(input, _operator) {
3835
+ const parts = input.split('.');
3836
+ const nums = [];
3837
+ for (const part of parts) {
3838
+ const lower = part.toLowerCase();
3839
+ if (lower === 'x' || lower === '*' || lower === '') {
3840
+ break;
3841
+ }
3842
+ const num = parseInt(part, 10);
3843
+ if (globalIsNaN(num) || num < 0) {
3844
+ return { success: false, error: `Invalid x-range: "${input}"` };
3845
+ }
3846
+ nums.push(num);
3847
+ }
3848
+ if (nums.length === 0) {
3849
+ // * or X alone - match any
3850
+ return { success: true, comparators: [] };
3851
+ }
3852
+ if (nums.length === 1) {
3853
+ // 1.x or 1.* -> >=1.0.0 <2.0.0
3854
+ const lower = createSemVer({ major: nums[0], minor: 0, patch: 0 });
3855
+ const upper = createSemVer({ major: nums[0] + 1, minor: 0, patch: 0 });
3856
+ return { success: true, comparators: [createComparator('>=', lower), createComparator('<', upper)] };
3857
+ }
3858
+ // 1.2.x -> >=1.2.0 <1.3.0
3859
+ const lower = createSemVer({ major: nums[0], minor: nums[1], patch: 0 });
3860
+ const upper = createSemVer({ major: nums[0], minor: nums[1] + 1, patch: 0 });
3861
+ return { success: true, comparators: [createComparator('>=', lower), createComparator('<', upper)] };
3862
+ }
3863
+ /**
3864
+ * Expands caret range: ^1.2.3 -> >=1.2.3 <2.0.0
3865
+ *
3866
+ * @param version - Base version for caret range
3867
+ * @returns Expanded comparator result
3868
+ */
3869
+ function expandCaretRange(version) {
3870
+ let upperMajor = version.major;
3871
+ let upperMinor = 0;
3872
+ let upperPatch = 0;
3873
+ if (version.major === 0) {
3874
+ if (version.minor === 0) {
3875
+ // ^0.0.x -> >=0.0.x <0.0.(x+1)
3876
+ upperPatch = version.patch + 1;
3877
+ upperMinor = version.minor;
3878
+ }
3879
+ else {
3880
+ // ^0.x.y -> >=0.x.y <0.(x+1).0
3881
+ upperMinor = version.minor + 1;
3882
+ }
3883
+ }
3884
+ else {
3885
+ // ^x.y.z -> >=x.y.z <(x+1).0.0
3886
+ upperMajor = version.major + 1;
3887
+ }
3888
+ const upper = createSemVer({ major: upperMajor, minor: upperMinor, patch: upperPatch });
3889
+ return { success: true, comparators: [createComparator('>=', version), createComparator('<', upper)] };
3890
+ }
3891
+ /**
3892
+ * Expands tilde range: ~1.2.3 -> >=1.2.3 <1.3.0
3893
+ *
3894
+ * @param version - Base version for tilde range
3895
+ * @returns Expanded comparator result
3896
+ */
3897
+ function expandTildeRange(version) {
3898
+ const upper = createSemVer({
3899
+ major: version.major,
3900
+ minor: version.minor + 1,
3901
+ patch: 0,
3902
+ });
3903
+ return { success: true, comparators: [createComparator('>=', version), createComparator('<', upper)] };
3904
+ }
3905
+ /**
3906
+ * Parses a simple version string (no range operators).
3907
+ * More lenient - accepts partial versions.
3908
+ *
3909
+ * @param input - Version string to parse
3910
+ * @returns Parsed SemVer or null if invalid
3911
+ */
3912
+ function parseSimpleVersion(input) {
3913
+ if (!input)
3914
+ return null;
3915
+ let pos = 0;
3916
+ // Skip leading v
3917
+ if (input[pos] === 'v' || input[pos] === 'V') {
3918
+ pos++;
3919
+ }
3920
+ const parts = input.slice(pos).split('.');
3921
+ if (parts.length === 0)
3922
+ return null;
3923
+ const nums = [];
3924
+ for (const part of parts) {
3925
+ // Stop at prerelease or build
3926
+ const dashIdx = part.indexOf('-');
3927
+ const plusIdx = part.indexOf('+');
3928
+ let numPart = part;
3929
+ if (dashIdx !== -1) {
3930
+ numPart = part.slice(0, dashIdx);
3931
+ }
3932
+ else if (plusIdx !== -1) {
3933
+ numPart = part.slice(0, plusIdx);
3934
+ }
3935
+ if (numPart === '' || numPart.toLowerCase() === 'x' || numPart === '*') {
3936
+ break;
3937
+ }
3938
+ const num = parseInt(numPart, 10);
3939
+ if (globalIsNaN(num) || num < 0)
3940
+ return null;
3941
+ nums.push(num);
3942
+ }
3943
+ if (nums.length === 0)
3944
+ return null;
3945
+ return createSemVer({
3946
+ major: nums[0],
3947
+ minor: nums[1] ?? 0,
3948
+ patch: nums[2] ?? 0,
3949
+ prerelease: [],
3950
+ build: [],
3951
+ raw: input,
3952
+ });
3953
+ }
3954
+
3955
+ /**
3956
+ * Workspace Validation
3957
+ *
3958
+ * Validation utilities for workspace integrity checks.
3959
+ * Verifies package configurations, dependencies, and workspace structure.
3960
+ */
3961
+ /**
3962
+ * Validates a workspace for common issues.
3963
+ *
3964
+ * @param workspace - The workspace to validate
3965
+ * @returns Validation report
3966
+ *
3967
+ * @example
3968
+ * ```typescript
3969
+ * import { validateWorkspace } from '@hyperfrontend/versioning'
3970
+ *
3971
+ * const report = validateWorkspace(workspace)
3972
+ *
3973
+ * if (!report.valid) {
3974
+ * console.error(`${report.errorCount} error(s) found`)
3975
+ * for (const result of report.results) {
3976
+ * if (!result.result.valid) {
3977
+ * console.error(` ${result.checkName}: ${result.result.error}`)
3978
+ * }
3979
+ * }
3980
+ * }
3981
+ * ```
3982
+ */
3983
+ function validateWorkspace(workspace) {
3984
+ const results = [];
3985
+ // Workspace-level checks
3986
+ results.push({
3987
+ checkId: 'workspace-has-projects',
3988
+ checkName: 'Workspace has projects',
3989
+ packageName: null,
3990
+ result: validateHasProjects(workspace),
3991
+ });
3992
+ results.push({
3993
+ checkId: 'no-circular-dependencies',
3994
+ checkName: 'No circular dependencies',
3995
+ packageName: null,
3996
+ result: validateNoCircularDependencies(workspace),
3997
+ });
3998
+ // Package-level checks
3999
+ for (const project of workspace.projectList) {
4000
+ results.push({
4001
+ checkId: 'valid-version',
4002
+ checkName: `Valid semver version`,
4003
+ packageName: project.name,
4004
+ result: validateProjectVersion(project),
4005
+ });
4006
+ results.push({
4007
+ checkId: 'valid-name',
4008
+ checkName: `Valid package name`,
4009
+ packageName: project.name,
4010
+ result: validateProjectName(project),
4011
+ });
4012
+ results.push({
4013
+ checkId: 'dependency-versions',
4014
+ checkName: `Internal dependency versions`,
4015
+ packageName: project.name,
4016
+ result: validateDependencyVersions(workspace, project),
4017
+ });
4018
+ }
4019
+ // Aggregate results
4020
+ const errors = results.filter((r) => !r.result.valid);
4021
+ const warnings = results.filter((r) => r.result.warning !== undefined);
4022
+ const invalidPackageNames = errors
4023
+ .filter((r) => r.packageName !== null)
4024
+ .map((r) => r.packageName)
4025
+ .filter((name) => name !== null);
4026
+ const invalidPackages = createSet(invalidPackageNames);
4027
+ return {
4028
+ results,
4029
+ valid: errors.length === 0,
4030
+ errorCount: errors.length,
4031
+ warningCount: warnings.length,
4032
+ invalidPackages: [...invalidPackages],
4033
+ };
4034
+ }
4035
+ /**
4036
+ * Validates that the workspace has at least one project.
4037
+ *
4038
+ * @param workspace - Workspace to validate for project existence
4039
+ * @returns Validation result indicating success or failure
4040
+ */
4041
+ function validateHasProjects(workspace) {
4042
+ if (workspace.projects.size === 0) {
4043
+ return {
4044
+ valid: false,
4045
+ error: 'Workspace has no projects',
4046
+ };
4047
+ }
4048
+ return { valid: true };
4049
+ }
4050
+ /**
4051
+ * Validates that there are no circular dependencies.
4052
+ *
4053
+ * @param workspace - Workspace to check for circular dependencies
4054
+ * @returns Validation result indicating success or failure with cycle info
4055
+ */
4056
+ function validateNoCircularDependencies(workspace) {
4057
+ // Build adjacency list
4058
+ const visited = createSet();
4059
+ const recursionStack = createSet();
4060
+ /**
4061
+ * Depth-first search to detect cycles in the dependency graph.
4062
+ *
4063
+ * @param node - Current node being visited
4064
+ * @returns True if a cycle was found starting from this node
4065
+ */
4066
+ function hasCycle(node) {
4067
+ visited.add(node);
4068
+ recursionStack.add(node);
4069
+ const deps = workspace.reverseDependencyGraph.get(node) ?? [];
4070
+ for (const dep of deps) {
4071
+ if (!visited.has(dep)) {
4072
+ if (hasCycle(dep)) {
4073
+ return true;
4074
+ }
4075
+ }
4076
+ else if (recursionStack.has(dep)) {
4077
+ return true;
4078
+ }
4079
+ }
4080
+ recursionStack.delete(node);
4081
+ return false;
4082
+ }
4083
+ for (const name of workspace.projects.keys()) {
4084
+ if (!visited.has(name)) {
4085
+ if (hasCycle(name)) {
4086
+ return {
4087
+ valid: false,
4088
+ error: 'Circular dependency detected',
4089
+ };
4090
+ }
4091
+ }
4092
+ }
4093
+ return { valid: true };
4094
+ }
4095
+ /**
4096
+ * Validates a project's version is valid semver.
4097
+ *
4098
+ * @param project - Project to validate version for
4099
+ * @returns Validation result indicating success or failure
4100
+ */
4101
+ function validateProjectVersion(project) {
4102
+ const result = parseVersion(project.version);
4103
+ if (!result.success) {
4104
+ return {
4105
+ valid: false,
4106
+ error: `Invalid semver version: ${project.version}`,
4107
+ };
4108
+ }
4109
+ return { valid: true };
4110
+ }
4111
+ /**
4112
+ * Validates a project's package name.
4113
+ *
4114
+ * @param project - Project to validate name for
4115
+ * @returns Validation result indicating success or failure
4116
+ */
4117
+ function validateProjectName(project) {
4118
+ if (!project.name || project.name.trim() === '') {
4119
+ return {
4120
+ valid: false,
4121
+ error: 'Package name is required',
4122
+ };
4123
+ }
4124
+ // Check for valid npm package name using safe validation without complex regex
4125
+ const nameValidationResult = validatePackageNameFormat(project.name);
4126
+ if (!nameValidationResult.valid) {
4127
+ return {
4128
+ valid: false,
4129
+ error: `Invalid package name format: ${project.name}`,
4130
+ };
4131
+ }
4132
+ // Check length
4133
+ if (project.name.length > 214) {
4134
+ return {
4135
+ valid: false,
4136
+ error: 'Package name exceeds 214 characters',
4137
+ };
4138
+ }
4139
+ return { valid: true };
4140
+ }
4141
+ /**
4142
+ * Validates that internal dependency versions are satisfiable.
4143
+ *
4144
+ * @param workspace - Workspace containing all projects
4145
+ * @param project - Project to validate dependencies for
4146
+ * @returns Validation result with warnings for unsatisfied versions
4147
+ */
4148
+ function validateDependencyVersions(workspace, project) {
4149
+ const warnings = [];
4150
+ for (const depName of project.internalDependencies) {
4151
+ const dep = workspace.projects.get(depName);
4152
+ if (!dep) {
4153
+ continue; // Handled by dependency existence check
4154
+ }
4155
+ // Get the version range from package.json
4156
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
4157
+ let versionRange;
4158
+ for (const depType of depTypes) {
4159
+ const deps = project.packageJson[depType];
4160
+ if (deps?.[depName]) {
4161
+ versionRange = deps[depName];
4162
+ break;
4163
+ }
4164
+ }
4165
+ if (versionRange) {
4166
+ const depVersionResult = parseVersion(dep.version);
4167
+ if (depVersionResult.success && depVersionResult.version && !isWorkspaceVersion(versionRange)) {
4168
+ // Parse the version range
4169
+ const rangeResult = parseRange(versionRange);
4170
+ if (rangeResult.success && rangeResult.range) {
4171
+ // Check if the current version satisfies the range
4172
+ if (!satisfies(depVersionResult.version, rangeResult.range)) {
4173
+ warnings.push(`${depName}@${dep.version} does not satisfy ${versionRange}`);
4174
+ }
4175
+ }
4176
+ }
4177
+ }
4178
+ }
4179
+ if (warnings.length > 0) {
4180
+ return {
4181
+ valid: true, // Warning, not error
4182
+ warning: warnings.join('; '),
4183
+ };
4184
+ }
4185
+ return { valid: true };
4186
+ }
4187
+ /**
4188
+ * Validates npm package name format without using complex regex.
4189
+ * This avoids ReDoS vulnerabilities from backtracking regex patterns.
4190
+ *
4191
+ * @param name - Package name to validate
4192
+ * @returns Validation result
4193
+ */
4194
+ function validatePackageNameFormat(name) {
4195
+ // Valid characters for package names: lowercase letters, digits, hyphens, underscores, dots
4196
+ const isValidChar = (char) => {
4197
+ const code = char.charCodeAt(0);
4198
+ return ((code >= 97 && code <= 122) || // a-z
4199
+ (code >= 48 && code <= 57) || // 0-9
4200
+ code === 45 || // -
4201
+ code === 95 || // _
4202
+ code === 46 // .
4203
+ );
4204
+ };
4205
+ // First char must be lowercase letter or digit
4206
+ const isValidFirstChar = (char) => {
4207
+ const code = char.charCodeAt(0);
4208
+ return (code >= 97 && code <= 122) || (code >= 48 && code <= 57);
4209
+ };
4210
+ // Handle scoped packages (@scope/name)
4211
+ if (name.startsWith('@')) {
4212
+ const slashIndex = name.indexOf('/');
4213
+ if (slashIndex === -1 || slashIndex === 1 || slashIndex === name.length - 1) {
4214
+ return { valid: false };
4215
+ }
4216
+ // Validate scope (after @, before /)
4217
+ const scope = name.slice(1, slashIndex);
4218
+ if (!isValidFirstChar(scope[0]))
4219
+ return { valid: false };
4220
+ for (const char of scope) {
4221
+ if (!isValidChar(char))
4222
+ return { valid: false };
4223
+ }
4224
+ // Validate name (after /)
4225
+ const packageName = name.slice(slashIndex + 1);
4226
+ if (!isValidFirstChar(packageName[0]))
4227
+ return { valid: false };
4228
+ for (const char of packageName) {
4229
+ if (!isValidChar(char))
4230
+ return { valid: false };
4231
+ }
4232
+ }
4233
+ else {
4234
+ // Unscoped package
4235
+ if (!isValidFirstChar(name[0]))
4236
+ return { valid: false };
4237
+ for (const char of name) {
4238
+ if (!isValidChar(char))
4239
+ return { valid: false };
4240
+ }
4241
+ }
4242
+ return { valid: true };
4243
+ }
4244
+ /**
4245
+ * Checks if a version range is a workspace protocol version.
4246
+ *
4247
+ * @param versionRange - Version range string to check
4248
+ * @returns True if the range uses workspace protocol
4249
+ */
4250
+ function isWorkspaceVersion(versionRange) {
4251
+ return versionRange.startsWith('workspace:') || versionRange === '*' || versionRange === 'link:';
4252
+ }
4253
+ /**
4254
+ * Validates a single project.
4255
+ *
4256
+ * @param project - The project to validate
4257
+ * @returns Validation result
4258
+ */
4259
+ function validateProject(project) {
4260
+ const versionResult = validateProjectVersion(project);
4261
+ if (!versionResult.valid) {
4262
+ return versionResult;
4263
+ }
4264
+ const nameResult = validateProjectName(project);
4265
+ if (!nameResult.valid) {
4266
+ return nameResult;
4267
+ }
4268
+ return { valid: true };
4269
+ }
4270
+ /**
4271
+ * Creates a summary of the validation report.
4272
+ *
4273
+ * @param report - Report object from workspace validation
4274
+ * @returns Human-readable summary
4275
+ */
4276
+ function summarizeValidation(report) {
4277
+ const lines = [];
4278
+ if (report.valid) {
4279
+ lines.push('Workspace validation passed');
4280
+ if (report.warningCount > 0) {
4281
+ lines.push(` ${report.warningCount} warning(s)`);
4282
+ }
4283
+ }
4284
+ else {
4285
+ lines.push('Workspace validation failed');
4286
+ lines.push(` ${report.errorCount} error(s)`);
4287
+ lines.push(` ${report.warningCount} warning(s)`);
4288
+ lines.push('');
4289
+ lines.push('Errors:');
4290
+ for (const result of report.results) {
4291
+ if (!result.result.valid) {
4292
+ const pkg = result.packageName ? `[${result.packageName}] ` : '';
4293
+ lines.push(` ${pkg}${result.checkName}: ${result.result.error}`);
4294
+ }
4295
+ }
4296
+ if (report.warningCount > 0) {
4297
+ lines.push('');
4298
+ lines.push('Warnings:');
4299
+ for (const result of report.results) {
4300
+ if (result.result.warning) {
4301
+ const pkg = result.packageName ? `[${result.packageName}] ` : '';
4302
+ lines.push(` ${pkg}${result.checkName}: ${result.result.warning}`);
4303
+ }
4304
+ }
4305
+ }
4306
+ }
4307
+ return lines.join('\n');
4308
+ }
4309
+
4310
+ /**
4311
+ * Workspace Module
4312
+ *
4313
+ * Package discovery, dependency management, and versioning coordination
4314
+ * for monorepo workspaces. Integrates with project-scope for file operations.
4315
+ *
4316
+ * @example
4317
+ * ```typescript
4318
+ * import { discoverPackages, calculateCascadeBumps, applyBumps } from '@hyperfrontend/versioning'
4319
+ *
4320
+ * // Discover workspace packages
4321
+ * const { projects, projectMap, workspaceRoot } = discoverPackages()
4322
+ *
4323
+ * // Calculate cascade bumps for a package update
4324
+ * const cascadeResult = calculateCascadeBumps(workspace, [
4325
+ * { name: 'lib-utils', bumpType: 'minor' }
4326
+ * ])
4327
+ *
4328
+ * // Apply the bumps
4329
+ * const updateResult = applyBumps(workspace, cascadeResult.bumps)
4330
+ * ```
4331
+ */
4332
+ /**
4333
+ * Detects the workspace type based on configuration markers.
4334
+ *
4335
+ * @param workspaceRoot - Absolute path to workspace root
4336
+ * @returns Detected workspace type (nx, turbo, lerna, pnpm, yarn, npm, rush, or unknown)
4337
+ */
4338
+ function detectWorkspaceType(workspaceRoot) {
4339
+ if (exists(node_path.join(workspaceRoot, 'nx.json')))
4340
+ return 'nx';
4341
+ if (exists(node_path.join(workspaceRoot, 'turbo.json')))
4342
+ return 'turbo';
4343
+ if (exists(node_path.join(workspaceRoot, 'lerna.json')))
4344
+ return 'lerna';
4345
+ if (exists(node_path.join(workspaceRoot, 'pnpm-workspace.yaml')))
4346
+ return 'pnpm';
4347
+ if (exists(node_path.join(workspaceRoot, 'rush.json')))
4348
+ return 'rush';
4349
+ // Check for yarn workspaces in package.json
4350
+ const rootPkg = readPackageJsonIfExists(node_path.join(workspaceRoot, 'package.json'));
4351
+ if (rootPkg?.workspaces)
4352
+ return 'yarn';
4353
+ return 'npm';
4354
+ }
4355
+ /**
4356
+ * Creates a complete workspace object by discovering packages
4357
+ * and building the dependency graph.
4358
+ *
4359
+ * @param options - Discovery configuration options
4360
+ * @returns Complete workspace object with projects and dependency graph
4361
+ *
4362
+ * @example
4363
+ * ```typescript
4364
+ * import { createWorkspaceFromDisk } from '@hyperfrontend/versioning'
4365
+ *
4366
+ * const workspace = createWorkspaceFromDisk()
4367
+ *
4368
+ * // Access projects
4369
+ * for (const project of workspace.projectList) {
4370
+ * console.log(`${project.name}@${project.version}`)
4371
+ * }
4372
+ *
4373
+ * // Get dependents of a package
4374
+ * const dependents = workspace.dependencyGraph.get('lib-utils')
4375
+ * ```
4376
+ */
4377
+ function createWorkspaceFromDisk(options = {}) {
4378
+ const discovery = discoverPackages(options);
4379
+ const analysis = buildDependencyGraph(discovery.projects);
4380
+ const workspaceType = detectWorkspaceType(discovery.workspaceRoot);
4381
+ const projectMap = createMap();
4382
+ for (const project of discovery.projects) {
4383
+ projectMap.set(project.name, project);
4384
+ }
4385
+ return createWorkspace({
4386
+ root: discovery.workspaceRoot,
4387
+ type: workspaceType,
4388
+ projects: projectMap,
4389
+ config: discovery.config,
4390
+ dependencyGraph: analysis.dependencyGraph,
4391
+ reverseDependencyGraph: analysis.reverseDependencyGraph,
4392
+ });
4393
+ }
4394
+
4395
+ exports.CHANGELOG_NAMES = CHANGELOG_NAMES;
4396
+ exports.DEFAULT_BATCH_UPDATE_OPTIONS = DEFAULT_BATCH_UPDATE_OPTIONS;
4397
+ exports.DEFAULT_CASCADE_OPTIONS = DEFAULT_CASCADE_OPTIONS;
4398
+ exports.DEFAULT_EXCLUDE = DEFAULT_EXCLUDE;
4399
+ exports.DEFAULT_PATTERNS = DEFAULT_PATTERNS;
4400
+ exports.DEFAULT_WORKSPACE_CONFIG = DEFAULT_WORKSPACE_CONFIG;
4401
+ exports.addDependent = addDependent;
4402
+ exports.applyBumps = applyBumps;
4403
+ exports.buildDependencyGraph = buildDependencyGraph;
4404
+ exports.calculateCascadeBumps = calculateCascadeBumps;
4405
+ exports.calculateCascadeBumpsFromPackage = calculateCascadeBumpsFromPackage;
4406
+ exports.createProject = createProject;
4407
+ exports.createWorkspace = createWorkspace;
4408
+ exports.createWorkspaceConfig = createWorkspaceConfig;
4409
+ exports.createWorkspaceFromDisk = createWorkspaceFromDisk;
4410
+ exports.dependsOn = dependsOn;
4411
+ exports.discoverAllChangelogs = discoverAllChangelogs;
4412
+ exports.discoverPackages = discoverPackages;
4413
+ exports.discoverProject = discoverProject;
4414
+ exports.discoverProjectByName = discoverProjectByName;
4415
+ exports.findChangelogs = findChangelogs;
4416
+ exports.findInternalDependencies = findInternalDependencies;
4417
+ exports.findInternalDependenciesWithTypes = findInternalDependenciesWithTypes;
4418
+ exports.findProjectChangelog = findProjectChangelog;
4419
+ exports.getDependencies = getDependencies;
4420
+ exports.getDependencyCount = getDependencyCount;
4421
+ exports.getDependentCount = getDependentCount;
4422
+ exports.getDependents = getDependents;
4423
+ exports.getExpectedChangelogPath = getExpectedChangelogPath;
4424
+ exports.getProject = getProject;
4425
+ exports.getProjectCount = getProjectCount;
4426
+ exports.getProjectNames = getProjectNames;
4427
+ exports.getTopologicalOrder = getTopologicalOrder;
4428
+ exports.getTransitiveDependencies = getTransitiveDependencies;
4429
+ exports.getTransitiveDependents = getTransitiveDependents;
4430
+ exports.hasChangelog = hasChangelog;
4431
+ exports.hasInternalDependencies = hasInternalDependencies;
4432
+ exports.hasInternalDependents = hasInternalDependents;
4433
+ exports.hasProject = hasProject;
4434
+ exports.hasProjectChangelog = hasChangelog$1;
4435
+ exports.isPrivate = isPrivate;
4436
+ exports.isPublishable = isPublishable;
4437
+ exports.summarizeBatchUpdate = summarizeBatchUpdate;
4438
+ exports.summarizeCascadeBumps = summarizeCascadeBumps;
4439
+ exports.summarizeValidation = summarizeValidation;
4440
+ exports.transitivelyDependsOn = transitivelyDependsOn;
4441
+ exports.updatePackageVersionInTree = updatePackageVersionInTree;
4442
+ exports.validateProject = validateProject;
4443
+ exports.validateWorkspace = validateWorkspace;
4444
+ exports.withDependents = withDependents;
4445
+ //# sourceMappingURL=index.cjs.js.map