@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,3504 @@
1
+ /**
2
+ * Safe copies of JSON built-in methods.
3
+ *
4
+ * These references are captured at module initialization time to protect against
5
+ * prototype pollution attacks. Import only what you need for tree-shaking.
6
+ *
7
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/json
8
+ */
9
+ // Capture references at module initialization time
10
+ const _JSON = globalThis.JSON;
11
+ /**
12
+ * (Safe copy) Converts a JavaScript Object Notation (JSON) string into an object.
13
+ */
14
+ const parse = _JSON.parse;
15
+ /**
16
+ * (Safe copy) Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
17
+ */
18
+ const stringify = _JSON.stringify;
19
+
20
+ /**
21
+ * Creates a flow step.
22
+ *
23
+ * @param id - Unique step identifier
24
+ * @param name - Human-readable step name
25
+ * @param execute - Step executor function
26
+ * @param options - Optional step configuration
27
+ * @returns A FlowStep object
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const fetchStep = createStep(
32
+ * 'fetch-registry',
33
+ * 'Fetch Registry Version',
34
+ * async (ctx) => {
35
+ * const version = await ctx.registry.getLatestVersion(ctx.packageName)
36
+ * return {
37
+ * status: 'success',
38
+ * stateUpdates: { publishedVersion: version },
39
+ * message: `Found published version: ${version}`
40
+ * }
41
+ * }
42
+ * )
43
+ * ```
44
+ */
45
+ function createStep(id, name, execute, options = {}) {
46
+ return {
47
+ id,
48
+ name,
49
+ execute,
50
+ description: options.description,
51
+ skipIf: options.skipIf,
52
+ continueOnError: options.continueOnError,
53
+ dependsOn: options.dependsOn,
54
+ };
55
+ }
56
+ /**
57
+ * Creates a skipped step result.
58
+ *
59
+ * @param message - Explanation for why the step was skipped
60
+ * @returns A FlowStepResult with 'skipped' status
61
+ */
62
+ function createSkippedResult(message) {
63
+ return {
64
+ status: 'skipped',
65
+ message,
66
+ };
67
+ }
68
+
69
+ const FETCH_REGISTRY_STEP_ID = 'fetch-registry';
70
+ /**
71
+ * Creates the fetch-registry step.
72
+ *
73
+ * This step:
74
+ * 1. Queries the registry for the latest published version
75
+ * 2. Reads the current version from package.json
76
+ * 3. Determines if this is a first release
77
+ *
78
+ * State updates:
79
+ * - publishedVersion: Latest version on registry (null if not published)
80
+ * - currentVersion: Version from local package.json
81
+ * - isFirstRelease: True if never published
82
+ *
83
+ * @returns A FlowStep that fetches registry information
84
+ */
85
+ function createFetchRegistryStep() {
86
+ return createStep(FETCH_REGISTRY_STEP_ID, 'Fetch Registry Version', async (ctx) => {
87
+ const { registry, tree, projectRoot, packageName, logger } = ctx;
88
+ // Read local package.json for current version
89
+ const packageJsonPath = `${projectRoot}/package.json`;
90
+ let currentVersion = '0.0.0';
91
+ try {
92
+ const content = tree.read(packageJsonPath, 'utf-8');
93
+ if (content) {
94
+ const pkg = parse(content);
95
+ currentVersion = pkg.version ?? '0.0.0';
96
+ }
97
+ }
98
+ catch (error) {
99
+ logger.warn(`Could not read package.json: ${error}`);
100
+ }
101
+ // Query registry for published version
102
+ let publishedVersion = null;
103
+ let isFirstRelease = true;
104
+ try {
105
+ publishedVersion = await registry.getLatestVersion(packageName);
106
+ isFirstRelease = publishedVersion === null;
107
+ }
108
+ catch (error) {
109
+ // Package might not exist yet, which is fine
110
+ logger.debug(`Registry query failed (package may not exist): ${error}`);
111
+ isFirstRelease = true;
112
+ }
113
+ const message = isFirstRelease ? `First release (local: ${currentVersion})` : `Published: ${publishedVersion}, Local: ${currentVersion}`;
114
+ return {
115
+ status: 'success',
116
+ stateUpdates: {
117
+ publishedVersion,
118
+ currentVersion,
119
+ isFirstRelease,
120
+ },
121
+ message,
122
+ };
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Safe copies of Error built-ins via factory functions.
128
+ *
129
+ * Since constructors cannot be safely captured via Object.assign, this module
130
+ * provides factory functions that use Reflect.construct internally.
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/error
136
+ */
137
+ // Capture references at module initialization time
138
+ const _Error = globalThis.Error;
139
+ const _Reflect$2 = globalThis.Reflect;
140
+ /**
141
+ * (Safe copy) Creates a new Error using the captured Error constructor.
142
+ * Use this instead of `new Error()`.
143
+ *
144
+ * @param message - Optional error message.
145
+ * @param options - Optional error options.
146
+ * @returns A new Error instance.
147
+ */
148
+ const createError = (message, options) => _Reflect$2.construct(_Error, [message, options]);
149
+
150
+ /**
151
+ * Replaces all occurrences of a character in a string.
152
+ * Uses character-by-character iteration to avoid regex (ReDoS-safe).
153
+ *
154
+ * @param input - The input string
155
+ * @param target - The character to replace
156
+ * @param replacement - The replacement character
157
+ * @returns String with all occurrences replaced
158
+ */
159
+ function replaceChar(input, target, replacement) {
160
+ const result = [];
161
+ for (let i = 0; i < input.length; i++) {
162
+ result.push(input[i] === target ? replacement : input[i]);
163
+ }
164
+ return result.join('');
165
+ }
166
+ /**
167
+ * Replaces all hyphens with spaces.
168
+ * Convenience wrapper for normalizing footer keys.
169
+ *
170
+ * @param input - The input string
171
+ * @returns String with hyphens replaced by spaces
172
+ */
173
+ function hyphenToSpace(input) {
174
+ return replaceChar(input, '-', ' ');
175
+ }
176
+
177
+ /**
178
+ * Parses the body section of a commit message.
179
+ *
180
+ * The body starts after the first blank line and continues until
181
+ * we encounter a footer (key: value or key #value pattern) or end of message.
182
+ *
183
+ * @param lines - All lines of the commit message
184
+ * @param startIndex - Index to start looking for body (after header)
185
+ * @returns Parsed body or undefined if no body
186
+ */
187
+ function parseBody(lines, startIndex) {
188
+ // Skip blank lines to find body start
189
+ let pos = startIndex;
190
+ while (pos < lines.length && lines[pos].trim() === '') {
191
+ pos++;
192
+ }
193
+ if (pos >= lines.length) {
194
+ return undefined;
195
+ }
196
+ // If the first non-blank line is a footer, there's no body
197
+ if (isFooterLine(lines[pos])) {
198
+ return undefined;
199
+ }
200
+ const bodyLines = [];
201
+ // Collect body lines until we hit a footer or end
202
+ while (pos < lines.length) {
203
+ const line = lines[pos];
204
+ // Check if this line looks like a footer
205
+ if (isFooterLine(line)) {
206
+ break;
207
+ }
208
+ // Check for blank line followed by a footer
209
+ if (line.trim() === '' && pos + 1 < lines.length && isFooterLine(lines[pos + 1])) {
210
+ break;
211
+ }
212
+ bodyLines.push(line);
213
+ pos++;
214
+ }
215
+ // Trim trailing blank lines from body
216
+ while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === '') {
217
+ bodyLines.pop();
218
+ }
219
+ if (bodyLines.length === 0) {
220
+ return undefined;
221
+ }
222
+ return {
223
+ body: bodyLines.join('\n'),
224
+ endIndex: pos,
225
+ };
226
+ }
227
+ /**
228
+ * Checks if a line looks like a footer.
229
+ *
230
+ * Footer format:
231
+ * - key: value
232
+ * - key #value (for issue references)
233
+ * - BREAKING CHANGE: description
234
+ * - BREAKING-CHANGE: description
235
+ *
236
+ * @param line - The line to check
237
+ * @returns True if the line looks like a footer
238
+ */
239
+ function isFooterLine(line) {
240
+ if (!line)
241
+ return false;
242
+ const trimmed = line.trim();
243
+ if (trimmed === '')
244
+ return false;
245
+ // Check for BREAKING CHANGE or BREAKING-CHANGE
246
+ if (trimmed.startsWith('BREAKING CHANGE:') || trimmed.startsWith('BREAKING-CHANGE:')) {
247
+ return true;
248
+ }
249
+ // Check for token: value or token #value pattern
250
+ let pos = 0;
251
+ // Skip leading whitespace
252
+ while (pos < trimmed.length && trimmed[pos] === ' ') {
253
+ pos++;
254
+ }
255
+ // Read token (alphanumeric and hyphens)
256
+ const tokenStart = pos;
257
+ while (pos < trimmed.length) {
258
+ const char = trimmed[pos];
259
+ const code = char.charCodeAt(0);
260
+ if ((code >= 97 && code <= 122) || // a-z
261
+ (code >= 65 && code <= 90) || // A-Z
262
+ (code >= 48 && code <= 57) || // 0-9
263
+ code === 45 // -
264
+ ) {
265
+ pos++;
266
+ }
267
+ else {
268
+ break;
269
+ }
270
+ }
271
+ // Must have at least one character in token
272
+ if (pos === tokenStart)
273
+ return false;
274
+ // Must be followed by : or space-#
275
+ if (trimmed[pos] === ':') {
276
+ return true;
277
+ }
278
+ if (trimmed[pos] === ' ' && trimmed[pos + 1] === '#') {
279
+ return true;
280
+ }
281
+ return false;
282
+ }
283
+
284
+ /**
285
+ * Checks if a footer key indicates a breaking change.
286
+ *
287
+ * @param key - The footer key to check
288
+ * @returns True if the key indicates a breaking change
289
+ */
290
+ function isBreakingFooterKey(key) {
291
+ const normalized = hyphenToSpace(key.toUpperCase());
292
+ return normalized === 'BREAKING CHANGE';
293
+ }
294
+
295
+ /**
296
+ * Parses the footer section of a commit message.
297
+ *
298
+ * @param lines - All lines of the commit message
299
+ * @param startIndex - Index where footers start
300
+ * @returns Parsed footers
301
+ */
302
+ function parseFooters(lines, startIndex) {
303
+ const footers = [];
304
+ let breakingDescription;
305
+ let pos = startIndex;
306
+ // Skip blank lines
307
+ while (pos < lines.length && lines[pos].trim() === '') {
308
+ pos++;
309
+ }
310
+ while (pos < lines.length) {
311
+ const line = lines[pos];
312
+ const footer = parseFooterLine(line);
313
+ if (footer) {
314
+ footers.push(footer);
315
+ // Check for breaking change
316
+ if (isBreakingFooterKey(footer.key)) {
317
+ breakingDescription = footer.value;
318
+ // Breaking description may span multiple lines
319
+ pos++;
320
+ while (pos < lines.length && !isNewFooter(lines[pos])) {
321
+ const nextLine = lines[pos];
322
+ if (nextLine.trim() !== '') {
323
+ breakingDescription += '\n' + nextLine;
324
+ }
325
+ pos++;
326
+ }
327
+ continue;
328
+ }
329
+ }
330
+ pos++;
331
+ }
332
+ return { footers, breakingDescription };
333
+ }
334
+ /**
335
+ * Parses a single footer line.
336
+ *
337
+ * @param line - The line to parse
338
+ * @returns The parsed CommitFooter or null if not a valid footer
339
+ */
340
+ function parseFooterLine(line) {
341
+ if (!line)
342
+ return null;
343
+ const trimmed = line.trim();
344
+ if (trimmed === '')
345
+ return null;
346
+ // Check for BREAKING CHANGE or BREAKING-CHANGE
347
+ if (trimmed.startsWith('BREAKING CHANGE:')) {
348
+ return {
349
+ key: 'BREAKING CHANGE',
350
+ value: trimmed.slice(16).trim(),
351
+ separator: ':',
352
+ };
353
+ }
354
+ if (trimmed.startsWith('BREAKING-CHANGE:')) {
355
+ return {
356
+ key: 'BREAKING-CHANGE',
357
+ value: trimmed.slice(16).trim(),
358
+ separator: ':',
359
+ };
360
+ }
361
+ // Parse token: value or token #value
362
+ let pos = 0;
363
+ // Read token
364
+ const tokenStart = pos;
365
+ while (pos < trimmed.length) {
366
+ const char = trimmed[pos];
367
+ const code = char.charCodeAt(0);
368
+ if ((code >= 97 && code <= 122) || // a-z
369
+ (code >= 65 && code <= 90) || // A-Z
370
+ (code >= 48 && code <= 57) || // 0-9
371
+ code === 45 // -
372
+ ) {
373
+ pos++;
374
+ }
375
+ else {
376
+ break;
377
+ }
378
+ }
379
+ if (pos === tokenStart)
380
+ return null;
381
+ const key = trimmed.slice(tokenStart, pos);
382
+ // Check separator
383
+ let separator;
384
+ let valueStart;
385
+ if (trimmed[pos] === ':') {
386
+ separator = ':';
387
+ valueStart = pos + 1;
388
+ }
389
+ else if (trimmed[pos] === ' ' && trimmed[pos + 1] === '#') {
390
+ separator = ' #';
391
+ valueStart = pos + 2;
392
+ }
393
+ else {
394
+ return null;
395
+ }
396
+ // Skip whitespace after separator (for : case)
397
+ if (separator === ':') {
398
+ while (valueStart < trimmed.length && trimmed[valueStart] === ' ') {
399
+ valueStart++;
400
+ }
401
+ }
402
+ const value = trimmed.slice(valueStart);
403
+ return { key, value, separator };
404
+ }
405
+ /**
406
+ * Checks if a line starts a new footer.
407
+ *
408
+ * @param line - The line to check
409
+ * @returns True if the line starts a new footer
410
+ */
411
+ function isNewFooter(line) {
412
+ if (!line)
413
+ return false;
414
+ return parseFooterLine(line) !== null;
415
+ }
416
+
417
+ /**
418
+ * Parses a conventional commit header line.
419
+ *
420
+ * @param line - The first line of the commit message
421
+ * @returns Parsed header with type, scope, subject, and breaking flag
422
+ */
423
+ function parseHeader$1(line) {
424
+ let pos = 0;
425
+ const len = line.length;
426
+ // Extract type (alphanumeric characters until ( or : or !)
427
+ const typeStart = pos;
428
+ while (pos < len) {
429
+ const char = line[pos];
430
+ const code = char.charCodeAt(0);
431
+ // a-z, A-Z, 0-9
432
+ if ((code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57)) {
433
+ pos++;
434
+ }
435
+ else {
436
+ break;
437
+ }
438
+ }
439
+ const type = line.slice(typeStart, pos).toLowerCase();
440
+ // Check for scope in parentheses
441
+ let scope;
442
+ if (line[pos] === '(') {
443
+ pos++; // skip (
444
+ const scopeStart = pos;
445
+ while (pos < len && line[pos] !== ')') {
446
+ pos++;
447
+ }
448
+ scope = line.slice(scopeStart, pos);
449
+ if (line[pos] === ')') {
450
+ pos++; // skip )
451
+ }
452
+ }
453
+ // Check for breaking change indicator (!)
454
+ const breaking = line[pos] === '!';
455
+ if (breaking) {
456
+ pos++;
457
+ }
458
+ // Expect colon
459
+ if (line[pos] === ':') {
460
+ pos++;
461
+ }
462
+ // Skip whitespace after colon
463
+ while (pos < len && line[pos] === ' ') {
464
+ pos++;
465
+ }
466
+ // Rest is subject
467
+ const subject = line.slice(pos).trim();
468
+ return {
469
+ type,
470
+ scope,
471
+ subject,
472
+ breaking,
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Maximum commit message length (10KB)
478
+ */
479
+ const MAX_MESSAGE_LENGTH = 10 * 1024;
480
+ /**
481
+ * Parses a conventional commit message.
482
+ *
483
+ * @param message - The complete commit message
484
+ * @returns Parsed ConventionalCommit object
485
+ * @throws {Error} If message exceeds maximum length
486
+ */
487
+ function parseConventionalCommit(message) {
488
+ if (message.length > MAX_MESSAGE_LENGTH) {
489
+ throw createError(`Commit message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
490
+ }
491
+ const lines = splitLines(message);
492
+ if (lines.length === 0) {
493
+ return {
494
+ type: '',
495
+ subject: '',
496
+ footers: [],
497
+ breaking: false,
498
+ raw: message,
499
+ };
500
+ }
501
+ // Parse header (first line)
502
+ const header = parseHeader$1(lines[0]);
503
+ // Parse body and footers
504
+ let body;
505
+ let footersStartIndex = 1;
506
+ if (lines.length > 1) {
507
+ const bodyResult = parseBody(lines, 1);
508
+ if (bodyResult) {
509
+ body = bodyResult.body;
510
+ footersStartIndex = bodyResult.endIndex;
511
+ }
512
+ }
513
+ // Parse footers
514
+ const footersResult = parseFooters(lines, footersStartIndex);
515
+ const breakingDescriptionFromFooter = footersResult.breakingDescription;
516
+ // Determine breaking status
517
+ const breaking = header.breaking || footersResult.footers.some((f) => hyphenToSpace(f.key.toUpperCase()) === 'BREAKING CHANGE');
518
+ // Determine breaking description
519
+ let breakingDescription;
520
+ if (header.breaking && header.subject) {
521
+ // If breaking via !, the subject may describe the breaking change
522
+ breakingDescription = header.subject;
523
+ }
524
+ if (breakingDescriptionFromFooter) {
525
+ breakingDescription = breakingDescriptionFromFooter;
526
+ }
527
+ return {
528
+ type: header.type,
529
+ scope: header.scope,
530
+ subject: header.subject,
531
+ body,
532
+ footers: footersResult.footers,
533
+ breaking,
534
+ breakingDescription,
535
+ raw: message,
536
+ };
537
+ }
538
+ /**
539
+ * Splits a message into lines, handling different line endings.
540
+ *
541
+ * @param message - The message to split into lines
542
+ * @returns An array of lines from the message
543
+ */
544
+ function splitLines(message) {
545
+ const lines = [];
546
+ let currentLine = '';
547
+ let i = 0;
548
+ while (i < message.length) {
549
+ const char = message[i];
550
+ if (char === '\r') {
551
+ lines.push(currentLine);
552
+ currentLine = '';
553
+ // Skip \n if this is \r\n
554
+ if (message[i + 1] === '\n') {
555
+ i++;
556
+ }
557
+ }
558
+ else if (char === '\n') {
559
+ lines.push(currentLine);
560
+ currentLine = '';
561
+ }
562
+ else {
563
+ currentLine += char;
564
+ }
565
+ i++;
566
+ }
567
+ // Add final line if not empty
568
+ if (currentLine || message.endsWith('\n') || message.endsWith('\r')) {
569
+ lines.push(currentLine);
570
+ }
571
+ return lines;
572
+ }
573
+
574
+ const ANALYZE_COMMITS_STEP_ID = 'analyze-commits';
575
+ /**
576
+ * Creates the analyze-commits step.
577
+ *
578
+ * This step:
579
+ * 1. Finds the last release tag for this package
580
+ * 2. Gets all commits since that tag (or all commits if first release)
581
+ * 3. Parses each commit using conventional commit format
582
+ * 4. Filters to only release-worthy commits
583
+ *
584
+ * State updates:
585
+ * - lastReleaseTag: Tag name of last release (null if first release)
586
+ * - commits: Array of parsed conventional commits
587
+ *
588
+ * @returns A FlowStep that analyzes commits
589
+ */
590
+ function createAnalyzeCommitsStep() {
591
+ return createStep(ANALYZE_COMMITS_STEP_ID, 'Analyze Commits', async (ctx) => {
592
+ const { git, projectName, packageName, config, logger, state } = ctx;
593
+ // Find the last release tag for this package
594
+ let lastReleaseTag = null;
595
+ if (!state.isFirstRelease) {
596
+ // Try to find a tag matching the package name pattern
597
+ const tags = git.getTagsForPackage(packageName);
598
+ if (tags.length > 0) {
599
+ // Tags are returned in reverse chronological order
600
+ lastReleaseTag = tags[0].name;
601
+ logger.debug(`Found last release tag: ${lastReleaseTag}`);
602
+ }
603
+ else {
604
+ // Try with project name format
605
+ const projectTags = git.getTagsForPackage(projectName);
606
+ if (projectTags.length > 0) {
607
+ lastReleaseTag = projectTags[0].name;
608
+ logger.debug(`Found last release tag (project format): ${lastReleaseTag}`);
609
+ }
610
+ }
611
+ }
612
+ // Get commits
613
+ let rawCommits;
614
+ if (lastReleaseTag) {
615
+ rawCommits = git.getCommitsSince(lastReleaseTag);
616
+ logger.debug(`Found ${rawCommits.length} commits since ${lastReleaseTag}`);
617
+ }
618
+ else {
619
+ // First release - get all commits (limit to recent for performance)
620
+ rawCommits = git.getCommitLog({ maxCount: 100 });
621
+ logger.debug(`First release - analyzing up to ${rawCommits.length} commits`);
622
+ }
623
+ // Parse commits using conventional commit format
624
+ const commits = [];
625
+ const releaseTypes = config.releaseTypes ?? ['feat', 'fix', 'perf', 'revert'];
626
+ for (const rawCommit of rawCommits) {
627
+ const parsed = parseConventionalCommit(rawCommit.message);
628
+ if (parsed.type && releaseTypes.includes(parsed.type)) {
629
+ commits.push(parsed);
630
+ }
631
+ }
632
+ const message = commits.length > 0
633
+ ? `Found ${commits.length} releasable commits (${rawCommits.length} total)`
634
+ : `No releasable commits found (${rawCommits.length} total)`;
635
+ return {
636
+ status: 'success',
637
+ stateUpdates: {
638
+ lastReleaseTag,
639
+ commits,
640
+ },
641
+ message,
642
+ };
643
+ }, {
644
+ dependsOn: ['fetch-registry'],
645
+ });
646
+ }
647
+
648
+ /**
649
+ * Converts a SemVer to its canonical string representation.
650
+ *
651
+ * @param version - The version to format
652
+ * @returns The version string (e.g., "1.2.3-alpha.1+build.123")
653
+ */
654
+ function format(version) {
655
+ let result = `${version.major}.${version.minor}.${version.patch}`;
656
+ if (version.prerelease.length > 0) {
657
+ result += '-' + version.prerelease.join('.');
658
+ }
659
+ if (version.build.length > 0) {
660
+ result += '+' + version.build.join('.');
661
+ }
662
+ return result;
663
+ }
664
+
665
+ /**
666
+ * Safe copies of Number built-in methods and constants.
667
+ *
668
+ * These references are captured at module initialization time to protect against
669
+ * prototype pollution attacks. Import only what you need for tree-shaking.
670
+ *
671
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/number
672
+ */
673
+ // Capture references at module initialization time
674
+ const _parseInt = globalThis.parseInt;
675
+ const _isNaN = globalThis.isNaN;
676
+ // ============================================================================
677
+ // Parsing
678
+ // ============================================================================
679
+ /**
680
+ * (Safe copy) Parses a string and returns an integer.
681
+ */
682
+ const parseInt = _parseInt;
683
+ // ============================================================================
684
+ // Global Type Checking (legacy, less strict)
685
+ // ============================================================================
686
+ /**
687
+ * (Safe copy) Global isNaN function (coerces to number first, less strict than Number.isNaN).
688
+ */
689
+ const globalIsNaN = _isNaN;
690
+
691
+ /**
692
+ * Creates a new SemVer object.
693
+ *
694
+ * @param options - Version components
695
+ * @returns A new SemVer object
696
+ */
697
+ function createSemVer(options) {
698
+ return {
699
+ major: options.major,
700
+ minor: options.minor,
701
+ patch: options.patch,
702
+ prerelease: options.prerelease ?? [],
703
+ build: options.build ?? [],
704
+ raw: options.raw,
705
+ };
706
+ }
707
+
708
+ /**
709
+ * Increments a version based on the bump type.
710
+ *
711
+ * @param version - The version to increment
712
+ * @param type - The type of bump (major, minor, patch, etc.)
713
+ * @param prereleaseId - Optional prerelease identifier for prerelease bumps
714
+ * @returns A new incremented SemVer
715
+ *
716
+ * @example
717
+ * increment(parseVersion('1.2.3'), 'minor') // 1.3.0
718
+ * increment(parseVersion('1.2.3'), 'major') // 2.0.0
719
+ * increment(parseVersion('1.2.3'), 'prerelease', 'alpha') // 1.2.4-alpha.0
720
+ */
721
+ function increment(version, type, prereleaseId) {
722
+ switch (type) {
723
+ case 'major':
724
+ return createSemVer({
725
+ major: version.major + 1,
726
+ minor: 0,
727
+ patch: 0,
728
+ prerelease: [],
729
+ build: [],
730
+ });
731
+ case 'minor':
732
+ return createSemVer({
733
+ major: version.major,
734
+ minor: version.minor + 1,
735
+ patch: 0,
736
+ prerelease: [],
737
+ build: [],
738
+ });
739
+ case 'patch':
740
+ // If version has prerelease, just remove it (1.2.3-alpha -> 1.2.3)
741
+ if (version.prerelease.length > 0) {
742
+ return createSemVer({
743
+ major: version.major,
744
+ minor: version.minor,
745
+ patch: version.patch,
746
+ prerelease: [],
747
+ build: [],
748
+ });
749
+ }
750
+ return createSemVer({
751
+ major: version.major,
752
+ minor: version.minor,
753
+ patch: version.patch + 1,
754
+ prerelease: [],
755
+ build: [],
756
+ });
757
+ case 'premajor':
758
+ return createSemVer({
759
+ major: version.major + 1,
760
+ minor: 0,
761
+ patch: 0,
762
+ prerelease: ['alpha', '0'],
763
+ build: [],
764
+ });
765
+ case 'preminor':
766
+ return createSemVer({
767
+ major: version.major,
768
+ minor: version.minor + 1,
769
+ patch: 0,
770
+ prerelease: ['alpha', '0'],
771
+ build: [],
772
+ });
773
+ case 'prepatch':
774
+ return createSemVer({
775
+ major: version.major,
776
+ minor: version.minor,
777
+ patch: version.patch + 1,
778
+ prerelease: ['alpha', '0'],
779
+ build: [],
780
+ });
781
+ case 'prerelease':
782
+ return incrementPrerelease(version);
783
+ case 'none':
784
+ default:
785
+ return version;
786
+ }
787
+ }
788
+ /**
789
+ * Increments the prerelease portion of a version.
790
+ *
791
+ * @param version - The version to increment
792
+ * @param id - Optional prerelease identifier
793
+ * @returns A new version with incremented prerelease
794
+ */
795
+ function incrementPrerelease(version, id) {
796
+ const prerelease = [...version.prerelease];
797
+ if (prerelease.length === 0) {
798
+ // No existing prerelease - start at patch+1 with id.0
799
+ return createSemVer({
800
+ major: version.major,
801
+ minor: version.minor,
802
+ patch: version.patch + 1,
803
+ prerelease: ['alpha', '0'],
804
+ build: [],
805
+ });
806
+ }
807
+ // Check if the last identifier is numeric
808
+ const lastIdx = prerelease.length - 1;
809
+ const last = prerelease[lastIdx];
810
+ const lastNum = parseInt(last, 10);
811
+ if (!globalIsNaN(lastNum)) {
812
+ // Increment the numeric part
813
+ prerelease[lastIdx] = String(lastNum + 1);
814
+ }
815
+ else {
816
+ // Append .0
817
+ prerelease.push('0');
818
+ }
819
+ return createSemVer({
820
+ major: version.major,
821
+ minor: version.minor,
822
+ patch: version.patch,
823
+ prerelease,
824
+ build: [],
825
+ });
826
+ }
827
+
828
+ /**
829
+ * Maximum version string length to prevent memory exhaustion.
830
+ */
831
+ const MAX_VERSION_LENGTH = 256;
832
+ /**
833
+ * Parses a semantic version string.
834
+ *
835
+ * Accepts versions in the format: MAJOR.MINOR.PATCH[-prerelease][+build]
836
+ * Optional leading 'v' or '=' prefixes are stripped.
837
+ *
838
+ * @param input - The version string to parse
839
+ * @returns A ParseVersionResult with the parsed version or error
840
+ *
841
+ * @example
842
+ * parseVersion('1.2.3') // { success: true, version: { major: 1, minor: 2, patch: 3, ... } }
843
+ * parseVersion('v1.0.0-alpha.1+build.123') // { success: true, ... }
844
+ * parseVersion('invalid') // { success: false, error: '...' }
845
+ */
846
+ function parseVersion(input) {
847
+ // Input validation
848
+ if (!input || typeof input !== 'string') {
849
+ return { success: false, error: 'Version string is required' };
850
+ }
851
+ if (input.length > MAX_VERSION_LENGTH) {
852
+ return { success: false, error: `Version string exceeds maximum length of ${MAX_VERSION_LENGTH}` };
853
+ }
854
+ // Strip leading whitespace
855
+ let pos = 0;
856
+ while (pos < input.length && isWhitespace$1(input.charCodeAt(pos))) {
857
+ pos++;
858
+ }
859
+ // Strip trailing whitespace
860
+ let end = input.length;
861
+ while (end > pos && isWhitespace$1(input.charCodeAt(end - 1))) {
862
+ end--;
863
+ }
864
+ // Strip optional leading 'v' or '='
865
+ if (pos < end) {
866
+ const code = input.charCodeAt(pos);
867
+ if (code === 118 || code === 86) {
868
+ // 'v' or 'V'
869
+ pos++;
870
+ }
871
+ else if (code === 61) {
872
+ // '='
873
+ pos++;
874
+ }
875
+ }
876
+ // Parse major version
877
+ const majorResult = parseNumericIdentifier(input, pos, end);
878
+ if (!majorResult.success) {
879
+ return { success: false, error: majorResult.error ?? 'Invalid major version' };
880
+ }
881
+ pos = majorResult.endPos;
882
+ // Expect dot
883
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
884
+ // '.'
885
+ return { success: false, error: 'Expected "." after major version' };
886
+ }
887
+ pos++;
888
+ // Parse minor version
889
+ const minorResult = parseNumericIdentifier(input, pos, end);
890
+ if (!minorResult.success) {
891
+ return { success: false, error: minorResult.error ?? 'Invalid minor version' };
892
+ }
893
+ pos = minorResult.endPos;
894
+ // Expect dot
895
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
896
+ // '.'
897
+ return { success: false, error: 'Expected "." after minor version' };
898
+ }
899
+ pos++;
900
+ // Parse patch version
901
+ const patchResult = parseNumericIdentifier(input, pos, end);
902
+ if (!patchResult.success) {
903
+ return { success: false, error: patchResult.error ?? 'Invalid patch version' };
904
+ }
905
+ pos = patchResult.endPos;
906
+ // Parse optional prerelease
907
+ const prerelease = [];
908
+ if (pos < end && input.charCodeAt(pos) === 45) {
909
+ // '-'
910
+ pos++;
911
+ const prereleaseResult = parseIdentifiers(input, pos, end, [43]); // Stop at '+'
912
+ if (!prereleaseResult.success) {
913
+ return { success: false, error: prereleaseResult.error ?? 'Invalid prerelease' };
914
+ }
915
+ prerelease.push(...prereleaseResult.identifiers);
916
+ pos = prereleaseResult.endPos;
917
+ }
918
+ // Parse optional build metadata
919
+ const build = [];
920
+ if (pos < end && input.charCodeAt(pos) === 43) {
921
+ // '+'
922
+ pos++;
923
+ const buildResult = parseIdentifiers(input, pos, end, []);
924
+ if (!buildResult.success) {
925
+ return { success: false, error: buildResult.error ?? 'Invalid build metadata' };
926
+ }
927
+ build.push(...buildResult.identifiers);
928
+ pos = buildResult.endPos;
929
+ }
930
+ // Check for trailing characters
931
+ if (pos < end) {
932
+ return { success: false, error: `Unexpected character at position ${pos}: "${input[pos]}"` };
933
+ }
934
+ return {
935
+ success: true,
936
+ version: createSemVer({
937
+ major: majorResult.value,
938
+ minor: minorResult.value,
939
+ patch: patchResult.value,
940
+ prerelease,
941
+ build,
942
+ raw: input,
943
+ }),
944
+ };
945
+ }
946
+ /**
947
+ * Parses a numeric identifier (non-negative integer, no leading zeros except for "0").
948
+ *
949
+ * @param input - Input string to parse
950
+ * @param start - Start position in the input
951
+ * @param end - End position in the input
952
+ * @returns Numeric parsing result
953
+ */
954
+ function parseNumericIdentifier(input, start, end) {
955
+ if (start >= end) {
956
+ return { success: false, value: 0, endPos: start, error: 'Expected numeric identifier' };
957
+ }
958
+ let pos = start;
959
+ const firstCode = input.charCodeAt(pos);
960
+ // Must start with a digit
961
+ if (!isDigit(firstCode)) {
962
+ return { success: false, value: 0, endPos: pos, error: 'Expected digit' };
963
+ }
964
+ // Check for leading zero (only "0" is valid, not "01", "007", etc.)
965
+ if (firstCode === 48 && pos + 1 < end && isDigit(input.charCodeAt(pos + 1))) {
966
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier cannot have leading zeros' };
967
+ }
968
+ // Consume digits
969
+ let value = 0;
970
+ while (pos < end && isDigit(input.charCodeAt(pos))) {
971
+ value = value * 10 + (input.charCodeAt(pos) - 48);
972
+ pos++;
973
+ // Prevent overflow
974
+ if (value > Number.MAX_SAFE_INTEGER) {
975
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier is too large' };
976
+ }
977
+ }
978
+ return { success: true, value, endPos: pos };
979
+ }
980
+ /**
981
+ * Parses dot-separated identifiers (for prerelease/build).
982
+ *
983
+ * @param input - Input string to parse
984
+ * @param start - Start position in the input
985
+ * @param end - End position in the input
986
+ * @param stopCodes - Character codes that signal end of identifiers
987
+ * @returns Identifiers parsing result
988
+ */
989
+ function parseIdentifiers(input, start, end, stopCodes) {
990
+ const identifiers = [];
991
+ let pos = start;
992
+ while (pos < end) {
993
+ // Check for stop characters
994
+ if (stopCodes.includes(input.charCodeAt(pos))) {
995
+ break;
996
+ }
997
+ // Parse one identifier
998
+ const identStart = pos;
999
+ while (pos < end) {
1000
+ const code = input.charCodeAt(pos);
1001
+ // Stop at dot or stop characters
1002
+ if (code === 46 || stopCodes.includes(code)) {
1003
+ break;
1004
+ }
1005
+ // Must be alphanumeric or hyphen
1006
+ if (!isAlphanumeric(code) && code !== 45) {
1007
+ return { success: false, identifiers: [], endPos: pos, error: `Invalid character in identifier: "${input[pos]}"` };
1008
+ }
1009
+ pos++;
1010
+ }
1011
+ // Empty identifier is not allowed
1012
+ if (pos === identStart) {
1013
+ return { success: false, identifiers: [], endPos: pos, error: 'Empty identifier' };
1014
+ }
1015
+ identifiers.push(input.slice(identStart, pos));
1016
+ // Consume dot separator
1017
+ if (pos < end && input.charCodeAt(pos) === 46) {
1018
+ pos++;
1019
+ // Dot at end is invalid
1020
+ if (pos >= end || stopCodes.includes(input.charCodeAt(pos))) {
1021
+ return { success: false, identifiers: [], endPos: pos, error: 'Identifier expected after dot' };
1022
+ }
1023
+ }
1024
+ }
1025
+ return { success: true, identifiers, endPos: pos };
1026
+ }
1027
+ /**
1028
+ * Checks if a character code is a digit (0-9).
1029
+ *
1030
+ * @param code - Character code to check
1031
+ * @returns True if the code represents a digit
1032
+ */
1033
+ function isDigit(code) {
1034
+ return code >= 48 && code <= 57;
1035
+ }
1036
+ /**
1037
+ * Checks if a character code is alphanumeric or hyphen.
1038
+ *
1039
+ * @param code - Character code to check
1040
+ * @returns True if the code represents an alphanumeric character
1041
+ */
1042
+ function isAlphanumeric(code) {
1043
+ return ((code >= 48 && code <= 57) || // 0-9
1044
+ (code >= 65 && code <= 90) || // A-Z
1045
+ (code >= 97 && code <= 122) // a-z
1046
+ );
1047
+ }
1048
+ /**
1049
+ * Checks if a character code is whitespace.
1050
+ *
1051
+ * @param code - Character code to check
1052
+ * @returns True if the code represents whitespace
1053
+ */
1054
+ function isWhitespace$1(code) {
1055
+ return code === 32 || code === 9 || code === 10 || code === 13;
1056
+ }
1057
+
1058
+ const CALCULATE_BUMP_STEP_ID = 'calculate-bump';
1059
+ /**
1060
+ * Determines the highest bump type from analyzed commits.
1061
+ *
1062
+ * Priority: major > minor > patch > none
1063
+ * Breaking changes always result in major (post-1.0.0) or minor (pre-1.0.0).
1064
+ *
1065
+ * @param commits - Parsed conventional commits
1066
+ * @param minorTypes - Types that trigger minor bumps
1067
+ * @param patchTypes - Types that trigger patch bumps
1068
+ * @param currentVersion - Current version string (for pre-1.0.0 handling)
1069
+ * @returns The highest bump type needed
1070
+ */
1071
+ function calculateBumpFromCommits(commits, minorTypes, patchTypes, currentVersion) {
1072
+ if (commits.length === 0) {
1073
+ return 'none';
1074
+ }
1075
+ // Check for breaking changes
1076
+ const hasBreaking = commits.some((c) => c.breaking);
1077
+ if (hasBreaking) {
1078
+ // Pre-1.0.0: breaking changes are minor, not major
1079
+ const parsed = parseVersion(currentVersion);
1080
+ if (parsed.success && parsed.version && parsed.version.major === 0) {
1081
+ return 'minor';
1082
+ }
1083
+ return 'major';
1084
+ }
1085
+ // Check for minor-triggering types
1086
+ const hasMinor = commits.some((c) => c.type && minorTypes.includes(c.type));
1087
+ if (hasMinor) {
1088
+ return 'minor';
1089
+ }
1090
+ // Check for patch-triggering types
1091
+ const hasPatch = commits.some((c) => c.type && patchTypes.includes(c.type));
1092
+ if (hasPatch) {
1093
+ return 'patch';
1094
+ }
1095
+ return 'none';
1096
+ }
1097
+ /**
1098
+ * Creates the calculate-bump step.
1099
+ *
1100
+ * This step:
1101
+ * 1. Analyzes commit types and breaking changes
1102
+ * 2. Determines the appropriate bump level
1103
+ * 3. Calculates the next version
1104
+ *
1105
+ * State updates:
1106
+ * - bumpType: 'major' | 'minor' | 'patch' | 'none'
1107
+ * - nextVersion: Calculated next version string
1108
+ *
1109
+ * @returns A FlowStep that calculates version bump
1110
+ */
1111
+ function createCalculateBumpStep() {
1112
+ return createStep(CALCULATE_BUMP_STEP_ID, 'Calculate Version Bump', async (ctx) => {
1113
+ const { config, state, logger } = ctx;
1114
+ const { commits, currentVersion, isFirstRelease } = state;
1115
+ // Handle first release
1116
+ if (isFirstRelease) {
1117
+ const firstVersion = config.firstReleaseVersion ?? '0.1.0';
1118
+ logger.info(`First release: using version ${firstVersion}`);
1119
+ return {
1120
+ status: 'success',
1121
+ stateUpdates: {
1122
+ bumpType: 'minor',
1123
+ nextVersion: firstVersion,
1124
+ },
1125
+ message: `First release: ${firstVersion}`,
1126
+ };
1127
+ }
1128
+ // Check for forced bump type (releaseAs)
1129
+ if (config.releaseAs) {
1130
+ const forcedBumpType = config.releaseAs;
1131
+ const current = parseVersion(currentVersion ?? '0.0.0');
1132
+ if (!current.success || !current.version) {
1133
+ return {
1134
+ status: 'failed',
1135
+ error: createError(`Invalid current version: ${currentVersion}`),
1136
+ message: `Could not parse current version: ${currentVersion}`,
1137
+ };
1138
+ }
1139
+ const next = increment(current.version, forcedBumpType);
1140
+ const nextVersion = format(next);
1141
+ logger.info(`Forced ${forcedBumpType} bump via releaseAs: ${currentVersion} → ${nextVersion}`);
1142
+ return {
1143
+ status: 'success',
1144
+ stateUpdates: {
1145
+ bumpType: forcedBumpType,
1146
+ nextVersion,
1147
+ },
1148
+ message: `${forcedBumpType} bump (forced): ${currentVersion} → ${nextVersion}`,
1149
+ };
1150
+ }
1151
+ // No commits = no bump needed
1152
+ if (!commits || commits.length === 0) {
1153
+ return createSkippedResult('No releasable commits found');
1154
+ }
1155
+ const minorTypes = config.minorTypes ?? ['feat'];
1156
+ const patchTypes = config.patchTypes ?? ['fix', 'perf', 'revert'];
1157
+ const bumpType = calculateBumpFromCommits(commits, minorTypes, patchTypes, currentVersion ?? '0.0.0');
1158
+ if (bumpType === 'none') {
1159
+ return {
1160
+ status: 'success',
1161
+ stateUpdates: {
1162
+ bumpType: 'none',
1163
+ nextVersion: undefined,
1164
+ },
1165
+ message: 'No version bump needed',
1166
+ };
1167
+ }
1168
+ // Calculate next version
1169
+ const current = parseVersion(currentVersion ?? '0.0.0');
1170
+ if (!current.success || !current.version) {
1171
+ return {
1172
+ status: 'failed',
1173
+ error: createError(`Invalid current version: ${currentVersion}`),
1174
+ message: `Could not parse current version: ${currentVersion}`,
1175
+ };
1176
+ }
1177
+ const next = increment(current.version, bumpType);
1178
+ const nextVersion = format(next);
1179
+ return {
1180
+ status: 'success',
1181
+ stateUpdates: {
1182
+ bumpType,
1183
+ nextVersion,
1184
+ },
1185
+ message: `${bumpType} bump: ${currentVersion} → ${nextVersion}`,
1186
+ };
1187
+ }, {
1188
+ dependsOn: ['analyze-commits'],
1189
+ });
1190
+ }
1191
+ /**
1192
+ * Creates a step that checks for idempotency.
1193
+ *
1194
+ * This step prevents redundant releases by checking if the
1195
+ * calculated version is already published.
1196
+ *
1197
+ * @returns A FlowStep that checks idempotency
1198
+ */
1199
+ function createCheckIdempotencyStep() {
1200
+ return createStep('check-idempotency', 'Check Idempotency', async (ctx) => {
1201
+ const { registry, packageName, state } = ctx;
1202
+ const { nextVersion, bumpType } = state;
1203
+ // No bump = nothing to check
1204
+ if (!nextVersion || bumpType === 'none') {
1205
+ return {
1206
+ status: 'success',
1207
+ message: 'No version to check (no bump needed)',
1208
+ };
1209
+ }
1210
+ // Check if version is already published
1211
+ const isPublished = await registry.isVersionPublished(packageName, nextVersion);
1212
+ if (isPublished) {
1213
+ return {
1214
+ status: 'skipped',
1215
+ stateUpdates: {
1216
+ bumpType: 'none',
1217
+ nextVersion: undefined,
1218
+ },
1219
+ message: `Version ${nextVersion} is already published`,
1220
+ };
1221
+ }
1222
+ return {
1223
+ status: 'success',
1224
+ message: `Version ${nextVersion} is not yet published`,
1225
+ };
1226
+ }, {
1227
+ dependsOn: ['calculate-bump'],
1228
+ });
1229
+ }
1230
+
1231
+ /**
1232
+ * Safe copies of Date built-in via factory function and static methods.
1233
+ *
1234
+ * Since constructors cannot be safely captured via Object.assign, this module
1235
+ * provides a factory function that uses Reflect.construct internally.
1236
+ *
1237
+ * These references are captured at module initialization time to protect against
1238
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1239
+ *
1240
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/date
1241
+ */
1242
+ // Capture references at module initialization time
1243
+ const _Date = globalThis.Date;
1244
+ const _Reflect$1 = globalThis.Reflect;
1245
+ function createDate(...args) {
1246
+ return _Reflect$1.construct(_Date, args);
1247
+ }
1248
+
1249
+ /**
1250
+ * Creates a new changelog item.
1251
+ *
1252
+ * @param description - The description text of the change
1253
+ * @param options - Optional configuration for scope, commits, references, and breaking flag
1254
+ * @returns A new ChangelogItem object
1255
+ */
1256
+ function createChangelogItem(description, options) {
1257
+ return {
1258
+ description,
1259
+ scope: options?.scope,
1260
+ commits: options?.commits ?? [],
1261
+ references: options?.references ?? [],
1262
+ breaking: options?.breaking ?? false,
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Creates a new changelog section.
1267
+ *
1268
+ * @param type - The type of section (features, fixes, breaking, etc.)
1269
+ * @param heading - The display heading for the section
1270
+ * @param items - Optional array of changelog items in this section
1271
+ * @returns A new ChangelogSection object
1272
+ */
1273
+ function createChangelogSection(type, heading, items = []) {
1274
+ return {
1275
+ type,
1276
+ heading,
1277
+ items,
1278
+ };
1279
+ }
1280
+ /**
1281
+ * Creates a new changelog entry.
1282
+ *
1283
+ * @param version - The version string (e.g., '1.0.0')
1284
+ * @param options - Optional configuration for date, sections, and other properties
1285
+ * @returns A new ChangelogEntry object
1286
+ */
1287
+ function createChangelogEntry(version, options) {
1288
+ return {
1289
+ version,
1290
+ date: options?.date ?? null,
1291
+ unreleased: options?.unreleased ?? false,
1292
+ compareUrl: options?.compareUrl,
1293
+ sections: options?.sections ?? [],
1294
+ rawContent: options?.rawContent,
1295
+ };
1296
+ }
1297
+
1298
+ /**
1299
+ * Maps section headings to their canonical types.
1300
+ * Used during parsing to normalize different heading styles.
1301
+ */
1302
+ const SECTION_TYPE_MAP = {
1303
+ // Breaking changes
1304
+ 'breaking changes': 'breaking',
1305
+ breaking: 'breaking',
1306
+ 'breaking change': 'breaking',
1307
+ // Features
1308
+ features: 'features',
1309
+ feature: 'features',
1310
+ added: 'features',
1311
+ new: 'features',
1312
+ // Fixes
1313
+ fixes: 'fixes',
1314
+ fix: 'fixes',
1315
+ 'bug fixes': 'fixes',
1316
+ bugfixes: 'fixes',
1317
+ fixed: 'fixes',
1318
+ // Performance
1319
+ performance: 'performance',
1320
+ 'performance improvements': 'performance',
1321
+ perf: 'performance',
1322
+ // Documentation
1323
+ documentation: 'documentation',
1324
+ docs: 'documentation',
1325
+ // Deprecations
1326
+ deprecations: 'deprecations',
1327
+ deprecated: 'deprecations',
1328
+ // Refactoring
1329
+ refactoring: 'refactoring',
1330
+ refactor: 'refactoring',
1331
+ 'code refactoring': 'refactoring',
1332
+ // Tests
1333
+ tests: 'tests',
1334
+ test: 'tests',
1335
+ testing: 'tests',
1336
+ // Build
1337
+ build: 'build',
1338
+ 'build system': 'build',
1339
+ dependencies: 'build',
1340
+ // CI
1341
+ ci: 'ci',
1342
+ 'continuous integration': 'ci',
1343
+ // Chores
1344
+ chores: 'chores',
1345
+ chore: 'chores',
1346
+ maintenance: 'chores',
1347
+ // Other
1348
+ other: 'other',
1349
+ miscellaneous: 'other',
1350
+ misc: 'other',
1351
+ changed: 'other',
1352
+ changes: 'other',
1353
+ removed: 'other',
1354
+ security: 'other',
1355
+ };
1356
+ /**
1357
+ * Standard section headings for serialization.
1358
+ * Maps section types to their preferred heading text.
1359
+ */
1360
+ const SECTION_HEADINGS = {
1361
+ breaking: 'Breaking Changes',
1362
+ features: 'Features',
1363
+ fixes: 'Bug Fixes',
1364
+ performance: 'Performance',
1365
+ documentation: 'Documentation',
1366
+ deprecations: 'Deprecations',
1367
+ refactoring: 'Refactoring',
1368
+ tests: 'Tests',
1369
+ build: 'Build',
1370
+ ci: 'CI',
1371
+ chores: 'Chores',
1372
+ other: 'Other',
1373
+ };
1374
+ /**
1375
+ * Determines the section type from a heading string.
1376
+ * Returns 'other' if the heading is not recognized.
1377
+ *
1378
+ * @param heading - The heading string to parse
1379
+ * @returns The corresponding ChangelogSectionType
1380
+ */
1381
+ function getSectionType(heading) {
1382
+ const normalized = heading.toLowerCase().trim();
1383
+ return SECTION_TYPE_MAP[normalized] ?? 'other';
1384
+ }
1385
+
1386
+ /**
1387
+ * Safe copies of Map built-in via factory function.
1388
+ *
1389
+ * Since constructors cannot be safely captured via Object.assign, this module
1390
+ * provides a factory function that uses Reflect.construct internally.
1391
+ *
1392
+ * These references are captured at module initialization time to protect against
1393
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1394
+ *
1395
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/map
1396
+ */
1397
+ // Capture references at module initialization time
1398
+ const _Map = globalThis.Map;
1399
+ const _Reflect = globalThis.Reflect;
1400
+ /**
1401
+ * (Safe copy) Creates a new Map using the captured Map constructor.
1402
+ * Use this instead of `new Map()`.
1403
+ *
1404
+ * @param iterable - Optional iterable of key-value pairs.
1405
+ * @returns A new Map instance.
1406
+ */
1407
+ const createMap = (iterable) => _Reflect.construct(_Map, iterable ? [iterable] : []);
1408
+
1409
+ /**
1410
+ * Safe copies of Object built-in methods.
1411
+ *
1412
+ * These references are captured at module initialization time to protect against
1413
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1414
+ *
1415
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/object
1416
+ */
1417
+ // Capture references at module initialization time
1418
+ const _Object = globalThis.Object;
1419
+ /**
1420
+ * (Safe copy) Returns an array of key/values of the enumerable own properties of an object.
1421
+ */
1422
+ const entries = _Object.entries;
1423
+
1424
+ /**
1425
+ * Safe copies of URL built-ins via factory functions.
1426
+ *
1427
+ * Provides safe references to URL and URLSearchParams.
1428
+ * These references are captured at module initialization time to protect against
1429
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1430
+ *
1431
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/url
1432
+ */
1433
+ // Capture references at module initialization time
1434
+ const _URL = globalThis.URL;
1435
+ /**
1436
+ * (Safe copy) Creates an object URL for the given object.
1437
+ * Use this instead of `URL.createObjectURL()`.
1438
+ *
1439
+ * Note: This is a browser-only API. In Node.js environments, this will throw.
1440
+ */
1441
+ typeof _URL.createObjectURL === 'function'
1442
+ ? _URL.createObjectURL.bind(_URL)
1443
+ : () => {
1444
+ throw new Error('URL.createObjectURL is not available in this environment');
1445
+ };
1446
+ /**
1447
+ * (Safe copy) Revokes an object URL previously created with createObjectURL.
1448
+ * Use this instead of `URL.revokeObjectURL()`.
1449
+ *
1450
+ * Note: This is a browser-only API. In Node.js environments, this will throw.
1451
+ */
1452
+ typeof _URL.revokeObjectURL === 'function'
1453
+ ? _URL.revokeObjectURL.bind(_URL)
1454
+ : () => {
1455
+ throw new Error('URL.revokeObjectURL is not available in this environment');
1456
+ };
1457
+
1458
+ /**
1459
+ * Safe copies of Math built-in methods.
1460
+ *
1461
+ * These references are captured at module initialization time to protect against
1462
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1463
+ *
1464
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
1465
+ */
1466
+ // Capture references at module initialization time
1467
+ const _Math = globalThis.Math;
1468
+ // ============================================================================
1469
+ // Min/Max
1470
+ // ============================================================================
1471
+ /**
1472
+ * (Safe copy) Returns the larger of zero or more numbers.
1473
+ */
1474
+ const max = _Math.max;
1475
+
1476
+ /**
1477
+ * Line Parser
1478
+ *
1479
+ * Utilities for parsing individual changelog lines without regex.
1480
+ */
1481
+ /**
1482
+ * Parses a version string from a heading.
1483
+ * Examples: "1.2.3", "v1.2.3", "[1.2.3]", "1.2.3 - 2024-01-01"
1484
+ *
1485
+ * @param heading - The heading string to parse
1486
+ * @returns An object containing the parsed version, date, and optional compareUrl
1487
+ */
1488
+ function parseVersionFromHeading(heading) {
1489
+ const trimmed = heading.trim();
1490
+ // Check for unreleased
1491
+ const lowerHeading = trimmed.toLowerCase();
1492
+ if (lowerHeading === 'unreleased' || lowerHeading === '[unreleased]') {
1493
+ return { version: 'Unreleased', date: null };
1494
+ }
1495
+ let pos = 0;
1496
+ let version = '';
1497
+ let date = null;
1498
+ let compareUrl;
1499
+ // Skip leading [ if present
1500
+ if (trimmed[pos] === '[') {
1501
+ pos++;
1502
+ }
1503
+ // Skip leading 'v' if present
1504
+ if (trimmed[pos] === 'v' || trimmed[pos] === 'V') {
1505
+ pos++;
1506
+ }
1507
+ // Parse version number (digits and dots)
1508
+ const versionStart = pos;
1509
+ while (pos < trimmed.length) {
1510
+ const char = trimmed[pos];
1511
+ const code = char.charCodeAt(0);
1512
+ // Allow digits, dots, hyphens (for prerelease), plus signs
1513
+ if ((code >= 48 && code <= 57) || // 0-9
1514
+ char === '.' ||
1515
+ char === '-' ||
1516
+ char === '+' ||
1517
+ (code >= 97 && code <= 122) || // a-z (for prerelease tags like alpha, beta, rc)
1518
+ (code >= 65 && code <= 90) // A-Z
1519
+ ) {
1520
+ pos++;
1521
+ }
1522
+ else {
1523
+ break;
1524
+ }
1525
+ }
1526
+ version = trimmed.slice(versionStart, pos);
1527
+ // Skip trailing ] if present
1528
+ if (trimmed[pos] === ']') {
1529
+ pos++;
1530
+ }
1531
+ // Skip whitespace and separator
1532
+ while (pos < trimmed.length && (trimmed[pos] === ' ' || trimmed[pos] === '-' || trimmed[pos] === '–')) {
1533
+ pos++;
1534
+ }
1535
+ // Try to parse date (YYYY-MM-DD format)
1536
+ if (pos < trimmed.length) {
1537
+ const dateMatch = extractDate(trimmed.slice(pos));
1538
+ if (dateMatch) {
1539
+ date = dateMatch.date;
1540
+ pos += dateMatch.length;
1541
+ }
1542
+ }
1543
+ // Skip to check for compare URL (in parentheses or link)
1544
+ while (pos < trimmed.length && trimmed[pos] === ' ') {
1545
+ pos++;
1546
+ }
1547
+ // Check for link at end: [compare](url)
1548
+ if (pos < trimmed.length) {
1549
+ const linkMatch = extractLink(trimmed.slice(pos));
1550
+ if (linkMatch?.url) {
1551
+ compareUrl = linkMatch.url;
1552
+ }
1553
+ }
1554
+ return { version, date, compareUrl };
1555
+ }
1556
+ /**
1557
+ * Extracts a date in YYYY-MM-DD format from a string.
1558
+ *
1559
+ * @param str - The string to extract a date from
1560
+ * @returns The extracted date and its length, or null if no date found
1561
+ */
1562
+ function extractDate(str) {
1563
+ let pos = 0;
1564
+ // Skip optional parentheses
1565
+ if (str[pos] === '(')
1566
+ pos++;
1567
+ // Parse year (4 digits)
1568
+ const yearStart = pos;
1569
+ while (pos < str.length && pos - yearStart < 4) {
1570
+ const code = str.charCodeAt(pos);
1571
+ if (code >= 48 && code <= 57) {
1572
+ pos++;
1573
+ }
1574
+ else {
1575
+ break;
1576
+ }
1577
+ }
1578
+ if (pos - yearStart !== 4)
1579
+ return null;
1580
+ // Expect - or /
1581
+ if (str[pos] !== '-' && str[pos] !== '/')
1582
+ return null;
1583
+ const separator = str[pos];
1584
+ pos++;
1585
+ // Parse month (2 digits)
1586
+ const monthStart = pos;
1587
+ while (pos < str.length && pos - monthStart < 2) {
1588
+ const code = str.charCodeAt(pos);
1589
+ if (code >= 48 && code <= 57) {
1590
+ pos++;
1591
+ }
1592
+ else {
1593
+ break;
1594
+ }
1595
+ }
1596
+ if (pos - monthStart !== 2)
1597
+ return null;
1598
+ // Expect same separator
1599
+ if (str[pos] !== separator)
1600
+ return null;
1601
+ pos++;
1602
+ // Parse day (2 digits)
1603
+ const dayStart = pos;
1604
+ while (pos < str.length && pos - dayStart < 2) {
1605
+ const code = str.charCodeAt(pos);
1606
+ if (code >= 48 && code <= 57) {
1607
+ pos++;
1608
+ }
1609
+ else {
1610
+ break;
1611
+ }
1612
+ }
1613
+ if (pos - dayStart !== 2)
1614
+ return null;
1615
+ // Skip optional closing parenthesis
1616
+ if (str[pos] === ')')
1617
+ pos++;
1618
+ const dateStr = str.slice(yearStart, dayStart + 2);
1619
+ const date = slashToHyphen(dateStr);
1620
+ return { date, length: pos };
1621
+ }
1622
+ /**
1623
+ * Replaces forward slashes with hyphens (ReDoS-safe).
1624
+ *
1625
+ * @param input - The input string
1626
+ * @returns String with forward slashes replaced by hyphens
1627
+ */
1628
+ function slashToHyphen(input) {
1629
+ const result = [];
1630
+ for (let i = 0; i < input.length; i++) {
1631
+ result.push(input[i] === '/' ? '-' : input[i]);
1632
+ }
1633
+ return result.join('');
1634
+ }
1635
+ /**
1636
+ * Extracts a markdown link from a string.
1637
+ *
1638
+ * @param str - The string to extract a link from
1639
+ * @returns The extracted link text, url, and length, or null if no link found
1640
+ */
1641
+ function extractLink(str) {
1642
+ if (str[0] !== '[')
1643
+ return null;
1644
+ let pos = 1;
1645
+ let depth = 1;
1646
+ // Find closing ]
1647
+ while (pos < str.length && depth > 0) {
1648
+ if (str[pos] === '[')
1649
+ depth++;
1650
+ else if (str[pos] === ']')
1651
+ depth--;
1652
+ pos++;
1653
+ }
1654
+ if (depth !== 0)
1655
+ return null;
1656
+ const text = str.slice(1, pos - 1);
1657
+ // Expect (
1658
+ if (str[pos] !== '(')
1659
+ return null;
1660
+ pos++;
1661
+ const urlStart = pos;
1662
+ depth = 1;
1663
+ while (pos < str.length && depth > 0) {
1664
+ if (str[pos] === '(')
1665
+ depth++;
1666
+ else if (str[pos] === ')')
1667
+ depth--;
1668
+ pos++;
1669
+ }
1670
+ if (depth !== 0)
1671
+ return null;
1672
+ const url = str.slice(urlStart, pos - 1);
1673
+ return { text, url, length: pos };
1674
+ }
1675
+ /**
1676
+ * Parses commit references from a line.
1677
+ * Examples: (abc1234), [abc1234], commit abc1234
1678
+ *
1679
+ * @param text - The text to parse for commit references
1680
+ * @param baseUrl - Optional base URL for constructing commit links
1681
+ * @returns An array of parsed CommitRef objects
1682
+ */
1683
+ function parseCommitRefs(text, baseUrl) {
1684
+ const refs = [];
1685
+ let pos = 0;
1686
+ while (pos < text.length) {
1687
+ // Look for potential hash patterns
1688
+ // Common formats: (abc1234), [abc1234], abc1234fabcdef
1689
+ // Check for parenthetical hash
1690
+ if (text[pos] === '(' || text[pos] === '[') {
1691
+ const closeChar = text[pos] === '(' ? ')' : ']';
1692
+ const start = pos + 1;
1693
+ pos++;
1694
+ // Read potential hash
1695
+ while (pos < text.length && isHexDigit(text[pos])) {
1696
+ pos++;
1697
+ }
1698
+ // Check if valid hash (7-40 hex chars)
1699
+ const hash = text.slice(start, pos);
1700
+ if (hash.length >= 7 && hash.length <= 40 && text[pos] === closeChar) {
1701
+ refs.push({
1702
+ hash,
1703
+ shortHash: hash.slice(0, 7),
1704
+ url: baseUrl ? `${baseUrl}/commit/${hash}` : undefined,
1705
+ });
1706
+ pos++; // skip closing bracket
1707
+ continue;
1708
+ }
1709
+ }
1710
+ pos++;
1711
+ }
1712
+ return refs;
1713
+ }
1714
+ /**
1715
+ * Parses issue/PR references from a line.
1716
+ * Examples: #123, GH-123, closes #123
1717
+ *
1718
+ * @param text - The text to parse for issue references
1719
+ * @param baseUrl - Optional base URL for constructing issue links
1720
+ * @returns An array of parsed IssueRef objects
1721
+ */
1722
+ function parseIssueRefs(text, baseUrl) {
1723
+ const refs = [];
1724
+ let pos = 0;
1725
+ while (pos < text.length) {
1726
+ // Look for # followed by digits
1727
+ if (text[pos] === '#') {
1728
+ pos++;
1729
+ const numStart = pos;
1730
+ while (pos < text.length && isDigitChar(text[pos])) {
1731
+ pos++;
1732
+ }
1733
+ if (pos > numStart) {
1734
+ const number = parseInt(text.slice(numStart, pos), 10);
1735
+ // Check context for PR vs issue
1736
+ const beforeHash = text.slice(max(0, numStart - 10), numStart - 1).toLowerCase();
1737
+ const type = beforeHash.includes('pr') || beforeHash.includes('pull') ? 'pull-request' : 'issue';
1738
+ refs.push({
1739
+ number,
1740
+ type,
1741
+ url: baseUrl ? `${baseUrl}/issues/${number}` : undefined,
1742
+ });
1743
+ continue;
1744
+ }
1745
+ }
1746
+ pos++;
1747
+ }
1748
+ return refs;
1749
+ }
1750
+ /**
1751
+ * Parses the scope from a changelog item.
1752
+ * Example: "**scope:** description" -> { scope: "scope", description: "description" }
1753
+ *
1754
+ * @param text - The text to parse for scope
1755
+ * @returns An object with optional scope and the description
1756
+ */
1757
+ function parseScopeFromItem(text) {
1758
+ const trimmed = text.trim();
1759
+ // Check for **scope:** pattern (colon inside or outside bold)
1760
+ if (trimmed.startsWith('**')) {
1761
+ let pos = 2;
1762
+ const scopeStart = pos;
1763
+ // Read until ** or :
1764
+ while (pos < trimmed.length && trimmed[pos] !== '*' && trimmed[pos] !== ':') {
1765
+ pos++;
1766
+ }
1767
+ // Handle **scope:** pattern (colon before closing **)
1768
+ if (trimmed[pos] === ':' && trimmed[pos + 1] === '*' && trimmed[pos + 2] === '*') {
1769
+ const scope = trimmed.slice(scopeStart, pos);
1770
+ pos += 3; // skip :**
1771
+ // Skip whitespace
1772
+ while (trimmed[pos] === ' ')
1773
+ pos++;
1774
+ return { scope, description: trimmed.slice(pos) };
1775
+ }
1776
+ // Handle **scope**: pattern (colon after closing **)
1777
+ if (trimmed[pos] === '*' && trimmed[pos + 1] === '*') {
1778
+ const scope = trimmed.slice(scopeStart, pos);
1779
+ pos += 2; // skip **
1780
+ // Skip : if present
1781
+ if (trimmed[pos] === ':')
1782
+ pos++;
1783
+ // Skip whitespace
1784
+ while (trimmed[pos] === ' ')
1785
+ pos++;
1786
+ return { scope, description: trimmed.slice(pos) };
1787
+ }
1788
+ }
1789
+ // Check for scope: pattern (without bold)
1790
+ const colonPos = trimmed.indexOf(':');
1791
+ if (colonPos > 0 && colonPos < 30) {
1792
+ // scope shouldn't be too long
1793
+ const potentialScope = trimmed.slice(0, colonPos);
1794
+ // Scope should be a simple identifier (letters, numbers, hyphens)
1795
+ if (isValidScope(potentialScope)) {
1796
+ const description = trimmed.slice(colonPos + 1).trim();
1797
+ return { scope: potentialScope, description };
1798
+ }
1799
+ }
1800
+ return { description: trimmed };
1801
+ }
1802
+ /**
1803
+ * Checks if a string is a valid scope (alphanumeric with hyphens).
1804
+ *
1805
+ * @param str - The string to check
1806
+ * @returns True if the string is a valid scope identifier
1807
+ */
1808
+ function isValidScope(str) {
1809
+ if (!str || str.length === 0)
1810
+ return false;
1811
+ for (let i = 0; i < str.length; i++) {
1812
+ const code = str.charCodeAt(i);
1813
+ if (!(code >= 48 && code <= 57) && // 0-9
1814
+ !(code >= 65 && code <= 90) && // A-Z
1815
+ !(code >= 97 && code <= 122) && // a-z
1816
+ code !== 45 // -
1817
+ ) {
1818
+ return false;
1819
+ }
1820
+ }
1821
+ return true;
1822
+ }
1823
+ /**
1824
+ * Checks if a character is a hex digit.
1825
+ *
1826
+ * @param char - The character to check
1827
+ * @returns True if the character is a hex digit (0-9, A-F, a-f)
1828
+ */
1829
+ function isHexDigit(char) {
1830
+ const code = char.charCodeAt(0);
1831
+ return ((code >= 48 && code <= 57) || // 0-9
1832
+ (code >= 65 && code <= 70) || // A-F
1833
+ (code >= 97 && code <= 102) // a-f
1834
+ );
1835
+ }
1836
+ /**
1837
+ * Checks if a character is a digit.
1838
+ *
1839
+ * @param char - The character to check
1840
+ * @returns True if the character is a digit (0-9)
1841
+ */
1842
+ function isDigitChar(char) {
1843
+ const code = char.charCodeAt(0);
1844
+ return code >= 48 && code <= 57;
1845
+ }
1846
+
1847
+ /**
1848
+ * Changelog Tokenizer
1849
+ *
1850
+ * A state machine tokenizer that processes markdown character-by-character.
1851
+ * No regex is used to ensure ReDoS safety.
1852
+ */
1853
+ /**
1854
+ * Maximum input length to prevent memory exhaustion (1MB)
1855
+ */
1856
+ const MAX_INPUT_LENGTH = 1024 * 1024;
1857
+ /**
1858
+ * Tokenizes a changelog markdown string into tokens.
1859
+ *
1860
+ * @param input - The markdown content to tokenize
1861
+ * @returns Array of tokens
1862
+ * @throws {Error} If input exceeds maximum length
1863
+ */
1864
+ function tokenize(input) {
1865
+ if (input.length > MAX_INPUT_LENGTH) {
1866
+ throw createError(`Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters`);
1867
+ }
1868
+ const state = {
1869
+ pos: 0,
1870
+ line: 1,
1871
+ column: 1,
1872
+ input,
1873
+ tokens: [],
1874
+ };
1875
+ while (state.pos < state.input.length) {
1876
+ const char = state.input[state.pos];
1877
+ // Check for newline
1878
+ if (char === '\n') {
1879
+ consumeNewline(state);
1880
+ continue;
1881
+ }
1882
+ // Check for carriage return (handle \r\n)
1883
+ if (char === '\r') {
1884
+ state.pos++;
1885
+ if (state.input[state.pos] === '\n') {
1886
+ consumeNewline(state);
1887
+ }
1888
+ continue;
1889
+ }
1890
+ // At start of line, check for special markers
1891
+ if (state.column === 1) {
1892
+ // Check for heading
1893
+ if (char === '#') {
1894
+ consumeHeading(state);
1895
+ continue;
1896
+ }
1897
+ // Check for list item
1898
+ if ((char === '-' || char === '*') && isWhitespace(state.input[state.pos + 1])) {
1899
+ consumeListItem(state);
1900
+ continue;
1901
+ }
1902
+ }
1903
+ // Check for inline markdown elements
1904
+ if (char === '[') {
1905
+ consumeLink(state);
1906
+ continue;
1907
+ }
1908
+ if (char === '`') {
1909
+ consumeCode(state);
1910
+ continue;
1911
+ }
1912
+ if (char === '*' && state.input[state.pos + 1] === '*') {
1913
+ consumeBold(state);
1914
+ continue;
1915
+ }
1916
+ // Default: consume as text
1917
+ consumeText(state);
1918
+ }
1919
+ // Add EOF token
1920
+ pushToken(state, 'eof', '');
1921
+ return state.tokens;
1922
+ }
1923
+ /**
1924
+ * Consumes a newline character.
1925
+ *
1926
+ * @param state - The tokenizer state to update
1927
+ */
1928
+ function consumeNewline(state) {
1929
+ // Check if this is a blank line (previous token was also newline or at start)
1930
+ const prevToken = state.tokens[state.tokens.length - 1];
1931
+ const isBlank = prevToken?.type === 'newline' || prevToken?.type === 'blank-line' || state.tokens.length === 0;
1932
+ pushToken(state, isBlank ? 'blank-line' : 'newline', '\n');
1933
+ state.pos++;
1934
+ state.line++;
1935
+ state.column = 1;
1936
+ }
1937
+ /**
1938
+ * Consumes a heading (# through ####).
1939
+ *
1940
+ * @param state - The tokenizer state to update
1941
+ */
1942
+ function consumeHeading(state) {
1943
+ const startColumn = state.column;
1944
+ let level = 0;
1945
+ // Count # characters
1946
+ while (state.input[state.pos] === '#' && level < 4) {
1947
+ state.pos++;
1948
+ state.column++;
1949
+ level++;
1950
+ }
1951
+ // Skip whitespace after #
1952
+ while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
1953
+ state.pos++;
1954
+ state.column++;
1955
+ }
1956
+ // Consume the rest of the line as heading content
1957
+ const contentStart = state.pos;
1958
+ while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
1959
+ state.pos++;
1960
+ state.column++;
1961
+ }
1962
+ const content = state.input.slice(contentStart, state.pos);
1963
+ const type = `heading-${level}`;
1964
+ state.tokens.push({
1965
+ type,
1966
+ value: content.trim(),
1967
+ line: state.line,
1968
+ column: startColumn,
1969
+ });
1970
+ }
1971
+ /**
1972
+ * Consumes a list item (- or *).
1973
+ *
1974
+ * @param state - The tokenizer state to update
1975
+ */
1976
+ function consumeListItem(state) {
1977
+ const startColumn = state.column;
1978
+ // Skip the marker and whitespace
1979
+ state.pos++; // skip - or *
1980
+ state.column++;
1981
+ while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
1982
+ state.pos++;
1983
+ state.column++;
1984
+ }
1985
+ // Consume the rest of the line as list item content
1986
+ const contentStart = state.pos;
1987
+ while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
1988
+ state.pos++;
1989
+ state.column++;
1990
+ }
1991
+ const content = state.input.slice(contentStart, state.pos);
1992
+ state.tokens.push({
1993
+ type: 'list-item',
1994
+ value: content,
1995
+ line: state.line,
1996
+ column: startColumn,
1997
+ });
1998
+ }
1999
+ /**
2000
+ * Consumes a markdown link [text](url).
2001
+ *
2002
+ * @param state - The tokenizer state to update
2003
+ */
2004
+ function consumeLink(state) {
2005
+ const startColumn = state.column;
2006
+ const startLine = state.line;
2007
+ // Skip opening [
2008
+ state.pos++;
2009
+ state.column++;
2010
+ // Find closing ]
2011
+ const textStart = state.pos;
2012
+ let depth = 1;
2013
+ while (state.pos < state.input.length && depth > 0) {
2014
+ const char = state.input[state.pos];
2015
+ if (char === '[')
2016
+ depth++;
2017
+ else if (char === ']')
2018
+ depth--;
2019
+ else if (char === '\n') {
2020
+ // Link text shouldn't span lines; emit '[' as text and reset
2021
+ state.tokens.push({
2022
+ type: 'text',
2023
+ value: '[',
2024
+ line: startLine,
2025
+ column: startColumn,
2026
+ });
2027
+ state.pos = textStart;
2028
+ state.column = startColumn + 1;
2029
+ return;
2030
+ }
2031
+ if (depth > 0) {
2032
+ state.pos++;
2033
+ state.column++;
2034
+ }
2035
+ }
2036
+ if (depth !== 0) {
2037
+ // No closing ], emit '[' as text and reset
2038
+ state.tokens.push({
2039
+ type: 'text',
2040
+ value: '[',
2041
+ line: startLine,
2042
+ column: startColumn,
2043
+ });
2044
+ state.pos = textStart;
2045
+ state.column = startColumn + 1;
2046
+ return;
2047
+ }
2048
+ const linkText = state.input.slice(textStart, state.pos);
2049
+ state.pos++; // skip ]
2050
+ state.column++;
2051
+ // Check for (url)
2052
+ if (state.input[state.pos] === '(') {
2053
+ state.pos++; // skip (
2054
+ state.column++;
2055
+ const urlStart = state.pos;
2056
+ depth = 1;
2057
+ while (state.pos < state.input.length && depth > 0) {
2058
+ const char = state.input[state.pos];
2059
+ if (char === '(')
2060
+ depth++;
2061
+ else if (char === ')')
2062
+ depth--;
2063
+ else if (char === '\n') {
2064
+ // URL shouldn't span lines
2065
+ break;
2066
+ }
2067
+ if (depth > 0) {
2068
+ state.pos++;
2069
+ state.column++;
2070
+ }
2071
+ }
2072
+ if (depth === 0) {
2073
+ const linkUrl = state.input.slice(urlStart, state.pos);
2074
+ state.pos++; // skip )
2075
+ state.column++;
2076
+ // Emit link-text token
2077
+ state.tokens.push({
2078
+ type: 'link-text',
2079
+ value: linkText,
2080
+ line: startLine,
2081
+ column: startColumn,
2082
+ });
2083
+ // Emit link-url token
2084
+ state.tokens.push({
2085
+ type: 'link-url',
2086
+ value: linkUrl,
2087
+ line: startLine,
2088
+ column: startColumn + linkText.length + 3,
2089
+ });
2090
+ return;
2091
+ }
2092
+ }
2093
+ // No URL part, emit as text
2094
+ state.tokens.push({
2095
+ type: 'text',
2096
+ value: `[${linkText}]`,
2097
+ line: startLine,
2098
+ column: startColumn,
2099
+ });
2100
+ }
2101
+ /**
2102
+ * Consumes a code span `code`.
2103
+ *
2104
+ * @param state - The tokenizer state to update
2105
+ */
2106
+ function consumeCode(state) {
2107
+ const startColumn = state.column;
2108
+ const startLine = state.line;
2109
+ state.pos++; // skip opening `
2110
+ state.column++;
2111
+ const contentStart = state.pos;
2112
+ while (state.pos < state.input.length && state.input[state.pos] !== '`' && state.input[state.pos] !== '\n') {
2113
+ state.pos++;
2114
+ state.column++;
2115
+ }
2116
+ if (state.input[state.pos] === '`') {
2117
+ const content = state.input.slice(contentStart, state.pos);
2118
+ state.pos++; // skip closing `
2119
+ state.column++;
2120
+ state.tokens.push({
2121
+ type: 'code',
2122
+ value: content,
2123
+ line: startLine,
2124
+ column: startColumn,
2125
+ });
2126
+ }
2127
+ else {
2128
+ // No closing `, emit as text
2129
+ state.tokens.push({
2130
+ type: 'text',
2131
+ value: '`' + state.input.slice(contentStart, state.pos),
2132
+ line: startLine,
2133
+ column: startColumn,
2134
+ });
2135
+ }
2136
+ }
2137
+ /**
2138
+ * Consumes bold text **text**.
2139
+ *
2140
+ * @param state - The tokenizer state to update
2141
+ */
2142
+ function consumeBold(state) {
2143
+ const startColumn = state.column;
2144
+ const startLine = state.line;
2145
+ state.pos += 2; // skip opening **
2146
+ state.column += 2;
2147
+ const contentStart = state.pos;
2148
+ while (state.pos < state.input.length) {
2149
+ // Check for closing **
2150
+ if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
2151
+ break;
2152
+ }
2153
+ if (state.input[state.pos] === '\n') {
2154
+ state.line++;
2155
+ state.column = 0;
2156
+ }
2157
+ state.pos++;
2158
+ state.column++;
2159
+ }
2160
+ if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
2161
+ const content = state.input.slice(contentStart, state.pos);
2162
+ state.pos += 2; // skip closing **
2163
+ state.column += 2;
2164
+ state.tokens.push({
2165
+ type: 'bold',
2166
+ value: content,
2167
+ line: startLine,
2168
+ column: startColumn,
2169
+ });
2170
+ }
2171
+ else {
2172
+ // No closing **, emit as text
2173
+ state.tokens.push({
2174
+ type: 'text',
2175
+ value: '**' + state.input.slice(contentStart, state.pos),
2176
+ line: startLine,
2177
+ column: startColumn,
2178
+ });
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Consumes plain text until a special character or end of line.
2183
+ *
2184
+ * @param state - The tokenizer state to update
2185
+ */
2186
+ function consumeText(state) {
2187
+ const startColumn = state.column;
2188
+ const startLine = state.line;
2189
+ const startPos = state.pos;
2190
+ while (state.pos < state.input.length) {
2191
+ const char = state.input[state.pos];
2192
+ // Stop at newline
2193
+ if (char === '\n' || char === '\r') {
2194
+ break;
2195
+ }
2196
+ // Stop at special characters (but only if they start a pattern)
2197
+ if (char === '[')
2198
+ break;
2199
+ if (char === '`')
2200
+ break;
2201
+ if (char === '*' && state.input[state.pos + 1] === '*')
2202
+ break;
2203
+ state.pos++;
2204
+ state.column++;
2205
+ }
2206
+ const content = state.input.slice(startPos, state.pos);
2207
+ if (content.length > 0) {
2208
+ state.tokens.push({
2209
+ type: 'text',
2210
+ value: content,
2211
+ line: startLine,
2212
+ column: startColumn,
2213
+ });
2214
+ }
2215
+ }
2216
+ /**
2217
+ * Pushes a token to the state.
2218
+ *
2219
+ * @param state - The tokenizer state to update
2220
+ * @param type - Token classification (e.g., 'heading-1', 'text', 'link-url')
2221
+ * @param value - Text content associated with the token
2222
+ */
2223
+ function pushToken(state, type, value) {
2224
+ state.tokens.push({
2225
+ type,
2226
+ value,
2227
+ line: state.line,
2228
+ column: state.column,
2229
+ });
2230
+ }
2231
+ /**
2232
+ * Checks if a character is whitespace (but not newline).
2233
+ *
2234
+ * @param char - The character to check
2235
+ * @returns True if the character is whitespace (space or tab)
2236
+ */
2237
+ function isWhitespace(char) {
2238
+ return char === ' ' || char === '\t';
2239
+ }
2240
+
2241
+ /**
2242
+ * Changelog Parser
2243
+ *
2244
+ * Parses a changelog markdown string into a structured Changelog object.
2245
+ * Uses a state machine tokenizer for ReDoS-safe parsing.
2246
+ */
2247
+ /**
2248
+ * Parses a changelog markdown string into a Changelog object.
2249
+ *
2250
+ * @param content - The markdown content to parse
2251
+ * @param source - Optional source file path
2252
+ * @returns Parsed Changelog object
2253
+ */
2254
+ function parseChangelog(content, source) {
2255
+ const tokens = tokenize(content);
2256
+ const state = {
2257
+ tokens,
2258
+ pos: 0,
2259
+ warnings: [],
2260
+ };
2261
+ // Parse header
2262
+ const header = parseHeader(state);
2263
+ // Parse entries
2264
+ const entries = parseEntries(state);
2265
+ // Detect format
2266
+ const format = detectFormat(header, entries);
2267
+ // Build metadata
2268
+ const metadata = {
2269
+ format,
2270
+ isConventional: format === 'conventional',
2271
+ repositoryUrl: state.repositoryUrl,
2272
+ warnings: state.warnings,
2273
+ };
2274
+ return {
2275
+ source,
2276
+ header,
2277
+ entries,
2278
+ metadata,
2279
+ };
2280
+ }
2281
+ /**
2282
+ * Parses the changelog header section.
2283
+ *
2284
+ * @param state - The parser state containing tokens and position
2285
+ * @returns The parsed ChangelogHeader with title, description, and links
2286
+ */
2287
+ function parseHeader(state) {
2288
+ let title = '# Changelog';
2289
+ const description = [];
2290
+ const links = [];
2291
+ // Look for h1 title
2292
+ const headingToken = currentToken(state);
2293
+ if (headingToken?.type === 'heading-1') {
2294
+ title = `# ${headingToken.value}`;
2295
+ advance(state);
2296
+ }
2297
+ // Skip newlines
2298
+ skipNewlines(state);
2299
+ // Collect description lines until we hit h2 (version entry)
2300
+ while (!isEOF(state) && currentToken(state)?.type !== 'heading-2') {
2301
+ const token = currentToken(state);
2302
+ if (!token)
2303
+ break;
2304
+ if (token.type === 'text') {
2305
+ description.push(token.value);
2306
+ }
2307
+ else if (token.type === 'link-text') {
2308
+ // Check for link definition
2309
+ const nextToken = peek(state, 1);
2310
+ if (nextToken?.type === 'link-url') {
2311
+ description.push(`[${token.value}](${nextToken.value})`);
2312
+ links.push({ label: token.value, url: nextToken.value });
2313
+ // Try to detect repository URL
2314
+ if (!state.repositoryUrl && nextToken.value.includes('github.com')) {
2315
+ state.repositoryUrl = extractRepoUrl(nextToken.value);
2316
+ }
2317
+ advance(state); // skip link-text
2318
+ advance(state); // skip link-url
2319
+ continue;
2320
+ }
2321
+ }
2322
+ else if (token.type === 'newline' || token.type === 'blank-line') {
2323
+ if (description.length > 0 && description[description.length - 1] !== '') {
2324
+ description.push('');
2325
+ }
2326
+ }
2327
+ advance(state);
2328
+ }
2329
+ // Trim trailing empty lines
2330
+ while (description.length > 0 && description[description.length - 1] === '') {
2331
+ description.pop();
2332
+ }
2333
+ return { title, description, links };
2334
+ }
2335
+ /**
2336
+ * Parses all changelog entries.
2337
+ *
2338
+ * @param state - The parser state containing tokens and position
2339
+ * @returns An array of parsed ChangelogEntry objects
2340
+ */
2341
+ function parseEntries(state) {
2342
+ const entries = [];
2343
+ while (!isEOF(state)) {
2344
+ // Look for h2 heading (version entry)
2345
+ if (currentToken(state)?.type === 'heading-2') {
2346
+ const entry = parseEntry(state);
2347
+ if (entry) {
2348
+ entries.push(entry);
2349
+ }
2350
+ }
2351
+ else {
2352
+ advance(state);
2353
+ }
2354
+ }
2355
+ return entries;
2356
+ }
2357
+ /**
2358
+ * Parses a single changelog entry.
2359
+ *
2360
+ * @param state - The parser state containing tokens and position
2361
+ * @returns The parsed ChangelogEntry or null if parsing fails
2362
+ */
2363
+ function parseEntry(state) {
2364
+ const headingToken = currentToken(state);
2365
+ if (headingToken?.type !== 'heading-2') {
2366
+ return null;
2367
+ }
2368
+ const { version, date, compareUrl } = parseVersionFromHeading(headingToken.value);
2369
+ const unreleased = version.toLowerCase() === 'unreleased';
2370
+ advance(state); // skip h2
2371
+ skipNewlines(state);
2372
+ // Parse sections
2373
+ const sections = parseSections(state);
2374
+ return {
2375
+ version,
2376
+ date,
2377
+ unreleased,
2378
+ compareUrl,
2379
+ sections,
2380
+ };
2381
+ }
2382
+ /**
2383
+ * Parses sections within an entry.
2384
+ *
2385
+ * @param state - The parser state containing tokens and position
2386
+ * @returns An array of parsed ChangelogSection objects
2387
+ */
2388
+ function parseSections(state) {
2389
+ const sections = [];
2390
+ while (!isEOF(state)) {
2391
+ const token = currentToken(state);
2392
+ // Stop at next version entry (h2)
2393
+ if (token?.type === 'heading-2') {
2394
+ break;
2395
+ }
2396
+ // Parse section (h3)
2397
+ if (token?.type === 'heading-3') {
2398
+ const section = parseSection(state);
2399
+ if (section) {
2400
+ sections.push(section);
2401
+ }
2402
+ }
2403
+ else if (token?.type === 'list-item') {
2404
+ // Items without section heading - create "other" section
2405
+ const items = parseItems(state);
2406
+ if (items.length > 0) {
2407
+ sections.push({
2408
+ type: 'other',
2409
+ heading: 'Changes',
2410
+ items,
2411
+ });
2412
+ }
2413
+ }
2414
+ else {
2415
+ advance(state);
2416
+ }
2417
+ }
2418
+ return sections;
2419
+ }
2420
+ /**
2421
+ * Parses a single section.
2422
+ *
2423
+ * @param state - The parser state containing tokens and position
2424
+ * @returns The parsed ChangelogSection or null if parsing fails
2425
+ */
2426
+ function parseSection(state) {
2427
+ const headingToken = currentToken(state);
2428
+ if (headingToken?.type !== 'heading-3') {
2429
+ return null;
2430
+ }
2431
+ const heading = headingToken.value;
2432
+ const type = getSectionType(heading);
2433
+ advance(state); // skip h3
2434
+ skipNewlines(state);
2435
+ // Parse items
2436
+ const items = parseItems(state);
2437
+ return {
2438
+ type,
2439
+ heading,
2440
+ items,
2441
+ };
2442
+ }
2443
+ /**
2444
+ * Parses list items.
2445
+ *
2446
+ * @param state - The parser state containing tokens and position
2447
+ * @returns An array of parsed ChangelogItem objects
2448
+ */
2449
+ function parseItems(state) {
2450
+ const items = [];
2451
+ while (!isEOF(state)) {
2452
+ const token = currentToken(state);
2453
+ // Stop at headings
2454
+ if (token?.type === 'heading-2' || token?.type === 'heading-3') {
2455
+ break;
2456
+ }
2457
+ // Parse list item
2458
+ if (token?.type === 'list-item') {
2459
+ const item = parseItem(state);
2460
+ if (item) {
2461
+ items.push(item);
2462
+ }
2463
+ }
2464
+ else {
2465
+ advance(state);
2466
+ }
2467
+ }
2468
+ return items;
2469
+ }
2470
+ /**
2471
+ * Parses a single list item.
2472
+ *
2473
+ * @param state - The parser state containing tokens and position
2474
+ * @returns The parsed ChangelogItem or null if parsing fails
2475
+ */
2476
+ function parseItem(state) {
2477
+ const token = currentToken(state);
2478
+ if (token?.type !== 'list-item') {
2479
+ return null;
2480
+ }
2481
+ const text = token.value;
2482
+ const { scope, description } = parseScopeFromItem(text);
2483
+ const commits = parseCommitRefs(text, state.repositoryUrl);
2484
+ const references = parseIssueRefs(text, state.repositoryUrl);
2485
+ // Check for breaking change indicators
2486
+ const breaking = isBreakingItem(text);
2487
+ advance(state);
2488
+ return createChangelogItem(description, {
2489
+ scope,
2490
+ commits,
2491
+ references,
2492
+ breaking,
2493
+ });
2494
+ }
2495
+ /**
2496
+ * Checks if an item indicates a breaking change.
2497
+ *
2498
+ * @param text - The text content of the item
2499
+ * @returns True if the item indicates a breaking change
2500
+ */
2501
+ function isBreakingItem(text) {
2502
+ const lower = text.toLowerCase();
2503
+ return lower.includes('breaking change') || lower.includes('breaking:') || lower.startsWith('!') || lower.includes('[breaking]');
2504
+ }
2505
+ /**
2506
+ * Detects the changelog format.
2507
+ *
2508
+ * @param header - The parsed header of the changelog
2509
+ * @param entries - The parsed changelog entries
2510
+ * @returns The detected ChangelogFormat
2511
+ */
2512
+ function detectFormat(header, entries) {
2513
+ const descriptionText = header.description.join(' ').toLowerCase();
2514
+ // Check for Keep a Changelog
2515
+ if (descriptionText.includes('keep a changelog') || descriptionText.includes('keepachangelog')) {
2516
+ return 'keep-a-changelog';
2517
+ }
2518
+ // Check for conventional changelog patterns
2519
+ const hasConventionalSections = entries.some((entry) => entry.sections.some((section) => ['features', 'fixes', 'performance'].includes(section.type)));
2520
+ if (hasConventionalSections) {
2521
+ return 'conventional';
2522
+ }
2523
+ // Check if we have entries with structured sections
2524
+ if (entries.some((entry) => entry.sections.length > 0)) {
2525
+ return 'custom';
2526
+ }
2527
+ return 'unknown';
2528
+ }
2529
+ /**
2530
+ * Extracts repository URL from a GitHub URL.
2531
+ *
2532
+ * @param url - The URL to extract the repository from
2533
+ * @returns The repository URL or undefined if not found
2534
+ */
2535
+ function extractRepoUrl(url) {
2536
+ // Try to extract base repo URL from various GitHub URL patterns
2537
+ const githubIndex = url.indexOf('github.com/');
2538
+ if (githubIndex !== -1) {
2539
+ const afterGithub = url.slice(githubIndex + 11);
2540
+ const parts = afterGithub.split('/');
2541
+ if (parts.length >= 2) {
2542
+ return `https://github.com/${parts[0]}/${parts[1]}`;
2543
+ }
2544
+ }
2545
+ return undefined;
2546
+ }
2547
+ // ============================================================================
2548
+ // Parser utilities
2549
+ // ============================================================================
2550
+ /**
2551
+ * Gets the current token at the parser position.
2552
+ *
2553
+ * @param state - The parser state
2554
+ * @returns The current token or undefined if at end
2555
+ */
2556
+ function currentToken(state) {
2557
+ return state.tokens[state.pos];
2558
+ }
2559
+ /**
2560
+ * Peeks at a token at an offset from the current position.
2561
+ *
2562
+ * @param state - The parser state
2563
+ * @param offset - The offset from current position
2564
+ * @returns The token at the offset or undefined if out of bounds
2565
+ */
2566
+ function peek(state, offset) {
2567
+ return state.tokens[state.pos + offset];
2568
+ }
2569
+ /**
2570
+ * Advances the parser position by one token.
2571
+ *
2572
+ * @param state - The parser state to advance
2573
+ */
2574
+ function advance(state) {
2575
+ state.pos++;
2576
+ }
2577
+ /**
2578
+ * Checks if the parser has reached the end of the token stream.
2579
+ *
2580
+ * @param state - The parser state
2581
+ * @returns True if at end of file
2582
+ */
2583
+ function isEOF(state) {
2584
+ const token = currentToken(state);
2585
+ return !token || token.type === 'eof';
2586
+ }
2587
+ /**
2588
+ * Skips newline tokens until a non-newline token is found.
2589
+ *
2590
+ * @param state - The parser state
2591
+ */
2592
+ function skipNewlines(state) {
2593
+ while (!isEOF(state)) {
2594
+ const token = currentToken(state);
2595
+ if (token?.type !== 'newline' && token?.type !== 'blank-line') {
2596
+ break;
2597
+ }
2598
+ advance(state);
2599
+ }
2600
+ }
2601
+
2602
+ /**
2603
+ * Changelog Serialization Templates
2604
+ *
2605
+ * Output templates and formatting helpers for changelog serialization.
2606
+ * Provides configurable options for different changelog styles.
2607
+ */
2608
+ /**
2609
+ * Default serialization options.
2610
+ */
2611
+ const DEFAULT_SERIALIZE_OPTIONS = {
2612
+ includeDescription: true,
2613
+ includeLinks: true,
2614
+ includeCompareUrls: true,
2615
+ includeCommits: true,
2616
+ includeReferences: true,
2617
+ includeScope: true,
2618
+ includeRawContent: false,
2619
+ sectionOrder: [
2620
+ 'breaking',
2621
+ 'features',
2622
+ 'fixes',
2623
+ 'performance',
2624
+ 'documentation',
2625
+ 'deprecations',
2626
+ 'refactoring',
2627
+ 'tests',
2628
+ 'build',
2629
+ 'ci',
2630
+ 'chores',
2631
+ 'other',
2632
+ ],
2633
+ sectionHeadings: {},
2634
+ lineEnding: '\n',
2635
+ entrySpacing: 1,
2636
+ sectionSpacing: 1,
2637
+ useAsterisks: false,
2638
+ };
2639
+ /**
2640
+ * Merges user options with defaults.
2641
+ *
2642
+ * @param options - User-provided options
2643
+ * @returns Complete options with defaults applied
2644
+ */
2645
+ function resolveOptions(options) {
2646
+ {
2647
+ return DEFAULT_SERIALIZE_OPTIONS;
2648
+ }
2649
+ }
2650
+ /**
2651
+ * Gets the heading for a section type, respecting custom headings.
2652
+ *
2653
+ * @param type - The changelog section type to get the heading for
2654
+ * @param customHeadings - Optional map of custom section type to heading overrides
2655
+ * @returns The heading string to use
2656
+ */
2657
+ function getSectionHeading(type, customHeadings) {
2658
+ return customHeadings?.[type] ?? SECTION_HEADINGS[type];
2659
+ }
2660
+ /**
2661
+ * Creates a markdown link.
2662
+ *
2663
+ * @param text - The display text for the link
2664
+ * @param url - The destination URL for the link
2665
+ * @returns Formatted markdown link
2666
+ */
2667
+ function formatLink(text, url) {
2668
+ return `[${text}](${url})`;
2669
+ }
2670
+ /**
2671
+ * Creates a list item marker.
2672
+ *
2673
+ * @param useAsterisks - Whether to use * instead of -
2674
+ * @returns The list item marker ('- ' or '* ')
2675
+ */
2676
+ function getListMarker(useAsterisks) {
2677
+ return useAsterisks ? '* ' : '- ';
2678
+ }
2679
+ /**
2680
+ * Creates blank lines for spacing.
2681
+ *
2682
+ * @param count - Number of blank lines
2683
+ * @param lineEnding - Line ending style
2684
+ * @returns String with specified number of blank lines
2685
+ */
2686
+ function createSpacing(count, lineEnding) {
2687
+ if (count <= 0)
2688
+ return '';
2689
+ return lineEnding.repeat(count);
2690
+ }
2691
+
2692
+ /**
2693
+ * Changelog Serialization to String
2694
+ *
2695
+ * Converts a Changelog object back to markdown format.
2696
+ * Supports configurable output formatting for different changelog styles.
2697
+ */
2698
+ /**
2699
+ * Serializes a Changelog object to markdown string.
2700
+ *
2701
+ * @param changelog - The changelog to serialize
2702
+ * @param options - Optional serialization options
2703
+ * @returns The markdown string representation
2704
+ *
2705
+ * @example
2706
+ * ```ts
2707
+ * const markdown = serializeChangelog(changelog)
2708
+ * ```
2709
+ *
2710
+ * @example
2711
+ * ```ts
2712
+ * const markdown = serializeChangelog(changelog, {
2713
+ * includeCommits: false,
2714
+ * useAsterisks: true,
2715
+ * })
2716
+ * ```
2717
+ */
2718
+ function serializeChangelog(changelog, options) {
2719
+ const opts = resolveOptions();
2720
+ const parts = [];
2721
+ // Serialize header
2722
+ parts.push(serializeHeader(changelog.header, opts));
2723
+ // Serialize entries
2724
+ for (let i = 0; i < changelog.entries.length; i++) {
2725
+ const entry = changelog.entries[i];
2726
+ parts.push(serializeEntry(entry, opts));
2727
+ // Add spacing between entries (except after the last one)
2728
+ if (i < changelog.entries.length - 1) {
2729
+ parts.push(createSpacing(opts.entrySpacing, opts.lineEnding));
2730
+ }
2731
+ }
2732
+ return parts.join('');
2733
+ }
2734
+ /**
2735
+ * Serializes the changelog header.
2736
+ *
2737
+ * @param header - The header to serialize
2738
+ * @param opts - Resolved options
2739
+ * @returns The serialized header string
2740
+ */
2741
+ function serializeHeader(header, opts) {
2742
+ const parts = [];
2743
+ const nl = opts.lineEnding;
2744
+ // Title
2745
+ parts.push(header.title + nl);
2746
+ parts.push(nl);
2747
+ // Description
2748
+ if (opts.includeDescription && header.description.length > 0) {
2749
+ for (const line of header.description) {
2750
+ parts.push(line + nl);
2751
+ }
2752
+ parts.push(nl);
2753
+ }
2754
+ // Links section
2755
+ if (opts.includeLinks && header.links.length > 0) {
2756
+ for (const link of header.links) {
2757
+ parts.push(serializeLink(link) + nl);
2758
+ }
2759
+ parts.push(nl);
2760
+ }
2761
+ return parts.join('');
2762
+ }
2763
+ /**
2764
+ * Serializes a changelog link.
2765
+ *
2766
+ * @param link - The link to serialize
2767
+ * @returns The serialized link
2768
+ */
2769
+ function serializeLink(link) {
2770
+ return `[${link.label}]: ${link.url}`;
2771
+ }
2772
+ /**
2773
+ * Serializes a changelog entry.
2774
+ *
2775
+ * @param entry - The entry to serialize
2776
+ * @param opts - Resolved options
2777
+ * @returns The serialized entry string
2778
+ */
2779
+ function serializeEntry(entry, opts) {
2780
+ const parts = [];
2781
+ const nl = opts.lineEnding;
2782
+ // Entry heading
2783
+ parts.push(serializeEntryHeading(entry, opts) + nl);
2784
+ parts.push(nl);
2785
+ // Raw content fallback
2786
+ if (opts.includeRawContent && entry.rawContent) {
2787
+ parts.push(entry.rawContent + nl);
2788
+ return parts.join('');
2789
+ }
2790
+ // Sort sections by specified order
2791
+ const sortedSections = sortSections(entry.sections, opts.sectionOrder);
2792
+ // Serialize sections
2793
+ for (let i = 0; i < sortedSections.length; i++) {
2794
+ const section = sortedSections[i];
2795
+ parts.push(serializeSection(section, opts));
2796
+ // Add spacing between sections (except after the last one)
2797
+ if (i < sortedSections.length - 1) {
2798
+ parts.push(createSpacing(opts.sectionSpacing, nl));
2799
+ }
2800
+ }
2801
+ return parts.join('');
2802
+ }
2803
+ /**
2804
+ * Serializes an entry heading.
2805
+ *
2806
+ * @param entry - The changelog entry to serialize the heading for
2807
+ * @param opts - Resolved serialization options
2808
+ * @returns The heading line
2809
+ */
2810
+ function serializeEntryHeading(entry, opts) {
2811
+ const parts = ['## '];
2812
+ // Version with optional compare URL
2813
+ if (opts.includeCompareUrls && entry.compareUrl) {
2814
+ parts.push(formatLink(entry.version, entry.compareUrl));
2815
+ }
2816
+ else {
2817
+ parts.push(entry.version);
2818
+ }
2819
+ // Date
2820
+ if (entry.date) {
2821
+ parts.push(` - ${entry.date}`);
2822
+ }
2823
+ return parts.join('');
2824
+ }
2825
+ /**
2826
+ * Sorts sections by the specified order.
2827
+ *
2828
+ * @param sections - The sections to sort
2829
+ * @param order - The desired section order
2830
+ * @returns Sorted sections
2831
+ */
2832
+ function sortSections(sections, order) {
2833
+ const orderMap = createMap();
2834
+ order.forEach((type, index) => orderMap.set(type, index));
2835
+ return [...sections].sort((a, b) => {
2836
+ const orderA = orderMap.get(a.type) ?? Number.MAX_SAFE_INTEGER;
2837
+ const orderB = orderMap.get(b.type) ?? Number.MAX_SAFE_INTEGER;
2838
+ return orderA - orderB;
2839
+ });
2840
+ }
2841
+ /**
2842
+ * Serializes a changelog section.
2843
+ *
2844
+ * @param section - The section to serialize
2845
+ * @param opts - Resolved options
2846
+ * @returns The serialized section string
2847
+ */
2848
+ function serializeSection(section, opts) {
2849
+ const parts = [];
2850
+ const nl = opts.lineEnding;
2851
+ // Section heading - use original heading if available, otherwise generate
2852
+ const heading = section.heading || getSectionHeading(section.type, opts.sectionHeadings);
2853
+ parts.push(`### ${heading}${nl}`);
2854
+ parts.push(nl);
2855
+ // Items
2856
+ for (const item of section.items) {
2857
+ parts.push(serializeItem(item, opts) + nl);
2858
+ }
2859
+ return parts.join('');
2860
+ }
2861
+ /**
2862
+ * Serializes a changelog item.
2863
+ *
2864
+ * @param item - The item to serialize
2865
+ * @param opts - Resolved options
2866
+ * @returns The serialized item string
2867
+ */
2868
+ function serializeItem(item, opts) {
2869
+ const parts = [];
2870
+ const marker = getListMarker(opts.useAsterisks);
2871
+ parts.push(marker);
2872
+ // Breaking change indicator
2873
+ if (item.breaking) {
2874
+ parts.push('**BREAKING** ');
2875
+ }
2876
+ // Scope
2877
+ if (opts.includeScope && item.scope) {
2878
+ parts.push(`**${item.scope}:** `);
2879
+ }
2880
+ // Description
2881
+ parts.push(item.description);
2882
+ // Commit references
2883
+ if (opts.includeCommits && item.commits.length > 0) {
2884
+ parts.push(' (');
2885
+ parts.push(item.commits.map(serializeCommitRef).join(', '));
2886
+ parts.push(')');
2887
+ }
2888
+ // Issue/PR references
2889
+ if (opts.includeReferences && item.references.length > 0) {
2890
+ const refs = item.references.map(serializeIssueRef).join(', ');
2891
+ parts.push(` ${refs}`);
2892
+ }
2893
+ return parts.join('');
2894
+ }
2895
+ /**
2896
+ * Serializes a commit reference.
2897
+ *
2898
+ * @param ref - The commit reference
2899
+ * @returns The serialized reference
2900
+ */
2901
+ function serializeCommitRef(ref) {
2902
+ if (ref.url) {
2903
+ return formatLink(ref.shortHash, ref.url);
2904
+ }
2905
+ return ref.shortHash;
2906
+ }
2907
+ /**
2908
+ * Serializes an issue/PR reference.
2909
+ *
2910
+ * @param ref - The issue reference
2911
+ * @returns The serialized reference
2912
+ */
2913
+ function serializeIssueRef(ref) {
2914
+ const text = `#${ref.number}`;
2915
+ if (ref.url) {
2916
+ return formatLink(text, ref.url);
2917
+ }
2918
+ return text;
2919
+ }
2920
+
2921
+ /**
2922
+ * Changelog Entry Addition
2923
+ *
2924
+ * Functions for adding new entries to a changelog.
2925
+ */
2926
+ /**
2927
+ * Adds a new entry to a changelog.
2928
+ *
2929
+ * @param changelog - The changelog to add to
2930
+ * @param entry - The entry to add
2931
+ * @param options - Optional add options
2932
+ * @returns A new changelog with the entry added
2933
+ *
2934
+ * @example
2935
+ * ```ts
2936
+ * const newChangelog = addEntry(changelog, {
2937
+ * version: '1.2.0',
2938
+ * date: '2024-01-15',
2939
+ * unreleased: false,
2940
+ * sections: [...]
2941
+ * })
2942
+ * ```
2943
+ */
2944
+ function addEntry(changelog, entry, options) {
2945
+ // Check for existing entry
2946
+ const existingIndex = changelog.entries.findIndex((e) => e.version === entry.version);
2947
+ if (existingIndex !== -1 && true) {
2948
+ throw createError(`Entry with version "${entry.version}" already exists. Use replaceExisting: true to replace.`);
2949
+ }
2950
+ let newEntries;
2951
+ {
2952
+ // Add new entry
2953
+ const insertIndex = 0 ;
2954
+ newEntries = [...changelog.entries];
2955
+ newEntries.splice(insertIndex, 0, entry);
2956
+ }
2957
+ // Build new metadata if requested
2958
+ const metadata = changelog.metadata;
2959
+ return {
2960
+ ...changelog,
2961
+ entries: newEntries,
2962
+ metadata,
2963
+ };
2964
+ }
2965
+
2966
+ const GENERATE_CHANGELOG_STEP_ID = 'generate-changelog';
2967
+ /**
2968
+ * Maps conventional commit types to changelog section types.
2969
+ */
2970
+ const COMMIT_TYPE_TO_SECTION = {
2971
+ feat: 'features',
2972
+ fix: 'fixes',
2973
+ perf: 'performance',
2974
+ docs: 'documentation',
2975
+ refactor: 'refactoring',
2976
+ revert: 'other',
2977
+ build: 'build',
2978
+ ci: 'ci',
2979
+ test: 'tests',
2980
+ chore: 'chores',
2981
+ style: 'other',
2982
+ };
2983
+ /**
2984
+ * Groups commits by their section type.
2985
+ *
2986
+ * @param commits - Array of conventional commits
2987
+ * @returns Record of section type to commits
2988
+ */
2989
+ function groupCommitsBySection(commits) {
2990
+ const groups = {};
2991
+ for (const commit of commits) {
2992
+ const sectionType = COMMIT_TYPE_TO_SECTION[commit.type ?? 'chore'] ?? 'chores';
2993
+ if (!groups[sectionType]) {
2994
+ groups[sectionType] = [];
2995
+ }
2996
+ groups[sectionType].push(commit);
2997
+ }
2998
+ return groups;
2999
+ }
3000
+ /**
3001
+ * Creates a changelog item from a conventional commit.
3002
+ *
3003
+ * @param commit - The conventional commit
3004
+ * @returns A changelog item
3005
+ */
3006
+ function commitToItem(commit) {
3007
+ let text = commit.subject;
3008
+ // Add scope prefix if present
3009
+ if (commit.scope) {
3010
+ text = `**${commit.scope}:** ${text}`;
3011
+ }
3012
+ // Add breaking change indicator
3013
+ if (commit.breaking) {
3014
+ text = `⚠️ BREAKING: ${text}`;
3015
+ }
3016
+ return createChangelogItem(text);
3017
+ }
3018
+ /**
3019
+ * Creates the generate-changelog step.
3020
+ *
3021
+ * This step:
3022
+ * 1. Groups commits by type/section
3023
+ * 2. Creates changelog items from commits
3024
+ * 3. Assembles a complete changelog entry
3025
+ *
3026
+ * State updates:
3027
+ * - changelogEntry: The generated ChangelogEntry
3028
+ *
3029
+ * @returns A FlowStep that generates changelog
3030
+ */
3031
+ function createGenerateChangelogStep() {
3032
+ return createStep(GENERATE_CHANGELOG_STEP_ID, 'Generate Changelog Entry', async (ctx) => {
3033
+ const { config, state } = ctx;
3034
+ const { commits, nextVersion, bumpType } = state;
3035
+ // Skip if no bump needed
3036
+ if (!nextVersion || bumpType === 'none') {
3037
+ return createSkippedResult('No version bump, skipping changelog generation');
3038
+ }
3039
+ // Skip if changelog disabled
3040
+ if (config.skipChangelog) {
3041
+ return createSkippedResult('Changelog generation disabled');
3042
+ }
3043
+ // Handle case with no commits (e.g., first release)
3044
+ if (!commits || commits.length === 0) {
3045
+ const entry = createChangelogEntry(nextVersion, {
3046
+ date: createDate().toISOString().split('T')[0],
3047
+ sections: [createChangelogSection('features', 'Features', [createChangelogItem('Initial release')])],
3048
+ });
3049
+ return {
3050
+ status: 'success',
3051
+ stateUpdates: { changelogEntry: entry },
3052
+ message: 'Generated initial release changelog entry',
3053
+ };
3054
+ }
3055
+ // Group commits by section
3056
+ const grouped = groupCommitsBySection(commits);
3057
+ // Create sections
3058
+ const sections = [];
3059
+ // Add breaking changes section first if any
3060
+ const breakingCommits = commits.filter((c) => c.breaking);
3061
+ if (breakingCommits.length > 0) {
3062
+ sections.push(createChangelogSection('breaking', 'Breaking Changes', breakingCommits.map((c) => {
3063
+ const text = c.breakingDescription ?? c.subject;
3064
+ return createChangelogItem(c.scope ? `**${c.scope}:** ${text}` : text);
3065
+ })));
3066
+ }
3067
+ // Add other sections in conventional order
3068
+ const sectionOrder = [
3069
+ { type: 'features', heading: 'Features' },
3070
+ { type: 'fixes', heading: 'Bug Fixes' },
3071
+ { type: 'performance', heading: 'Performance' },
3072
+ { type: 'documentation', heading: 'Documentation' },
3073
+ { type: 'refactoring', heading: 'Code Refactoring' },
3074
+ { type: 'build', heading: 'Build' },
3075
+ { type: 'ci', heading: 'Continuous Integration' },
3076
+ { type: 'tests', heading: 'Tests' },
3077
+ { type: 'chores', heading: 'Chores' },
3078
+ { type: 'other', heading: 'Other' },
3079
+ ];
3080
+ for (const { type: sectionType, heading } of sectionOrder) {
3081
+ const sectionCommits = grouped[sectionType];
3082
+ if (sectionCommits && sectionCommits.length > 0) {
3083
+ sections.push(createChangelogSection(sectionType, heading, sectionCommits.map(commitToItem)));
3084
+ }
3085
+ }
3086
+ // Create the entry
3087
+ const entry = createChangelogEntry(nextVersion, {
3088
+ date: createDate().toISOString().split('T')[0],
3089
+ sections,
3090
+ });
3091
+ return {
3092
+ status: 'success',
3093
+ stateUpdates: { changelogEntry: entry },
3094
+ message: `Generated changelog with ${sections.length} section(s), ${commits.length} commit(s)`,
3095
+ };
3096
+ }, {
3097
+ dependsOn: ['check-idempotency'],
3098
+ });
3099
+ }
3100
+ /**
3101
+ * Creates the write-changelog step.
3102
+ *
3103
+ * This step writes the generated changelog entry to CHANGELOG.md.
3104
+ *
3105
+ * @returns A FlowStep that writes changelog to file
3106
+ */
3107
+ function createWriteChangelogStep() {
3108
+ return createStep('write-changelog', 'Write Changelog', async (ctx) => {
3109
+ const { tree, projectRoot, config, state, logger } = ctx;
3110
+ const { changelogEntry, nextVersion, bumpType } = state;
3111
+ // Skip if no bump or no changelog
3112
+ if (!nextVersion || bumpType === 'none' || !changelogEntry || config.skipChangelog) {
3113
+ return createSkippedResult('No changelog to write');
3114
+ }
3115
+ const changelogPath = `${projectRoot}/CHANGELOG.md`;
3116
+ let existingContent = '';
3117
+ // Read existing changelog
3118
+ try {
3119
+ existingContent = tree.read(changelogPath, 'utf-8') ?? '';
3120
+ }
3121
+ catch {
3122
+ logger.debug('No existing CHANGELOG.md found');
3123
+ }
3124
+ // If no existing content, create new changelog
3125
+ if (!existingContent.trim()) {
3126
+ const newChangelog = {
3127
+ header: {
3128
+ title: '# Changelog',
3129
+ description: ['All notable changes to this project will be documented in this file.'],
3130
+ links: [],
3131
+ },
3132
+ entries: [changelogEntry]};
3133
+ const serialized = serializeChangelog(newChangelog);
3134
+ tree.write(changelogPath, serialized);
3135
+ return {
3136
+ status: 'success',
3137
+ stateUpdates: {
3138
+ modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
3139
+ },
3140
+ message: `Created CHANGELOG.md with version ${nextVersion}`,
3141
+ };
3142
+ }
3143
+ // Parse existing and add entry
3144
+ const existing = parseChangelog(existingContent);
3145
+ const updated = addEntry(existing, changelogEntry);
3146
+ const serialized = serializeChangelog(updated);
3147
+ tree.write(changelogPath, serialized);
3148
+ return {
3149
+ status: 'success',
3150
+ stateUpdates: {
3151
+ modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
3152
+ },
3153
+ message: `Updated CHANGELOG.md with version ${nextVersion}`,
3154
+ };
3155
+ }, {
3156
+ dependsOn: ['generate-changelog'],
3157
+ });
3158
+ }
3159
+
3160
+ const UPDATE_PACKAGES_STEP_ID = 'update-packages';
3161
+ /**
3162
+ * Creates the update-packages step.
3163
+ *
3164
+ * This step:
3165
+ * 1. Updates the version field in package.json
3166
+ * 2. Tracks the modified files
3167
+ *
3168
+ * State updates:
3169
+ * - modifiedFiles: Adds package.json to list
3170
+ *
3171
+ * @returns A FlowStep that updates package.json
3172
+ */
3173
+ function createUpdatePackageStep() {
3174
+ return createStep(UPDATE_PACKAGES_STEP_ID, 'Update Package Version', async (ctx) => {
3175
+ const { tree, projectRoot, state, logger } = ctx;
3176
+ const { nextVersion, bumpType, currentVersion } = state;
3177
+ // Skip if no bump needed
3178
+ if (!nextVersion || bumpType === 'none') {
3179
+ return createSkippedResult('No version bump needed');
3180
+ }
3181
+ const packageJsonPath = `${projectRoot}/package.json`;
3182
+ // Read package.json
3183
+ let content;
3184
+ try {
3185
+ content = tree.read(packageJsonPath, 'utf-8') ?? '';
3186
+ if (!content) {
3187
+ return {
3188
+ status: 'failed',
3189
+ error: createError('package.json not found'),
3190
+ message: 'Could not read package.json',
3191
+ };
3192
+ }
3193
+ }
3194
+ catch (error) {
3195
+ return {
3196
+ status: 'failed',
3197
+ error: error instanceof Error ? error : createError(String(error)),
3198
+ message: 'Failed to read package.json',
3199
+ };
3200
+ }
3201
+ // Parse and update version
3202
+ let pkg;
3203
+ try {
3204
+ pkg = parse(content);
3205
+ }
3206
+ catch (error) {
3207
+ return {
3208
+ status: 'failed',
3209
+ error: error instanceof Error ? error : createError(String(error)),
3210
+ message: 'Failed to parse package.json',
3211
+ };
3212
+ }
3213
+ pkg['version'] = nextVersion;
3214
+ // Write back with preserved formatting
3215
+ const updated = stringify(pkg, null, 2) + '\n';
3216
+ tree.write(packageJsonPath, updated);
3217
+ logger.info(`Updated package.json: ${currentVersion} → ${nextVersion}`);
3218
+ return {
3219
+ status: 'success',
3220
+ stateUpdates: {
3221
+ modifiedFiles: [...(state.modifiedFiles ?? []), packageJsonPath],
3222
+ },
3223
+ message: `Updated version to ${nextVersion}`,
3224
+ };
3225
+ }, {
3226
+ dependsOn: ['calculate-bump'],
3227
+ });
3228
+ }
3229
+ /**
3230
+ * Creates a step that updates dependent packages in a monorepo.
3231
+ *
3232
+ * This step cascades version updates to packages that depend
3233
+ * on the updated package.
3234
+ *
3235
+ * @returns A FlowStep that cascades dependency updates
3236
+ */
3237
+ function createCascadeDependenciesStep() {
3238
+ return createStep('cascade-dependencies', 'Cascade Dependency Updates', async (ctx) => {
3239
+ const { config, state, logger } = ctx;
3240
+ const { nextVersion, bumpType } = state;
3241
+ // Skip if dependency tracking not enabled
3242
+ if (!config.trackDeps) {
3243
+ return createSkippedResult('Dependency tracking not enabled');
3244
+ }
3245
+ // Skip if no bump needed
3246
+ if (!nextVersion || bumpType === 'none') {
3247
+ return createSkippedResult('No version bump to cascade');
3248
+ }
3249
+ // In a full implementation, this would:
3250
+ // 1. Use workspace discovery to find dependent packages
3251
+ // 2. Update their dependency references
3252
+ // 3. Track the modified files
3253
+ logger.warn('Cascade dependencies step is not fully implemented yet');
3254
+ return {
3255
+ status: 'success',
3256
+ message: 'Dependency cascade skipped (not fully implemented)',
3257
+ };
3258
+ }, {
3259
+ dependsOn: ['update-packages'],
3260
+ });
3261
+ }
3262
+
3263
+ /**
3264
+ * Interpolates template variables in a string.
3265
+ *
3266
+ * Supports: ${projectName}, ${packageName}, ${version}
3267
+ *
3268
+ * @param template - Template string with ${var} placeholders
3269
+ * @param vars - Variable values
3270
+ * @returns Interpolated string
3271
+ */
3272
+ function interpolate(template, vars) {
3273
+ let result = template;
3274
+ for (const [key, value] of entries(vars)) {
3275
+ const placeholder = '${' + key + '}';
3276
+ let index = result.indexOf(placeholder);
3277
+ while (index !== -1) {
3278
+ result = result.slice(0, index) + value + result.slice(index + placeholder.length);
3279
+ index = result.indexOf(placeholder, index + value.length);
3280
+ }
3281
+ }
3282
+ return result;
3283
+ }
3284
+
3285
+ const CREATE_COMMIT_STEP_ID = 'create-commit';
3286
+ /**
3287
+ * Creates the create-commit step.
3288
+ *
3289
+ * This step:
3290
+ * 1. Stages modified files
3291
+ * 2. Creates a commit with the configured message
3292
+ *
3293
+ * State updates:
3294
+ * - commitHash: Hash of the created commit
3295
+ *
3296
+ * @returns A FlowStep that creates a git commit
3297
+ */
3298
+ function createGitCommitStep() {
3299
+ return createStep(CREATE_COMMIT_STEP_ID, 'Create Version Commit', async (ctx) => {
3300
+ const { git, config, state, projectName, packageName, logger } = ctx;
3301
+ const { nextVersion, bumpType, modifiedFiles } = state;
3302
+ // Skip if git operations disabled
3303
+ if (config.skipGit) {
3304
+ return createSkippedResult('Git operations disabled');
3305
+ }
3306
+ // Skip if no bump needed
3307
+ if (!nextVersion || bumpType === 'none') {
3308
+ return createSkippedResult('No version bump, no commit needed');
3309
+ }
3310
+ // Skip if dry run
3311
+ if (config.dryRun) {
3312
+ const message = interpolate(config.commitMessage ?? 'chore(${projectName}): release version ${version}', {
3313
+ projectName,
3314
+ packageName,
3315
+ version: nextVersion,
3316
+ });
3317
+ return {
3318
+ status: 'success',
3319
+ message: `[DRY RUN] Would commit: "${message}"`,
3320
+ };
3321
+ }
3322
+ // Stage files
3323
+ if (modifiedFiles && modifiedFiles.length > 0) {
3324
+ try {
3325
+ git.stage(modifiedFiles);
3326
+ logger.debug(`Staged ${modifiedFiles.length} file(s)`);
3327
+ }
3328
+ catch (error) {
3329
+ return {
3330
+ status: 'failed',
3331
+ error: error instanceof Error ? error : createError(String(error)),
3332
+ message: 'Failed to stage files',
3333
+ };
3334
+ }
3335
+ }
3336
+ else {
3337
+ // Stage all changes
3338
+ try {
3339
+ git.stageAll();
3340
+ logger.debug('Staged all changes');
3341
+ }
3342
+ catch (error) {
3343
+ return {
3344
+ status: 'failed',
3345
+ error: error instanceof Error ? error : createError(String(error)),
3346
+ message: 'Failed to stage files',
3347
+ };
3348
+ }
3349
+ }
3350
+ // Create commit message
3351
+ const message = interpolate(config.commitMessage ?? 'chore(${projectName}): release version ${version}', {
3352
+ projectName,
3353
+ packageName,
3354
+ version: nextVersion,
3355
+ });
3356
+ // Create commit
3357
+ try {
3358
+ const commit = git.createCommit(message);
3359
+ logger.info(`Created commit: ${commit.hash.slice(0, 7)}`);
3360
+ return {
3361
+ status: 'success',
3362
+ stateUpdates: {
3363
+ commitHash: commit.hash,
3364
+ },
3365
+ message: `Created commit ${commit.hash.slice(0, 7)}: ${message}`,
3366
+ };
3367
+ }
3368
+ catch (error) {
3369
+ return {
3370
+ status: 'failed',
3371
+ error: error instanceof Error ? error : createError(String(error)),
3372
+ message: 'Failed to create commit',
3373
+ };
3374
+ }
3375
+ }, {
3376
+ dependsOn: ['update-packages', 'write-changelog'],
3377
+ });
3378
+ }
3379
+
3380
+ const CREATE_TAG_STEP_ID = 'create-tag';
3381
+ /**
3382
+ * Creates the create-tag step.
3383
+ *
3384
+ * This step:
3385
+ * 1. Creates an annotated git tag
3386
+ * 2. Uses the configured tag format
3387
+ *
3388
+ * State updates:
3389
+ * - tagName: Name of the created tag
3390
+ *
3391
+ * @returns A FlowStep that creates a git tag
3392
+ */
3393
+ function createTagStep() {
3394
+ return createStep(CREATE_TAG_STEP_ID, 'Create Git Tag', async (ctx) => {
3395
+ const { git, config, state, projectName, packageName, logger } = ctx;
3396
+ const { nextVersion, bumpType, changelogEntry } = state;
3397
+ // Skip if git operations disabled
3398
+ if (config.skipGit) {
3399
+ return createSkippedResult('Git operations disabled');
3400
+ }
3401
+ // Skip if tags disabled
3402
+ if (config.skipTag) {
3403
+ return createSkippedResult('Tag creation disabled');
3404
+ }
3405
+ // Skip if no bump needed
3406
+ if (!nextVersion || bumpType === 'none') {
3407
+ return createSkippedResult('No version bump, no tag needed');
3408
+ }
3409
+ // Generate tag name
3410
+ const tagName = interpolate(config.tagFormat ?? '${projectName}@${version}', {
3411
+ projectName,
3412
+ packageName,
3413
+ version: nextVersion,
3414
+ });
3415
+ // Skip if dry run
3416
+ if (config.dryRun) {
3417
+ return {
3418
+ status: 'success',
3419
+ stateUpdates: { tagName },
3420
+ message: `[DRY RUN] Would create tag: ${tagName}`,
3421
+ };
3422
+ }
3423
+ // Create tag message from changelog entry if available
3424
+ let tagMessage = `Release ${nextVersion}`;
3425
+ if (changelogEntry && changelogEntry.sections.length > 0) {
3426
+ const highlights = [];
3427
+ for (const section of changelogEntry.sections.slice(0, 3)) {
3428
+ const itemCount = section.items.length;
3429
+ highlights.push(`${section.type}: ${itemCount} change${itemCount !== 1 ? 's' : ''}`);
3430
+ }
3431
+ tagMessage = `Release ${nextVersion}\n\n${highlights.join('\n')}`;
3432
+ }
3433
+ // Create tag
3434
+ try {
3435
+ const tag = git.createTag(tagName, {
3436
+ message: tagMessage,
3437
+ });
3438
+ logger.info(`Created tag: ${tag.name}`);
3439
+ return {
3440
+ status: 'success',
3441
+ stateUpdates: {
3442
+ tagName: tag.name,
3443
+ },
3444
+ message: `Created tag: ${tagName}`,
3445
+ };
3446
+ }
3447
+ catch (error) {
3448
+ return {
3449
+ status: 'failed',
3450
+ error: error instanceof Error ? error : createError(String(error)),
3451
+ message: `Failed to create tag: ${tagName}`,
3452
+ };
3453
+ }
3454
+ }, {
3455
+ dependsOn: ['create-commit'],
3456
+ });
3457
+ }
3458
+ /**
3459
+ * Creates a step that pushes the created tag to remote.
3460
+ *
3461
+ * @returns A FlowStep that pushes the git tag
3462
+ */
3463
+ function createPushTagStep() {
3464
+ return createStep('push-tag', 'Push Git Tag', async (ctx) => {
3465
+ const { git, config, state, logger } = ctx;
3466
+ const { tagName } = state;
3467
+ // Skip if git operations disabled
3468
+ if (config.skipGit || config.skipTag) {
3469
+ return createSkippedResult('Git/tag operations disabled');
3470
+ }
3471
+ // Skip if no tag created
3472
+ if (!tagName) {
3473
+ return createSkippedResult('No tag to push');
3474
+ }
3475
+ // Skip if dry run
3476
+ if (config.dryRun) {
3477
+ return {
3478
+ status: 'success',
3479
+ message: `[DRY RUN] Would push tag: ${tagName}`,
3480
+ };
3481
+ }
3482
+ try {
3483
+ git.pushTag(tagName);
3484
+ logger.info(`Pushed tag: ${tagName}`);
3485
+ return {
3486
+ status: 'success',
3487
+ message: `Pushed tag: ${tagName}`,
3488
+ };
3489
+ }
3490
+ catch (error) {
3491
+ return {
3492
+ status: 'failed',
3493
+ error: error instanceof Error ? error : createError(String(error)),
3494
+ message: `Failed to push tag: ${tagName}`,
3495
+ };
3496
+ }
3497
+ }, {
3498
+ dependsOn: ['create-tag'],
3499
+ continueOnError: true, // Don't fail flow if push fails
3500
+ });
3501
+ }
3502
+
3503
+ export { ANALYZE_COMMITS_STEP_ID, CALCULATE_BUMP_STEP_ID, CREATE_COMMIT_STEP_ID, CREATE_TAG_STEP_ID, FETCH_REGISTRY_STEP_ID, GENERATE_CHANGELOG_STEP_ID, UPDATE_PACKAGES_STEP_ID, createAnalyzeCommitsStep, createCalculateBumpStep, createCascadeDependenciesStep, createCheckIdempotencyStep, createFetchRegistryStep, createGenerateChangelogStep, createGitCommitStep, createPushTagStep, createTagStep, createUpdatePackageStep, createWriteChangelogStep };
3504
+ //# sourceMappingURL=index.esm.js.map