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