@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,2785 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Creates a git commit model.
5
+ *
6
+ * @param options - Commit creation options
7
+ * @returns A new GitCommit object
8
+ *
9
+ * @example
10
+ * const commit = createGitCommit({
11
+ * hash: 'abc123...',
12
+ * authorName: 'John Doe',
13
+ * authorEmail: 'john@example.com',
14
+ * authorDate: '2026-03-12T10:00:00Z',
15
+ * subject: 'feat: add new feature',
16
+ * })
17
+ */
18
+ function createGitCommit(options) {
19
+ const body = options.body ?? '';
20
+ const subject = options.subject;
21
+ return {
22
+ hash: options.hash,
23
+ shortHash: getShortHash(options.hash),
24
+ authorName: options.authorName,
25
+ authorEmail: options.authorEmail,
26
+ authorDate: options.authorDate,
27
+ committerName: options.committerName ?? options.authorName,
28
+ committerEmail: options.committerEmail ?? options.authorEmail,
29
+ commitDate: options.commitDate ?? options.authorDate,
30
+ subject,
31
+ body,
32
+ message: body ? `${subject}\n\n${body}` : subject,
33
+ parents: options.parents ?? [],
34
+ refs: options.refs ?? [],
35
+ };
36
+ }
37
+ /**
38
+ * Gets a short hash (7 characters) from a full commit hash.
39
+ *
40
+ * @param hash - Full commit hash
41
+ * @returns Short hash (7 characters)
42
+ */
43
+ function getShortHash(hash) {
44
+ return hash.slice(0, 7);
45
+ }
46
+ /**
47
+ * Checks if two commits are the same based on their hash.
48
+ *
49
+ * @param a - First commit
50
+ * @param b - Second commit
51
+ * @returns True if commits have the same hash
52
+ */
53
+ function isSameCommit(a, b) {
54
+ return a.hash === b.hash;
55
+ }
56
+ /**
57
+ * Checks if a commit is a merge commit (has multiple parents).
58
+ *
59
+ * @param commit - Commit to check
60
+ * @returns True if commit has more than one parent
61
+ */
62
+ function isMergeCommit(commit) {
63
+ return commit.parents.length > 1;
64
+ }
65
+ /**
66
+ * Checks if a commit is a root commit (has no parents).
67
+ *
68
+ * @param commit - Commit to check
69
+ * @returns True if commit has no parents
70
+ */
71
+ function isRootCommit(commit) {
72
+ return commit.parents.length === 0;
73
+ }
74
+ /**
75
+ * Extracts the scope from a commit subject if it follows conventional commit format.
76
+ * Uses character-by-character parsing (no regex).
77
+ *
78
+ * @param subject - Commit subject line
79
+ * @returns Scope string or undefined if no scope found
80
+ *
81
+ * @example
82
+ * extractScope('feat(lib-versioning): add git support') // 'lib-versioning'
83
+ * extractScope('fix: resolve issue') // undefined
84
+ */
85
+ function extractScope(subject) {
86
+ // Look for pattern: type(scope): or type(scope)!:
87
+ let i = 0;
88
+ // Skip type characters (a-z)
89
+ while (i < subject.length) {
90
+ const code = subject.charCodeAt(i);
91
+ if (code >= 97 && code <= 122) {
92
+ // a-z
93
+ i++;
94
+ }
95
+ else {
96
+ break;
97
+ }
98
+ }
99
+ // Must find opening parenthesis
100
+ if (i >= subject.length || subject[i] !== '(') {
101
+ return undefined;
102
+ }
103
+ // Skip '('
104
+ i++;
105
+ const scopeStart = i;
106
+ // Find closing parenthesis
107
+ while (i < subject.length && subject[i] !== ')') {
108
+ i++;
109
+ }
110
+ if (i >= subject.length) {
111
+ return undefined;
112
+ }
113
+ const scope = subject.slice(scopeStart, i);
114
+ return scope || undefined;
115
+ }
116
+ /**
117
+ * Extracts the type from a commit subject if it follows conventional commit format.
118
+ * Uses character-by-character parsing (no regex).
119
+ *
120
+ * @param subject - Commit subject line
121
+ * @returns Type string or undefined if no valid type found
122
+ *
123
+ * @example
124
+ * extractType('feat(lib-versioning): add git support') // 'feat'
125
+ * extractType('fix: resolve issue') // 'fix'
126
+ * extractType('random message') // undefined
127
+ */
128
+ function extractType(subject) {
129
+ let i = 0;
130
+ // Collect type characters (a-z)
131
+ while (i < subject.length) {
132
+ const code = subject.charCodeAt(i);
133
+ if (code >= 97 && code <= 122) {
134
+ // a-z
135
+ i++;
136
+ }
137
+ else {
138
+ break;
139
+ }
140
+ }
141
+ if (i === 0) {
142
+ return undefined;
143
+ }
144
+ const type = subject.slice(0, i);
145
+ // Next character must be '(' , '!' or ':'
146
+ if (i >= subject.length) {
147
+ return undefined;
148
+ }
149
+ const next = subject[i];
150
+ if (next === '(' || next === ':' || next === '!') {
151
+ return type;
152
+ }
153
+ return undefined;
154
+ }
155
+
156
+ /**
157
+ * Safe copies of Math built-in methods.
158
+ *
159
+ * These references are captured at module initialization time to protect against
160
+ * prototype pollution attacks. Import only what you need for tree-shaking.
161
+ *
162
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
163
+ */
164
+ // Capture references at module initialization time
165
+ const _Math = globalThis.Math;
166
+ // ============================================================================
167
+ // Min/Max
168
+ // ============================================================================
169
+ /**
170
+ * (Safe copy) Returns the larger of zero or more numbers.
171
+ */
172
+ const max = _Math.max;
173
+
174
+ /**
175
+ * Creates a lightweight git tag.
176
+ *
177
+ * @param options - Tag creation options
178
+ * @returns A new GitTag object
179
+ *
180
+ * @example
181
+ * const tag = createLightweightTag({
182
+ * name: 'v1.0.0',
183
+ * commitHash: 'abc123...',
184
+ * })
185
+ */
186
+ function createLightweightTag(options) {
187
+ return {
188
+ name: options.name,
189
+ commitHash: options.commitHash,
190
+ type: 'lightweight',
191
+ };
192
+ }
193
+ /**
194
+ * Creates an annotated git tag.
195
+ *
196
+ * @param options - Tag creation options
197
+ * @returns A new GitTag object
198
+ *
199
+ * @example
200
+ * const tag = createAnnotatedTag({
201
+ * name: 'v1.0.0',
202
+ * commitHash: 'abc123...',
203
+ * message: 'Release v1.0.0',
204
+ * taggerName: 'John Doe',
205
+ * taggerEmail: 'john@example.com',
206
+ * tagDate: '2026-03-12T10:00:00Z',
207
+ * })
208
+ */
209
+ function createAnnotatedTag(options) {
210
+ return {
211
+ name: options.name,
212
+ commitHash: options.commitHash,
213
+ type: 'annotated',
214
+ message: options.message,
215
+ taggerName: options.taggerName,
216
+ taggerEmail: options.taggerEmail,
217
+ tagDate: options.tagDate,
218
+ };
219
+ }
220
+ /**
221
+ * Checks if a tag is annotated.
222
+ *
223
+ * @param tag - Tag to check
224
+ * @returns True if tag is annotated
225
+ */
226
+ function isAnnotatedTag(tag) {
227
+ return tag.type === 'annotated';
228
+ }
229
+ /**
230
+ * Checks if a tag is lightweight.
231
+ *
232
+ * @param tag - Tag to check
233
+ * @returns True if tag is lightweight
234
+ */
235
+ function isLightweightTag(tag) {
236
+ return tag.type === 'lightweight';
237
+ }
238
+ /**
239
+ * Extracts version from tag name.
240
+ * Handles common formats: v1.2.3, `@scope/package@1.2.3`, package@1.2.3
241
+ * Uses character-by-character parsing (no regex).
242
+ *
243
+ * @param tagName - Tag name to parse
244
+ * @returns The extracted version string or undefined if no version found
245
+ *
246
+ * @example
247
+ * extractVersionFromTag('v1.2.3') // '1.2.3'
248
+ * extractVersionFromTag('@scope/pkg@1.2.3') // '1.2.3'
249
+ * extractVersionFromTag('release-1.2.3') // '1.2.3'
250
+ */
251
+ function extractVersionFromTag(tagName) {
252
+ // Strategy: Find last occurrence of '@' followed by version, or 'v' followed by version
253
+ // Version starts with digit and contains digits, dots, and possibly prerelease identifiers
254
+ // First, try to find @version pattern (for scoped packages)
255
+ let i = tagName.length - 1;
256
+ // Find last '@'
257
+ while (i >= 0 && tagName[i] !== '@') {
258
+ i--;
259
+ }
260
+ if (i >= 0) {
261
+ // Found '@', check if followed by version
262
+ const afterAt = tagName.slice(i + 1);
263
+ const version = parseVersionPart(afterAt);
264
+ if (version) {
265
+ return version;
266
+ }
267
+ }
268
+ // Try to find 'v' followed by version
269
+ i = 0;
270
+ while (i < tagName.length) {
271
+ if ((tagName[i] === 'v' || tagName[i] === 'V') && i + 1 < tagName.length) {
272
+ const nextCode = tagName.charCodeAt(i + 1);
273
+ if (nextCode >= 48 && nextCode <= 57) {
274
+ // 0-9
275
+ const version = parseVersionPart(tagName.slice(i + 1));
276
+ if (version) {
277
+ return version;
278
+ }
279
+ }
280
+ }
281
+ i++;
282
+ }
283
+ // Try to find digit sequence after common separators (-, _)
284
+ i = 0;
285
+ while (i < tagName.length) {
286
+ const char = tagName[i];
287
+ if (char === '-' || char === '_') {
288
+ const afterSep = tagName.slice(i + 1);
289
+ const code = afterSep.charCodeAt(0);
290
+ if (code >= 48 && code <= 57) {
291
+ // 0-9
292
+ const version = parseVersionPart(afterSep);
293
+ if (version) {
294
+ return version;
295
+ }
296
+ }
297
+ }
298
+ i++;
299
+ }
300
+ // Last resort: if entire string is a version
301
+ return parseVersionPart(tagName);
302
+ }
303
+ /**
304
+ * Parses a version-like string from the start of input.
305
+ * Uses character-by-character parsing (no regex).
306
+ *
307
+ * @param input - String to parse
308
+ * @returns Version string or undefined
309
+ */
310
+ function parseVersionPart(input) {
311
+ if (!input)
312
+ return undefined;
313
+ // Must start with digit
314
+ const firstCode = input.charCodeAt(0);
315
+ if (firstCode < 48 || firstCode > 57) {
316
+ return undefined;
317
+ }
318
+ // Collect version characters: digits, dots, hyphens, plus, letters (for prerelease)
319
+ let i = 0;
320
+ let dotCount = 0;
321
+ while (i < input.length) {
322
+ const code = input.charCodeAt(i);
323
+ if (code >= 48 && code <= 57) {
324
+ // 0-9
325
+ i++;
326
+ }
327
+ else if (code === 46) {
328
+ // .
329
+ dotCount++;
330
+ i++;
331
+ }
332
+ else if (code === 45) {
333
+ // -
334
+ i++;
335
+ }
336
+ else if (code === 43) {
337
+ // +
338
+ i++;
339
+ }
340
+ else if ((code >= 97 && code <= 122) || (code >= 65 && code <= 90)) {
341
+ // a-z, A-Z
342
+ i++;
343
+ }
344
+ else {
345
+ break;
346
+ }
347
+ }
348
+ // Must have at least one dot (e.g., 1.0)
349
+ if (dotCount === 0) {
350
+ return undefined;
351
+ }
352
+ const version = input.slice(0, i);
353
+ // Basic validation: must contain at least two parts separated by dots
354
+ if (!version.includes('.')) {
355
+ return undefined;
356
+ }
357
+ return version;
358
+ }
359
+ /**
360
+ * Extracts package name from tag name.
361
+ * Handles formats like: `@scope/package@1.2.3`, package@1.2.3, package-v1.2.3
362
+ * Uses character-by-character parsing (no regex).
363
+ *
364
+ * @param tagName - Tag name to parse
365
+ * @returns The extracted package name or undefined if not found
366
+ *
367
+ * @example
368
+ * extractPackageFromTag('@scope/pkg@1.2.3') // '@scope/pkg'
369
+ * extractPackageFromTag('lib-utils@1.2.3') // 'lib-utils'
370
+ * extractPackageFromTag('v1.2.3') // undefined
371
+ */
372
+ function extractPackageFromTag(tagName) {
373
+ // Find the last '@' that's followed by a version number
374
+ let lastVersionAt = -1;
375
+ let i = tagName.length - 1;
376
+ while (i >= 0) {
377
+ if (tagName[i] === '@') {
378
+ // Check if followed by a digit
379
+ if (i + 1 < tagName.length) {
380
+ const nextCode = tagName.charCodeAt(i + 1);
381
+ if (nextCode >= 48 && nextCode <= 57) {
382
+ // 0-9
383
+ lastVersionAt = i;
384
+ break;
385
+ }
386
+ }
387
+ }
388
+ i--;
389
+ }
390
+ if (lastVersionAt > 0) {
391
+ return tagName.slice(0, lastVersionAt);
392
+ }
393
+ // Check for -v or _v pattern
394
+ i = tagName.length - 1;
395
+ while (i > 0) {
396
+ if (tagName[i] === 'v' || tagName[i] === 'V') {
397
+ const prev = tagName[i - 1];
398
+ if (prev === '-' || prev === '_') {
399
+ // Check if followed by digit
400
+ if (i + 1 < tagName.length) {
401
+ const nextCode = tagName.charCodeAt(i + 1);
402
+ if (nextCode >= 48 && nextCode <= 57) {
403
+ return tagName.slice(0, i - 1);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ i--;
409
+ }
410
+ return undefined;
411
+ }
412
+ /**
413
+ * Builds a tag name from package name and version.
414
+ *
415
+ * @param packageName - Package name (e.g., '@scope/package' or 'package')
416
+ * @param version - Version string (e.g., '1.2.3')
417
+ * @param format - Tag format template, uses ${package} and ${version} placeholders
418
+ * @returns Formatted tag name
419
+ *
420
+ * @example
421
+ * buildTagName('@scope/pkg', '1.2.3') // '@scope/pkg@1.2.3'
422
+ * buildTagName('utils', '1.0.0', 'v${version}') // 'v1.0.0'
423
+ * buildTagName('pkg', '2.0.0', '${package}-v${version}') // 'pkg-v2.0.0'
424
+ */
425
+ function buildTagName(packageName, version, format = '${package}@${version}') {
426
+ // Simple character-by-character replacement (no regex)
427
+ let result = '';
428
+ let i = 0;
429
+ while (i < format.length) {
430
+ if (i + 9 < format.length && format.slice(i, i + 10) === '${package}') {
431
+ result += packageName;
432
+ i += 10;
433
+ }
434
+ else if (i + 9 < format.length && format.slice(i, i + 10) === '${version}') {
435
+ result += version;
436
+ i += 10;
437
+ }
438
+ else {
439
+ result += format[i];
440
+ i++;
441
+ }
442
+ }
443
+ return result;
444
+ }
445
+ /**
446
+ * Compares two tags by version (newest first).
447
+ * Useful for sorting tags.
448
+ *
449
+ * @param a - First tag
450
+ * @param b - Second tag
451
+ * @returns Comparison result (-1, 0, or 1)
452
+ */
453
+ function compareTagsByVersion(a, b) {
454
+ const versionA = extractVersionFromTag(a.name);
455
+ const versionB = extractVersionFromTag(b.name);
456
+ if (!versionA && !versionB)
457
+ return 0;
458
+ if (!versionA)
459
+ return 1;
460
+ if (!versionB)
461
+ return -1;
462
+ return compareVersionStrings(versionB, versionA); // Descending order
463
+ }
464
+ /**
465
+ * Simple version string comparison.
466
+ * Compares major.minor.patch numerically.
467
+ *
468
+ * @param a - First version
469
+ * @param b - Second version
470
+ * @returns Comparison result (-1, 0, or 1)
471
+ */
472
+ function compareVersionStrings(a, b) {
473
+ const partsA = a.split('.');
474
+ const partsB = b.split('.');
475
+ const maxLen = max(partsA.length, partsB.length);
476
+ for (let i = 0; i < maxLen; i++) {
477
+ const numA = parseNumericPart(partsA[i]);
478
+ const numB = parseNumericPart(partsB[i]);
479
+ if (numA < numB)
480
+ return -1;
481
+ if (numA > numB)
482
+ return 1;
483
+ }
484
+ return 0;
485
+ }
486
+ /**
487
+ * Parses numeric part of version segment.
488
+ *
489
+ * @param part - Version part to parse
490
+ * @returns Numeric value
491
+ */
492
+ function parseNumericPart(part) {
493
+ if (!part)
494
+ return 0;
495
+ // Extract leading digits only
496
+ let num = 0;
497
+ for (let i = 0; i < part.length; i++) {
498
+ const code = part.charCodeAt(i);
499
+ if (code >= 48 && code <= 57) {
500
+ num = num * 10 + (code - 48);
501
+ }
502
+ else {
503
+ break;
504
+ }
505
+ }
506
+ return num;
507
+ }
508
+
509
+ /**
510
+ * Creates a git reference from full name.
511
+ * Parses the reference type from the full name.
512
+ *
513
+ * @param options - Reference creation options
514
+ * @returns A new GitRef object
515
+ *
516
+ * @example
517
+ * const ref = createGitRef({
518
+ * fullName: 'refs/heads/main',
519
+ * commitHash: 'abc123...',
520
+ * })
521
+ */
522
+ function createGitRef(options) {
523
+ const { type, name, remote } = parseRefName(options.fullName);
524
+ return {
525
+ fullName: options.fullName,
526
+ name,
527
+ type,
528
+ commitHash: options.commitHash,
529
+ remote,
530
+ isHead: options.isHead,
531
+ };
532
+ }
533
+ /**
534
+ * Parses a full reference name into its components.
535
+ * Uses character-by-character parsing (no regex).
536
+ *
537
+ * @param fullName - Full reference name
538
+ * @returns Parsed components
539
+ *
540
+ * @example
541
+ * parseRefName('refs/heads/main') // { type: 'branch', name: 'main' }
542
+ * parseRefName('refs/remotes/origin/main') // { type: 'remote', name: 'main', remote: 'origin' }
543
+ */
544
+ function parseRefName(fullName) {
545
+ // Handle HEAD specially
546
+ if (fullName === 'HEAD') {
547
+ return { type: 'head', name: 'HEAD' };
548
+ }
549
+ // Split by '/' character
550
+ const parts = splitByChar(fullName, '/');
551
+ // refs/heads/... -> branch
552
+ if (parts.length >= 3 && parts[0] === 'refs' && parts[1] === 'heads') {
553
+ return {
554
+ type: 'branch',
555
+ name: parts.slice(2).join('/'),
556
+ };
557
+ }
558
+ // refs/tags/... -> tag
559
+ if (parts.length >= 3 && parts[0] === 'refs' && parts[1] === 'tags') {
560
+ return {
561
+ type: 'tag',
562
+ name: parts.slice(2).join('/'),
563
+ };
564
+ }
565
+ // refs/remotes/origin/... -> remote
566
+ if (parts.length >= 4 && parts[0] === 'refs' && parts[1] === 'remotes') {
567
+ return {
568
+ type: 'remote',
569
+ name: parts.slice(3).join('/'),
570
+ remote: parts[2],
571
+ };
572
+ }
573
+ // refs/stash -> stash
574
+ if (parts.length >= 2 && parts[0] === 'refs' && parts[1] === 'stash') {
575
+ return {
576
+ type: 'stash',
577
+ name: parts.slice(1).join('/'),
578
+ };
579
+ }
580
+ // Default to branch for unknown patterns
581
+ return {
582
+ type: 'branch',
583
+ name: fullName,
584
+ };
585
+ }
586
+ /**
587
+ * Splits a string by a character.
588
+ * Character-by-character implementation (no regex).
589
+ *
590
+ * @param str - String to split
591
+ * @param char - Character to split by
592
+ * @returns Array of parts
593
+ */
594
+ function splitByChar(str, char) {
595
+ const parts = [];
596
+ let current = '';
597
+ for (let i = 0; i < str.length; i++) {
598
+ if (str[i] === char) {
599
+ parts.push(current);
600
+ current = '';
601
+ }
602
+ else {
603
+ current += str[i];
604
+ }
605
+ }
606
+ parts.push(current);
607
+ return parts;
608
+ }
609
+ /**
610
+ * Checks if a reference is a branch.
611
+ *
612
+ * @param ref - Reference to check
613
+ * @returns True if reference is a branch
614
+ */
615
+ function isBranchRef(ref) {
616
+ return ref.type === 'branch';
617
+ }
618
+ /**
619
+ * Checks if a reference is a tag.
620
+ *
621
+ * @param ref - Reference to check
622
+ * @returns True if reference is a tag
623
+ */
624
+ function isTagRef(ref) {
625
+ return ref.type === 'tag';
626
+ }
627
+ /**
628
+ * Checks if a reference is a remote tracking branch.
629
+ *
630
+ * @param ref - Reference to check
631
+ * @returns True if reference is a remote
632
+ */
633
+ function isRemoteRef(ref) {
634
+ return ref.type === 'remote';
635
+ }
636
+ /**
637
+ * Checks if a reference points to the current HEAD.
638
+ *
639
+ * @param ref - Reference to check
640
+ * @returns True if reference is HEAD
641
+ */
642
+ function isHeadRef(ref) {
643
+ return ref.type === 'head' || ref.isHead === true;
644
+ }
645
+ /**
646
+ * Gets the tracking remote for a reference.
647
+ *
648
+ * @param ref - Reference to check
649
+ * @returns Remote name or undefined
650
+ */
651
+ function getRemote(ref) {
652
+ return ref.remote;
653
+ }
654
+ /**
655
+ * Builds a full reference name from type and name.
656
+ *
657
+ * @param type - Reference type
658
+ * @param name - Reference name
659
+ * @param remote - Remote name (for remote type)
660
+ * @returns Full reference name
661
+ *
662
+ * @example
663
+ * buildRefName('branch', 'main') // 'refs/heads/main'
664
+ * buildRefName('tag', 'v1.0.0') // 'refs/tags/v1.0.0'
665
+ * buildRefName('remote', 'main', 'origin') // 'refs/remotes/origin/main'
666
+ */
667
+ function buildRefName(type, name, remote) {
668
+ switch (type) {
669
+ case 'branch':
670
+ return `refs/heads/${name}`;
671
+ case 'tag':
672
+ return `refs/tags/${name}`;
673
+ case 'remote':
674
+ return remote ? `refs/remotes/${remote}/${name}` : `refs/remotes/${name}`;
675
+ case 'head':
676
+ return 'HEAD';
677
+ case 'stash':
678
+ return `refs/stash`;
679
+ default:
680
+ return name;
681
+ }
682
+ }
683
+ /**
684
+ * Compares two references by name (alphabetically).
685
+ *
686
+ * @param a - First reference
687
+ * @param b - Second reference
688
+ * @returns Comparison result (-1, 0, or 1)
689
+ */
690
+ function compareRefsByName(a, b) {
691
+ if (a.name < b.name)
692
+ return -1;
693
+ if (a.name > b.name)
694
+ return 1;
695
+ return 0;
696
+ }
697
+ /**
698
+ * Filters references by type.
699
+ *
700
+ * @param refs - References to filter
701
+ * @param type - Type to filter by
702
+ * @returns Filtered references
703
+ */
704
+ function filterRefsByType(refs, type) {
705
+ const result = [];
706
+ for (const ref of refs) {
707
+ if (ref.type === type) {
708
+ result.push(ref);
709
+ }
710
+ }
711
+ return result;
712
+ }
713
+ /**
714
+ * Filters references by remote.
715
+ *
716
+ * @param refs - References to filter
717
+ * @param remote - Remote name to filter by
718
+ * @returns Filtered references
719
+ */
720
+ function filterRefsByRemote(refs, remote) {
721
+ const result = [];
722
+ for (const ref of refs) {
723
+ if (ref.remote === remote) {
724
+ result.push(ref);
725
+ }
726
+ }
727
+ return result;
728
+ }
729
+
730
+ /**
731
+ * Safe copies of Error built-ins via factory functions.
732
+ *
733
+ * Since constructors cannot be safely captured via Object.assign, this module
734
+ * provides factory functions that use Reflect.construct internally.
735
+ *
736
+ * These references are captured at module initialization time to protect against
737
+ * prototype pollution attacks. Import only what you need for tree-shaking.
738
+ *
739
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/error
740
+ */
741
+ // Capture references at module initialization time
742
+ const _Error = globalThis.Error;
743
+ const _Reflect$1 = globalThis.Reflect;
744
+ /**
745
+ * (Safe copy) Creates a new Error using the captured Error constructor.
746
+ * Use this instead of `new Error()`.
747
+ *
748
+ * @param message - Optional error message.
749
+ * @param options - Optional error options.
750
+ * @returns A new Error instance.
751
+ */
752
+ const createError = (message, options) => _Reflect$1.construct(_Error, [message, options]);
753
+
754
+ /**
755
+ * Default log options.
756
+ */
757
+ const DEFAULT_LOG_OPTIONS = {
758
+ maxCount: 100,
759
+ includeMerges: true,
760
+ timeout: 30000,
761
+ };
762
+ /**
763
+ * Git log format string for structured output.
764
+ * Uses ASCII delimiters that won't appear in commit messages.
765
+ */
766
+ const LOG_FORMAT = [
767
+ '%H', // full hash
768
+ '%an', // author name
769
+ '%ae', // author email
770
+ '%aI', // author date (ISO 8601)
771
+ '%cn', // committer name
772
+ '%ce', // committer email
773
+ '%cI', // commit date (ISO 8601)
774
+ '%s', // subject
775
+ '%b', // body
776
+ '%P', // parent hashes
777
+ '%D', // refs
778
+ ].join('%x00'); // NUL separator
779
+ /**
780
+ * Record separator for commit entries.
781
+ */
782
+ const RECORD_SEPARATOR = '\x1e'; // ASCII Record Separator
783
+ /**
784
+ * Gets the commit log from a git repository.
785
+ *
786
+ * @param options - Configuration for retrieving the commit log
787
+ * @returns Array of GitCommit objects
788
+ *
789
+ * @example
790
+ * const commits = getCommitLog({ maxCount: 10 })
791
+ * const recentChanges = getCommitLog({ from: 'v1.0.0', to: 'HEAD' })
792
+ */
793
+ function getCommitLog(options = {}) {
794
+ const opts = { ...DEFAULT_LOG_OPTIONS, ...options };
795
+ const args = ['log', `--format=${RECORD_SEPARATOR}${LOG_FORMAT}`];
796
+ // Add options
797
+ if (opts.maxCount !== undefined && opts.maxCount > 0) {
798
+ args.push(`-n${opts.maxCount}`);
799
+ }
800
+ if (!opts.includeMerges) {
801
+ args.push('--no-merges');
802
+ }
803
+ if (opts.author) {
804
+ const safeAuthor = escapeGitArg(opts.author);
805
+ args.push(`--author=${safeAuthor}`);
806
+ }
807
+ // Add range
808
+ if (opts.from && opts.to) {
809
+ const safeFrom = escapeGitRef(opts.from);
810
+ const safeTo = escapeGitRef(opts.to);
811
+ args.push(`${safeFrom}..${safeTo}`);
812
+ }
813
+ else if (opts.from) {
814
+ const safeFrom = escapeGitRef(opts.from);
815
+ args.push(`${safeFrom}..HEAD`);
816
+ }
817
+ else if (opts.to) {
818
+ const safeTo = escapeGitRef(opts.to);
819
+ args.push(safeTo);
820
+ }
821
+ // Add path filter
822
+ if (opts.path) {
823
+ const safePath = escapeGitPath(opts.path);
824
+ args.push('--', safePath);
825
+ }
826
+ try {
827
+ const output = execSync(`git ${args.join(' ')}`, {
828
+ encoding: 'utf-8',
829
+ cwd: opts.cwd,
830
+ timeout: opts.timeout,
831
+ stdio: ['pipe', 'pipe', 'pipe'],
832
+ maxBuffer: 50 * 1024 * 1024, // 50MB
833
+ });
834
+ return parseCommitLog(output);
835
+ }
836
+ catch (error) {
837
+ // Check if error is due to no commits
838
+ if (error instanceof Error && error.message.includes('does not have any commits')) {
839
+ return [];
840
+ }
841
+ throw error;
842
+ }
843
+ }
844
+ /**
845
+ * Gets commits between two references.
846
+ *
847
+ * @param from - Starting reference (exclusive)
848
+ * @param to - Ending reference (inclusive, default: HEAD)
849
+ * @param options - Additional options
850
+ * @returns Array of GitCommit objects
851
+ *
852
+ * @example
853
+ * const commits = getCommitsBetween('v1.0.0', 'v1.1.0')
854
+ */
855
+ function getCommitsBetween(from, to = 'HEAD', options = {}) {
856
+ return getCommitLog({ ...options, from, to });
857
+ }
858
+ /**
859
+ * Gets commits since a specific tag or reference.
860
+ *
861
+ * @param since - Reference to start from (exclusive)
862
+ * @param options - Additional options
863
+ * @returns Array of GitCommit objects
864
+ *
865
+ * @example
866
+ * const commits = getCommitsSince('v1.0.0')
867
+ */
868
+ function getCommitsSince(since, options = {}) {
869
+ return getCommitLog({ ...options, from: since });
870
+ }
871
+ /**
872
+ * Gets a single commit by its hash.
873
+ *
874
+ * @param hash - Commit hash (full or short)
875
+ * @param options - Additional options
876
+ * @returns GitCommit or null if not found
877
+ *
878
+ * @example
879
+ * const commit = getCommit('abc1234')
880
+ */
881
+ function getCommit(hash, options = {}) {
882
+ const safeHash = escapeGitRef(hash);
883
+ try {
884
+ const commits = getCommitLog({
885
+ ...options,
886
+ to: safeHash,
887
+ maxCount: 1,
888
+ });
889
+ return commits[0] ?? null;
890
+ }
891
+ catch {
892
+ return null;
893
+ }
894
+ }
895
+ /**
896
+ * Checks if a commit exists in the repository.
897
+ *
898
+ * @param hash - Commit hash to check
899
+ * @param options - Additional options
900
+ * @returns True if commit exists
901
+ */
902
+ function commitExists(hash, options = {}) {
903
+ const safeHash = escapeGitRef(hash);
904
+ try {
905
+ execSync(`git cat-file -t ${safeHash}`, {
906
+ encoding: 'utf-8',
907
+ cwd: options.cwd,
908
+ timeout: options.timeout ?? 5000,
909
+ stdio: ['pipe', 'pipe', 'pipe'],
910
+ });
911
+ return true;
912
+ }
913
+ catch {
914
+ return false;
915
+ }
916
+ }
917
+ /**
918
+ * Parses raw git log output into GitCommit objects.
919
+ *
920
+ * @param output - Raw git log output
921
+ * @returns Array of GitCommit objects
922
+ */
923
+ function parseCommitLog(output) {
924
+ const commits = [];
925
+ if (!output.trim()) {
926
+ return commits;
927
+ }
928
+ // Split by record separator
929
+ const records = splitByDelimiter(output, RECORD_SEPARATOR);
930
+ for (const record of records) {
931
+ const trimmed = record.trim();
932
+ if (!trimmed)
933
+ continue;
934
+ // Split by NUL character
935
+ const fields = splitByDelimiter(trimmed, '\x00');
936
+ if (fields.length < 10)
937
+ continue;
938
+ const [hash, authorName, authorEmail, authorDate, committerName, committerEmail, commitDate, subject, body, parentsStr, refsStr] = fields;
939
+ // Parse parents (space-separated hashes)
940
+ const parents = parentsStr ? splitByDelimiter(parentsStr, ' ').filter((p) => p.trim()) : [];
941
+ // Parse refs (comma-separated, may have prefixes like 'HEAD -> ')
942
+ const refs = parseRefs(refsStr || '');
943
+ commits.push(createGitCommit({
944
+ hash,
945
+ authorName,
946
+ authorEmail,
947
+ authorDate,
948
+ committerName,
949
+ committerEmail,
950
+ commitDate,
951
+ subject,
952
+ body: body || undefined,
953
+ parents,
954
+ refs,
955
+ }));
956
+ }
957
+ return commits;
958
+ }
959
+ /**
960
+ * Parses ref string from git log.
961
+ *
962
+ * @param refsStr - Raw refs string from git log
963
+ * @returns Array of ref names
964
+ */
965
+ function parseRefs(refsStr) {
966
+ if (!refsStr.trim()) {
967
+ return [];
968
+ }
969
+ const refs = [];
970
+ const parts = splitByDelimiter(refsStr, ',');
971
+ for (const part of parts) {
972
+ let ref = part.trim();
973
+ // Handle 'HEAD -> branch' format
974
+ const arrowIndex = findSubstring(ref, ' -> ');
975
+ if (arrowIndex !== -1) {
976
+ refs.push('HEAD');
977
+ ref = ref.slice(arrowIndex + 4);
978
+ }
979
+ // Handle 'tag: tagname' format
980
+ if (startsWithPrefix$2(ref, 'tag: ')) {
981
+ ref = ref.slice(5);
982
+ }
983
+ if (ref) {
984
+ refs.push(ref);
985
+ }
986
+ }
987
+ return refs;
988
+ }
989
+ /**
990
+ * Splits string by delimiter (no regex).
991
+ *
992
+ * @param str - String to split
993
+ * @param delimiter - Delimiter to split by
994
+ * @returns Array of parts
995
+ */
996
+ function splitByDelimiter(str, delimiter) {
997
+ const parts = [];
998
+ let current = '';
999
+ let i = 0;
1000
+ while (i < str.length) {
1001
+ if (matchesAt(str, i, delimiter)) {
1002
+ parts.push(current);
1003
+ current = '';
1004
+ i += delimiter.length;
1005
+ }
1006
+ else {
1007
+ current += str[i];
1008
+ i++;
1009
+ }
1010
+ }
1011
+ parts.push(current);
1012
+ return parts;
1013
+ }
1014
+ /**
1015
+ * Checks if string matches at position.
1016
+ *
1017
+ * @param str - String to check
1018
+ * @param pos - Position to check at
1019
+ * @param pattern - Pattern to match
1020
+ * @returns True if matches
1021
+ */
1022
+ function matchesAt(str, pos, pattern) {
1023
+ if (pos + pattern.length > str.length)
1024
+ return false;
1025
+ for (let i = 0; i < pattern.length; i++) {
1026
+ if (str[pos + i] !== pattern[i])
1027
+ return false;
1028
+ }
1029
+ return true;
1030
+ }
1031
+ /**
1032
+ * Finds substring position (no regex).
1033
+ *
1034
+ * @param str - String to search
1035
+ * @param pattern - Pattern to find
1036
+ * @returns Position or -1 if not found
1037
+ */
1038
+ function findSubstring(str, pattern) {
1039
+ for (let i = 0; i <= str.length - pattern.length; i++) {
1040
+ if (matchesAt(str, i, pattern)) {
1041
+ return i;
1042
+ }
1043
+ }
1044
+ return -1;
1045
+ }
1046
+ /**
1047
+ * Checks if string starts with prefix (no regex).
1048
+ *
1049
+ * @param str - String to check
1050
+ * @param prefix - Prefix to check for
1051
+ * @returns True if starts with prefix
1052
+ */
1053
+ function startsWithPrefix$2(str, prefix) {
1054
+ return matchesAt(str, 0, prefix);
1055
+ }
1056
+ // ============================================================================
1057
+ // Security helpers - character-by-character validation (no regex)
1058
+ // ============================================================================
1059
+ /**
1060
+ * Maximum allowed git reference length.
1061
+ */
1062
+ const MAX_REF_LENGTH = 256;
1063
+ /**
1064
+ * Escapes a git reference for safe use in shell commands.
1065
+ *
1066
+ * @param ref - Reference to escape
1067
+ * @returns Safe reference string
1068
+ * @throws {Error} If reference contains invalid characters
1069
+ */
1070
+ function escapeGitRef(ref) {
1071
+ if (!ref || typeof ref !== 'string') {
1072
+ throw createError('Git reference is required');
1073
+ }
1074
+ if (ref.length > MAX_REF_LENGTH) {
1075
+ throw createError(`Git reference exceeds maximum length of ${MAX_REF_LENGTH}`);
1076
+ }
1077
+ const safe = [];
1078
+ for (let i = 0; i < ref.length; i++) {
1079
+ const code = ref.charCodeAt(i);
1080
+ // Allow: a-z, A-Z, 0-9, /, -, _, ., ~, ^, @, {, }
1081
+ if ((code >= 97 && code <= 122) || // a-z
1082
+ (code >= 65 && code <= 90) || // A-Z
1083
+ (code >= 48 && code <= 57) || // 0-9
1084
+ code === 47 || // /
1085
+ code === 45 || // -
1086
+ code === 95 || // _
1087
+ code === 46 || // .
1088
+ code === 126 || // ~
1089
+ code === 94 || // ^
1090
+ code === 64 || // @
1091
+ code === 123 || // {
1092
+ code === 125 // }
1093
+ ) {
1094
+ safe.push(ref[i]);
1095
+ }
1096
+ else {
1097
+ throw createError(`Invalid character in git reference at position ${i}: "${ref[i]}"`);
1098
+ }
1099
+ }
1100
+ return safe.join('');
1101
+ }
1102
+ /**
1103
+ * Maximum allowed git path length.
1104
+ */
1105
+ const MAX_PATH_LENGTH$1 = 4096;
1106
+ /**
1107
+ * Escapes a file path for safe use in git commands.
1108
+ *
1109
+ * @param path - Path to escape
1110
+ * @returns Safe path string
1111
+ * @throws {Error} If path contains invalid characters
1112
+ */
1113
+ function escapeGitPath(path) {
1114
+ if (!path || typeof path !== 'string') {
1115
+ throw createError('Path is required');
1116
+ }
1117
+ if (path.length > MAX_PATH_LENGTH$1) {
1118
+ throw createError(`Path exceeds maximum length of ${MAX_PATH_LENGTH$1}`);
1119
+ }
1120
+ const safe = [];
1121
+ for (let i = 0; i < path.length; i++) {
1122
+ const code = path.charCodeAt(i);
1123
+ // Allow: a-z, A-Z, 0-9, /, \, -, _, ., space
1124
+ if ((code >= 97 && code <= 122) || // a-z
1125
+ (code >= 65 && code <= 90) || // A-Z
1126
+ (code >= 48 && code <= 57) || // 0-9
1127
+ code === 47 || // /
1128
+ code === 92 || // \
1129
+ code === 45 || // -
1130
+ code === 95 || // _
1131
+ code === 46 || // .
1132
+ code === 32 // space
1133
+ ) {
1134
+ safe.push(path[i]);
1135
+ }
1136
+ else {
1137
+ throw createError(`Invalid character in path at position ${i}: "${path[i]}"`);
1138
+ }
1139
+ }
1140
+ return safe.join('');
1141
+ }
1142
+ /**
1143
+ * Maximum allowed argument length.
1144
+ */
1145
+ const MAX_ARG_LENGTH = 1000;
1146
+ /**
1147
+ * Escapes a general git argument for safe use in shell commands.
1148
+ *
1149
+ * @param arg - Argument to escape
1150
+ * @returns Safe argument string
1151
+ * @throws {Error} If argument contains invalid characters
1152
+ */
1153
+ function escapeGitArg(arg) {
1154
+ if (!arg || typeof arg !== 'string') {
1155
+ throw createError('Argument is required');
1156
+ }
1157
+ if (arg.length > MAX_ARG_LENGTH) {
1158
+ throw createError(`Argument exceeds maximum length of ${MAX_ARG_LENGTH}`);
1159
+ }
1160
+ const safe = [];
1161
+ for (let i = 0; i < arg.length; i++) {
1162
+ const code = arg.charCodeAt(i);
1163
+ // Allow: a-z, A-Z, 0-9, space, @, ., -, _, <, >, +
1164
+ if ((code >= 97 && code <= 122) || // a-z
1165
+ (code >= 65 && code <= 90) || // A-Z
1166
+ (code >= 48 && code <= 57) || // 0-9
1167
+ code === 32 || // space
1168
+ code === 64 || // @
1169
+ code === 46 || // .
1170
+ code === 45 || // -
1171
+ code === 95 || // _
1172
+ code === 60 || // <
1173
+ code === 62 || // >
1174
+ code === 43 // +
1175
+ ) {
1176
+ safe.push(arg[i]);
1177
+ }
1178
+ else {
1179
+ throw createError(`Invalid character in argument at position ${i}: "${arg[i]}"`);
1180
+ }
1181
+ }
1182
+ return safe.join('');
1183
+ }
1184
+
1185
+ /**
1186
+ * Safe copies of Date built-in via factory function and static methods.
1187
+ *
1188
+ * Since constructors cannot be safely captured via Object.assign, this module
1189
+ * provides a factory function that uses Reflect.construct internally.
1190
+ *
1191
+ * These references are captured at module initialization time to protect against
1192
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1193
+ *
1194
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/date
1195
+ */
1196
+ // Capture references at module initialization time
1197
+ const _Date = globalThis.Date;
1198
+ const _Reflect = globalThis.Reflect;
1199
+ function createDate(...args) {
1200
+ return _Reflect.construct(_Date, args);
1201
+ }
1202
+
1203
+ /**
1204
+ * Safe copies of Number built-in methods and constants.
1205
+ *
1206
+ * These references are captured at module initialization time to protect against
1207
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1208
+ *
1209
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/number
1210
+ */
1211
+ // Capture references at module initialization time
1212
+ const _parseInt = globalThis.parseInt;
1213
+ const _isNaN = globalThis.isNaN;
1214
+ // ============================================================================
1215
+ // Parsing
1216
+ // ============================================================================
1217
+ /**
1218
+ * (Safe copy) Parses a string and returns an integer.
1219
+ */
1220
+ const parseInt = _parseInt;
1221
+ // ============================================================================
1222
+ // Global Type Checking (legacy, less strict)
1223
+ // ============================================================================
1224
+ /**
1225
+ * (Safe copy) Global isNaN function (coerces to number first, less strict than Number.isNaN).
1226
+ */
1227
+ const globalIsNaN = _isNaN;
1228
+
1229
+ /**
1230
+ * Default tag options.
1231
+ */
1232
+ const DEFAULT_TAG_OPTIONS = {
1233
+ timeout: 10000,
1234
+ };
1235
+ /**
1236
+ * Gets all tags from the repository.
1237
+ *
1238
+ * @param options - Tag listing options
1239
+ * @returns Array of GitTag objects
1240
+ *
1241
+ * @example
1242
+ * const tags = getTags()
1243
+ * const versionTags = getTags({ pattern: 'v' })
1244
+ */
1245
+ function getTags(options = {}) {
1246
+ const opts = { ...DEFAULT_TAG_OPTIONS, ...options };
1247
+ const args = ['tag', '-l', '--sort=-creatordate'];
1248
+ if (opts.pattern) {
1249
+ const safePattern = escapeGitTagPattern(opts.pattern);
1250
+ args.push(safePattern + '*');
1251
+ }
1252
+ try {
1253
+ const output = execSync(`git ${args.join(' ')}`, {
1254
+ encoding: 'utf-8',
1255
+ cwd: opts.cwd,
1256
+ timeout: opts.timeout,
1257
+ stdio: ['pipe', 'pipe', 'pipe'],
1258
+ });
1259
+ const tagNames = output
1260
+ .split('\n')
1261
+ .map((line) => line.trim())
1262
+ .filter((line) => line.length > 0);
1263
+ // Limit results if requested
1264
+ const limitedNames = opts.maxCount ? tagNames.slice(0, opts.maxCount) : tagNames;
1265
+ // Get details for each tag
1266
+ const tags = [];
1267
+ for (const name of limitedNames) {
1268
+ const tag = getTagDetails(name, opts);
1269
+ if (tag) {
1270
+ tags.push(tag);
1271
+ }
1272
+ }
1273
+ return tags;
1274
+ }
1275
+ catch {
1276
+ return [];
1277
+ }
1278
+ }
1279
+ /**
1280
+ * Gets detailed information about a specific tag.
1281
+ *
1282
+ * @param name - The tag name to look up
1283
+ * @param options - Configuration for the tag operation
1284
+ * @returns GitTag or null if not found
1285
+ *
1286
+ * @example
1287
+ * const tag = getTag('v1.0.0')
1288
+ */
1289
+ function getTag(name, options = {}) {
1290
+ return getTagDetails(name, { ...DEFAULT_TAG_OPTIONS, ...options });
1291
+ }
1292
+ /**
1293
+ * Gets tag details including type and commit hash.
1294
+ *
1295
+ * @param name - The tag name to retrieve details for
1296
+ * @param options - Configuration for the tag operation
1297
+ * @returns GitTag or null
1298
+ */
1299
+ function getTagDetails(name, options) {
1300
+ const safeName = escapeGitRef(name);
1301
+ try {
1302
+ // Get the commit hash the tag points to
1303
+ const commitHash = execSync(`git rev-list -1 ${safeName}`, {
1304
+ encoding: 'utf-8',
1305
+ cwd: options.cwd,
1306
+ timeout: options.timeout,
1307
+ stdio: ['pipe', 'pipe', 'pipe'],
1308
+ }).trim();
1309
+ // Check if it's an annotated tag by trying to get tag message
1310
+ try {
1311
+ const tagInfo = execSync(`git cat-file tag ${safeName}`, {
1312
+ encoding: 'utf-8',
1313
+ cwd: options.cwd,
1314
+ timeout: options.timeout,
1315
+ stdio: ['pipe', 'pipe', 'pipe'],
1316
+ });
1317
+ // Parse annotated tag info
1318
+ const parsed = parseAnnotatedTagInfo(tagInfo);
1319
+ return createAnnotatedTag({
1320
+ name,
1321
+ commitHash,
1322
+ message: parsed.message,
1323
+ taggerName: parsed.taggerName,
1324
+ taggerEmail: parsed.taggerEmail,
1325
+ tagDate: parsed.tagDate,
1326
+ });
1327
+ }
1328
+ catch {
1329
+ // Not an annotated tag, it's lightweight
1330
+ return createLightweightTag({
1331
+ name,
1332
+ commitHash,
1333
+ });
1334
+ }
1335
+ }
1336
+ catch {
1337
+ return null;
1338
+ }
1339
+ }
1340
+ /**
1341
+ * Parses annotated tag info from git cat-file output.
1342
+ *
1343
+ * @param info - Raw tag info
1344
+ * @returns Parsed info
1345
+ */
1346
+ function parseAnnotatedTagInfo(info) {
1347
+ const lines = info.split('\n');
1348
+ let taggerName = '';
1349
+ let taggerEmail = '';
1350
+ let tagDate = '';
1351
+ let messageStart = -1;
1352
+ for (let i = 0; i < lines.length; i++) {
1353
+ const line = lines[i];
1354
+ if (startsWithPrefix$1(line, 'tagger ')) {
1355
+ const taggerLine = line.slice(7);
1356
+ const parsed = parseTaggerLine(taggerLine);
1357
+ taggerName = parsed.name;
1358
+ taggerEmail = parsed.email;
1359
+ tagDate = parsed.date;
1360
+ }
1361
+ // Empty line marks start of message
1362
+ if (line === '' && messageStart === -1) {
1363
+ messageStart = i + 1;
1364
+ break;
1365
+ }
1366
+ }
1367
+ const message = messageStart >= 0 ? lines.slice(messageStart).join('\n').trim() : '';
1368
+ return {
1369
+ message,
1370
+ taggerName,
1371
+ taggerEmail,
1372
+ tagDate,
1373
+ };
1374
+ }
1375
+ /**
1376
+ * Parses tagger line from annotated tag.
1377
+ * Format: Name <email> timestamp timezone
1378
+ *
1379
+ * @param line - Raw tagger line from git output
1380
+ * @returns Parsed tagger info with name, email, and date
1381
+ */
1382
+ function parseTaggerLine(line) {
1383
+ let name = '';
1384
+ let email = '';
1385
+ let date = '';
1386
+ // Find email in angle brackets
1387
+ let emailStart = -1;
1388
+ let emailEnd = -1;
1389
+ for (let i = 0; i < line.length; i++) {
1390
+ if (line[i] === '<') {
1391
+ emailStart = i + 1;
1392
+ }
1393
+ else if (line[i] === '>' && emailStart !== -1) {
1394
+ emailEnd = i;
1395
+ break;
1396
+ }
1397
+ }
1398
+ if (emailStart !== -1 && emailEnd !== -1) {
1399
+ name = line.slice(0, emailStart - 1).trim();
1400
+ email = line.slice(emailStart, emailEnd);
1401
+ // Rest is timestamp and timezone
1402
+ const rest = line.slice(emailEnd + 1).trim();
1403
+ const parts = rest.split(' ');
1404
+ if (parts.length >= 1) {
1405
+ // Convert Unix timestamp to ISO 8601
1406
+ const timestamp = parseInt(parts[0], 10);
1407
+ if (!globalIsNaN(timestamp)) {
1408
+ date = createDate(timestamp * 1000).toISOString();
1409
+ }
1410
+ }
1411
+ }
1412
+ return { name, email, date };
1413
+ }
1414
+ /**
1415
+ * Checks if a tag exists.
1416
+ *
1417
+ * @param name - The tag name to verify
1418
+ * @param options - Configuration for the tag operation
1419
+ * @returns True if tag exists
1420
+ *
1421
+ * @example
1422
+ * if (tagExists('v1.0.0')) { ... }
1423
+ */
1424
+ function tagExists(name, options = {}) {
1425
+ const opts = { ...DEFAULT_TAG_OPTIONS, ...options };
1426
+ const safeName = escapeGitRef(name);
1427
+ try {
1428
+ execSync(`git rev-parse ${safeName}`, {
1429
+ encoding: 'utf-8',
1430
+ cwd: opts.cwd,
1431
+ timeout: opts.timeout,
1432
+ stdio: ['pipe', 'pipe', 'pipe'],
1433
+ });
1434
+ return true;
1435
+ }
1436
+ catch {
1437
+ return false;
1438
+ }
1439
+ }
1440
+ /**
1441
+ * Gets the latest tag (by creation date).
1442
+ *
1443
+ * @param options - Tag options with optional pattern
1444
+ * @returns Latest GitTag or null
1445
+ *
1446
+ * @example
1447
+ * const latest = getLatestTag()
1448
+ * const latestVersion = getLatestTag({ pattern: 'v' })
1449
+ */
1450
+ function getLatestTag(options = {}) {
1451
+ const tags = getTags({ ...options, maxCount: 1 });
1452
+ return tags[0] ?? null;
1453
+ }
1454
+ /**
1455
+ * Gets tags that match a package name.
1456
+ *
1457
+ * @param packageName - Package name to match
1458
+ * @param options - Tag options
1459
+ * @returns Array of matching tags
1460
+ *
1461
+ * @example
1462
+ * const tags = getTagsForPackage('@scope/pkg')
1463
+ */
1464
+ function getTagsForPackage(packageName, options = {}) {
1465
+ // Common patterns: @scope/pkg@version, pkg@version, pkg-vversion
1466
+ const allTags = getTags(options);
1467
+ return allTags.filter((tag) => {
1468
+ // Check if tag starts with package name followed by @ or -v
1469
+ const name = tag.name;
1470
+ // Pattern: package@version
1471
+ if (startsWithPrefix$1(name, packageName + '@')) {
1472
+ return true;
1473
+ }
1474
+ // Pattern: package-v
1475
+ if (startsWithPrefix$1(name, packageName + '-v')) {
1476
+ return true;
1477
+ }
1478
+ return false;
1479
+ });
1480
+ }
1481
+ // ============================================================================
1482
+ // Helper functions
1483
+ // ============================================================================
1484
+ /**
1485
+ * Checks if string starts with prefix (no regex).
1486
+ *
1487
+ * @param str - The string to check
1488
+ * @param prefix - The prefix to look for
1489
+ * @returns True if str starts with the given prefix
1490
+ */
1491
+ function startsWithPrefix$1(str, prefix) {
1492
+ if (prefix.length > str.length)
1493
+ return false;
1494
+ for (let i = 0; i < prefix.length; i++) {
1495
+ if (str[i] !== prefix[i])
1496
+ return false;
1497
+ }
1498
+ return true;
1499
+ }
1500
+ /**
1501
+ * Maximum tag pattern length.
1502
+ */
1503
+ const MAX_PATTERN_LENGTH = 256;
1504
+ /**
1505
+ * Escapes a tag pattern for safe use in git commands.
1506
+ *
1507
+ * @param pattern - Pattern to escape
1508
+ * @returns Safe pattern string
1509
+ */
1510
+ function escapeGitTagPattern(pattern) {
1511
+ if (!pattern || typeof pattern !== 'string') {
1512
+ throw createError('Pattern is required');
1513
+ }
1514
+ if (pattern.length > MAX_PATTERN_LENGTH) {
1515
+ throw createError(`Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH}`);
1516
+ }
1517
+ const safe = [];
1518
+ for (let i = 0; i < pattern.length; i++) {
1519
+ const code = pattern.charCodeAt(i);
1520
+ // Allow: a-z, A-Z, 0-9, /, -, _, ., @
1521
+ if ((code >= 97 && code <= 122) || // a-z
1522
+ (code >= 65 && code <= 90) || // A-Z
1523
+ (code >= 48 && code <= 57) || // 0-9
1524
+ code === 47 || // /
1525
+ code === 45 || // -
1526
+ code === 95 || // _
1527
+ code === 46 || // .
1528
+ code === 64 // @
1529
+ ) {
1530
+ safe.push(pattern[i]);
1531
+ }
1532
+ else {
1533
+ throw createError(`Invalid character in pattern at position ${i}: "${pattern[i]}"`);
1534
+ }
1535
+ }
1536
+ return safe.join('');
1537
+ }
1538
+
1539
+ /**
1540
+ * Creates a new tag.
1541
+ *
1542
+ * @param name - The name for the new tag
1543
+ * @param options - Configuration including optional message for annotated tags
1544
+ * @returns Created GitTag
1545
+ *
1546
+ * @example
1547
+ * // Create lightweight tag
1548
+ * const tag = createTag('v1.0.0')
1549
+ *
1550
+ * // Create annotated tag
1551
+ * const tag = createTag('v1.0.0', { message: 'Release v1.0.0' })
1552
+ */
1553
+ function createTag(name, options = {}) {
1554
+ const opts = { ...DEFAULT_TAG_OPTIONS, ...options };
1555
+ const safeName = escapeGitRef(name);
1556
+ const args = ['tag'];
1557
+ if (opts.force) {
1558
+ args.push('-f');
1559
+ }
1560
+ if (opts.message) {
1561
+ // Annotated tag
1562
+ args.push('-a');
1563
+ args.push(safeName);
1564
+ args.push('-m');
1565
+ args.push(`"${escapeGitMessage(opts.message)}"`);
1566
+ }
1567
+ else {
1568
+ // Lightweight tag
1569
+ args.push(safeName);
1570
+ }
1571
+ if (opts.target) {
1572
+ args.push(escapeGitRef(opts.target));
1573
+ }
1574
+ try {
1575
+ execSync(`git ${args.join(' ')}`, {
1576
+ encoding: 'utf-8',
1577
+ cwd: opts.cwd,
1578
+ timeout: opts.timeout,
1579
+ stdio: ['pipe', 'pipe', 'pipe'],
1580
+ });
1581
+ // Get the created tag
1582
+ const tag = getTag(name, opts);
1583
+ if (!tag) {
1584
+ throw createError(`Failed to retrieve created tag: ${name}`);
1585
+ }
1586
+ return tag;
1587
+ }
1588
+ catch (error) {
1589
+ if (error instanceof Error) {
1590
+ throw createError(`Failed to create tag ${name}: ${error.message}`);
1591
+ }
1592
+ throw error;
1593
+ }
1594
+ }
1595
+ /**
1596
+ * Deletes a tag.
1597
+ *
1598
+ * @param name - The tag name to delete
1599
+ * @param options - Configuration for the tag operation
1600
+ * @returns True if deleted
1601
+ *
1602
+ * @example
1603
+ * const deleted = deleteTag('v1.0.0')
1604
+ */
1605
+ function deleteTag(name, options = {}) {
1606
+ const opts = { ...DEFAULT_TAG_OPTIONS, ...options };
1607
+ const safeName = escapeGitRef(name);
1608
+ try {
1609
+ execSync(`git tag -d ${safeName}`, {
1610
+ encoding: 'utf-8',
1611
+ cwd: opts.cwd,
1612
+ timeout: opts.timeout,
1613
+ stdio: ['pipe', 'pipe', 'pipe'],
1614
+ });
1615
+ return true;
1616
+ }
1617
+ catch {
1618
+ return false;
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Pushes a tag to a remote.
1623
+ *
1624
+ * @param name - The tag to push to the remote
1625
+ * @param remote - Remote name (defaults to 'origin')
1626
+ * @param options - Configuration for the tag operation
1627
+ * @returns True if pushed successfully
1628
+ *
1629
+ * @example
1630
+ * pushTag('v1.0.0')
1631
+ * pushTag('v1.0.0', 'upstream')
1632
+ */
1633
+ function pushTag(name, remote = 'origin', options = {}) {
1634
+ const opts = { ...DEFAULT_TAG_OPTIONS, ...options };
1635
+ const safeName = escapeGitRef(name);
1636
+ const safeRemote = escapeGitRef(remote);
1637
+ try {
1638
+ execSync(`git push ${safeRemote} ${safeName}`, {
1639
+ encoding: 'utf-8',
1640
+ cwd: opts.cwd,
1641
+ timeout: opts.timeout * 3, // Allow more time for network
1642
+ stdio: ['pipe', 'pipe', 'pipe'],
1643
+ });
1644
+ return true;
1645
+ }
1646
+ catch {
1647
+ return false;
1648
+ }
1649
+ }
1650
+ // ============================================================================
1651
+ // Security helpers
1652
+ // ============================================================================
1653
+ /**
1654
+ * Maximum message length.
1655
+ */
1656
+ const MAX_MESSAGE_LENGTH = 10000;
1657
+ /**
1658
+ * Escapes a message for safe use in git commands.
1659
+ *
1660
+ * @param message - Message to escape
1661
+ * @returns Safe message string
1662
+ */
1663
+ function escapeGitMessage(message) {
1664
+ if (!message || typeof message !== 'string') {
1665
+ throw createError('Message is required');
1666
+ }
1667
+ if (message.length > MAX_MESSAGE_LENGTH) {
1668
+ throw createError(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH}`);
1669
+ }
1670
+ const safe = [];
1671
+ for (let i = 0; i < message.length; i++) {
1672
+ const char = message[i];
1673
+ const code = message.charCodeAt(i);
1674
+ // Escape double quotes and backslashes
1675
+ if (char === '"' || char === '\\') {
1676
+ safe.push('\\');
1677
+ safe.push(char);
1678
+ }
1679
+ // Allow printable ASCII and common whitespace
1680
+ else if ((code >= 32 && code <= 126) || // Printable ASCII
1681
+ code === 10 || // newline
1682
+ code === 13 || // carriage return
1683
+ code === 9 // tab
1684
+ ) {
1685
+ safe.push(char);
1686
+ }
1687
+ // Skip other control characters
1688
+ }
1689
+ return safe.join('');
1690
+ }
1691
+
1692
+ /**
1693
+ * Default commit options.
1694
+ */
1695
+ const DEFAULT_COMMIT_OPTIONS = {
1696
+ timeout: 30000,
1697
+ };
1698
+ /**
1699
+ * Creates a new commit.
1700
+ *
1701
+ * @param message - Commit message (subject line)
1702
+ * @param options - Create options
1703
+ * @returns Created GitCommit
1704
+ *
1705
+ * @example
1706
+ * const commit = createCommit('feat: add new feature')
1707
+ * const commit = createCommit('fix: resolve bug', { body: 'Detailed description' })
1708
+ */
1709
+ function commit(message, options = {}) {
1710
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
1711
+ // noEdit is only valid when amending - reuse existing commit message
1712
+ const isNoEditAmend = opts.amend && opts.noEdit;
1713
+ if (!isNoEditAmend && (!message || typeof message !== 'string')) {
1714
+ throw createError('Commit message is required');
1715
+ }
1716
+ const args = ['commit'];
1717
+ if (isNoEditAmend) {
1718
+ // Amend without changing the message
1719
+ args.push('--amend', '--no-edit');
1720
+ }
1721
+ else {
1722
+ const safeMessage = escapeGitMessage(message);
1723
+ // Build message with optional body
1724
+ let fullMessage = safeMessage;
1725
+ if (opts.body) {
1726
+ const safeBody = escapeGitMessage(opts.body);
1727
+ fullMessage = `${safeMessage}\n\n${safeBody}`;
1728
+ }
1729
+ args.push('-m', `"${fullMessage}"`);
1730
+ if (opts.amend) {
1731
+ args.push('--amend');
1732
+ }
1733
+ }
1734
+ if (opts.allowEmpty) {
1735
+ args.push('--allow-empty');
1736
+ }
1737
+ if (opts.sign) {
1738
+ args.push('-S');
1739
+ }
1740
+ if (opts.noVerify) {
1741
+ args.push('--no-verify');
1742
+ }
1743
+ if (opts.author) {
1744
+ const safeAuthor = escapeAuthor(opts.author);
1745
+ args.push(`--author="${safeAuthor}"`);
1746
+ }
1747
+ // Add specific files if provided
1748
+ if (opts.files && opts.files.length > 0) {
1749
+ args.push('--');
1750
+ for (const file of opts.files) {
1751
+ args.push(escapeFilePath(file));
1752
+ }
1753
+ }
1754
+ try {
1755
+ execSync(`git ${args.join(' ')}`, {
1756
+ encoding: 'utf-8',
1757
+ cwd: opts.cwd,
1758
+ timeout: opts.timeout,
1759
+ stdio: ['pipe', 'pipe', 'pipe'],
1760
+ });
1761
+ // Get the created commit
1762
+ const commit = getCommit('HEAD', opts);
1763
+ if (!commit) {
1764
+ throw createError('Failed to retrieve created commit');
1765
+ }
1766
+ return commit;
1767
+ }
1768
+ catch (error) {
1769
+ if (error instanceof Error) {
1770
+ throw createError(`Failed to create commit: ${error.message}`);
1771
+ }
1772
+ throw error;
1773
+ }
1774
+ }
1775
+ /**
1776
+ * Amends the last commit with new message.
1777
+ *
1778
+ * @param message - The new commit message to use
1779
+ * @param options - Configuration for the commit operation
1780
+ * @returns GitCommit object representing the amended commit
1781
+ *
1782
+ * @example
1783
+ * const commit = amendCommit('feat: improved feature')
1784
+ */
1785
+ function amendCommit(message, options = {}) {
1786
+ return commit(message, { ...options, amend: true });
1787
+ }
1788
+ /**
1789
+ * Amends the last commit without changing the message.
1790
+ * Useful for adding staged changes to the previous commit.
1791
+ *
1792
+ * @param options - Configuration for the commit operation
1793
+ * @returns GitCommit object representing the amended commit
1794
+ *
1795
+ * @example
1796
+ * stage(['extra-file.ts'])
1797
+ * amendCommitNoEdit() // adds staged files to last commit
1798
+ */
1799
+ function amendCommitNoEdit(options = {}) {
1800
+ return commit('', { ...options, amend: true, noEdit: true });
1801
+ }
1802
+ /**
1803
+ * Creates an empty commit (useful for CI triggers).
1804
+ *
1805
+ * @param message - Text for the empty commit
1806
+ * @param options - Configuration for the commit operation
1807
+ * @returns GitCommit object representing the new empty commit
1808
+ *
1809
+ * @example
1810
+ * const commit = createEmptyCommit('chore: trigger CI')
1811
+ */
1812
+ function createEmptyCommit(message, options = {}) {
1813
+ return commit(message, { ...options, allowEmpty: true });
1814
+ }
1815
+ // ============================================================================
1816
+ // Security helpers - character-by-character validation (no regex)
1817
+ // ============================================================================
1818
+ /**
1819
+ * Maximum file path length.
1820
+ */
1821
+ const MAX_PATH_LENGTH = 4096;
1822
+ /**
1823
+ * Escapes a file path for safe use in git commands.
1824
+ *
1825
+ * @param path - Path to escape
1826
+ * @returns Safe path string
1827
+ */
1828
+ function escapeFilePath(path) {
1829
+ if (!path || typeof path !== 'string') {
1830
+ throw createError('File path is required');
1831
+ }
1832
+ if (path.length > MAX_PATH_LENGTH) {
1833
+ throw createError(`Path exceeds maximum length of ${MAX_PATH_LENGTH}`);
1834
+ }
1835
+ const safe = [];
1836
+ for (let i = 0; i < path.length; i++) {
1837
+ const code = path.charCodeAt(i);
1838
+ // Allow: a-z, A-Z, 0-9, /, \, -, _, ., space
1839
+ if ((code >= 97 && code <= 122) || // a-z
1840
+ (code >= 65 && code <= 90) || // A-Z
1841
+ (code >= 48 && code <= 57) || // 0-9
1842
+ code === 47 || // /
1843
+ code === 92 || // \
1844
+ code === 45 || // -
1845
+ code === 95 || // _
1846
+ code === 46 || // .
1847
+ code === 32 // space
1848
+ ) {
1849
+ safe.push(path[i]);
1850
+ }
1851
+ else {
1852
+ throw createError(`Invalid character in path at position ${i}: "${path[i]}"`);
1853
+ }
1854
+ }
1855
+ return safe.join('');
1856
+ }
1857
+ /**
1858
+ * Maximum author length.
1859
+ */
1860
+ const MAX_AUTHOR_LENGTH = 500;
1861
+ /**
1862
+ * Escapes an author string for safe use in git commands.
1863
+ * Format: "Name <email>"
1864
+ *
1865
+ * @param author - Author to escape
1866
+ * @returns Safe author string
1867
+ */
1868
+ function escapeAuthor(author) {
1869
+ if (!author || typeof author !== 'string') {
1870
+ throw createError('Author is required');
1871
+ }
1872
+ if (author.length > MAX_AUTHOR_LENGTH) {
1873
+ throw createError(`Author exceeds maximum length of ${MAX_AUTHOR_LENGTH}`);
1874
+ }
1875
+ const safe = [];
1876
+ for (let i = 0; i < author.length; i++) {
1877
+ const code = author.charCodeAt(i);
1878
+ // Allow: a-z, A-Z, 0-9, space, @, ., -, _, <, >
1879
+ if ((code >= 97 && code <= 122) || // a-z
1880
+ (code >= 65 && code <= 90) || // A-Z
1881
+ (code >= 48 && code <= 57) || // 0-9
1882
+ code === 32 || // space
1883
+ code === 64 || // @
1884
+ code === 46 || // .
1885
+ code === 45 || // -
1886
+ code === 95 || // _
1887
+ code === 60 || // <
1888
+ code === 62 // >
1889
+ ) {
1890
+ safe.push(author[i]);
1891
+ }
1892
+ else {
1893
+ throw createError(`Invalid character in author at position ${i}: "${author[i]}"`);
1894
+ }
1895
+ }
1896
+ return safe.join('');
1897
+ }
1898
+
1899
+ /**
1900
+ * Stages files for commit.
1901
+ *
1902
+ * @param files - Array of file paths relative to working directory
1903
+ * @param options - Configuration for the staging operation
1904
+ * @returns True if staging succeeded
1905
+ *
1906
+ * @example
1907
+ * stage(['package.json', 'CHANGELOG.md'])
1908
+ * stage(['.'], { all: true })
1909
+ */
1910
+ function stage(files, options = {}) {
1911
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
1912
+ const args = ['add'];
1913
+ if (opts.all) {
1914
+ args.push('-A');
1915
+ }
1916
+ else if (opts.update) {
1917
+ args.push('-u');
1918
+ }
1919
+ if (opts.force) {
1920
+ args.push('-f');
1921
+ }
1922
+ // Add files
1923
+ for (const file of files) {
1924
+ args.push(escapeFilePath(file));
1925
+ }
1926
+ try {
1927
+ execSync(`git ${args.join(' ')}`, {
1928
+ encoding: 'utf-8',
1929
+ cwd: opts.cwd,
1930
+ timeout: opts.timeout,
1931
+ stdio: ['pipe', 'pipe', 'pipe'],
1932
+ });
1933
+ return true;
1934
+ }
1935
+ catch {
1936
+ return false;
1937
+ }
1938
+ }
1939
+ /**
1940
+ * Unstages files.
1941
+ *
1942
+ * @param files - Array of file paths to remove from staging area
1943
+ * @param options - Configuration for the unstage operation
1944
+ * @returns True if unstaging succeeded
1945
+ *
1946
+ * @example
1947
+ * unstage(['package.json'])
1948
+ */
1949
+ function unstage(files, options = {}) {
1950
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
1951
+ const args = ['reset', 'HEAD', '--'];
1952
+ for (const file of files) {
1953
+ args.push(escapeFilePath(file));
1954
+ }
1955
+ try {
1956
+ execSync(`git ${args.join(' ')}`, {
1957
+ encoding: 'utf-8',
1958
+ cwd: opts.cwd,
1959
+ timeout: opts.timeout,
1960
+ stdio: ['pipe', 'pipe', 'pipe'],
1961
+ });
1962
+ return true;
1963
+ }
1964
+ catch {
1965
+ return false;
1966
+ }
1967
+ }
1968
+ /**
1969
+ * Stages all changes (tracked and untracked).
1970
+ *
1971
+ * @param options - Configuration for the staging operation
1972
+ * @returns True if all changes were successfully added to the index
1973
+ *
1974
+ * @example
1975
+ * stageAll() // stages all tracked and untracked changes
1976
+ */
1977
+ function stageAll(options = {}) {
1978
+ return stage(['.'], { ...options, all: true });
1979
+ }
1980
+ /**
1981
+ * Checks if there are staged changes.
1982
+ *
1983
+ * @param options - Configuration for the operation
1984
+ * @returns True if there are staged changes ready to commit
1985
+ *
1986
+ * @example
1987
+ * if (hasStagedChanges()) { createCommit('...') }
1988
+ */
1989
+ function hasStagedChanges(options = {}) {
1990
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
1991
+ try {
1992
+ execSync('git diff --cached --quiet', {
1993
+ encoding: 'utf-8',
1994
+ cwd: opts.cwd,
1995
+ timeout: opts.timeout,
1996
+ stdio: ['pipe', 'pipe', 'pipe'],
1997
+ });
1998
+ // Exit code 0 means no changes
1999
+ return false;
2000
+ }
2001
+ catch {
2002
+ // Exit code 1 means there are changes
2003
+ return true;
2004
+ }
2005
+ }
2006
+ /**
2007
+ * Checks if there are unstaged changes (working tree dirty).
2008
+ *
2009
+ * @param options - Configuration for the operation
2010
+ * @returns True if there are unstaged changes in the working tree
2011
+ *
2012
+ * @example
2013
+ * if (hasUnstagedChanges()) { stage(['.']) }
2014
+ */
2015
+ function hasUnstagedChanges(options = {}) {
2016
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
2017
+ try {
2018
+ execSync('git diff --quiet', {
2019
+ encoding: 'utf-8',
2020
+ cwd: opts.cwd,
2021
+ timeout: opts.timeout,
2022
+ stdio: ['pipe', 'pipe', 'pipe'],
2023
+ });
2024
+ // Exit code 0 means no changes
2025
+ return false;
2026
+ }
2027
+ catch {
2028
+ // Exit code 1 means there are changes
2029
+ return true;
2030
+ }
2031
+ }
2032
+
2033
+ /**
2034
+ * Gets the current HEAD commit hash.
2035
+ *
2036
+ * @param options - Git operation configuration
2037
+ * @returns HEAD commit hash or null
2038
+ *
2039
+ * @example
2040
+ * const head = getHead()
2041
+ */
2042
+ function getHead(options = {}) {
2043
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
2044
+ try {
2045
+ return execSync('git rev-parse HEAD', {
2046
+ encoding: 'utf-8',
2047
+ cwd: opts.cwd,
2048
+ timeout: opts.timeout,
2049
+ stdio: ['pipe', 'pipe', 'pipe'],
2050
+ }).trim();
2051
+ }
2052
+ catch {
2053
+ return null;
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Gets the current branch name.
2058
+ *
2059
+ * @param options - Configuration for the operation
2060
+ * @returns Branch name or null if detached
2061
+ *
2062
+ * @example
2063
+ * const branch = getCurrentBranch()
2064
+ */
2065
+ function getCurrentBranch(options = {}) {
2066
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
2067
+ try {
2068
+ const result = execSync('git symbolic-ref --short HEAD', {
2069
+ encoding: 'utf-8',
2070
+ cwd: opts.cwd,
2071
+ timeout: opts.timeout,
2072
+ stdio: ['pipe', 'pipe', 'pipe'],
2073
+ }).trim();
2074
+ return result || null;
2075
+ }
2076
+ catch {
2077
+ // Detached HEAD or not a git repo
2078
+ return null;
2079
+ }
2080
+ }
2081
+ /**
2082
+ * Checks if there are untracked files.
2083
+ *
2084
+ * @param options - Configuration for the operation
2085
+ * @returns True if there are untracked files in the working directory
2086
+ */
2087
+ function hasUntrackedFiles(options = {}) {
2088
+ const opts = { ...DEFAULT_COMMIT_OPTIONS, ...options };
2089
+ try {
2090
+ const result = execSync('git ls-files --others --exclude-standard', {
2091
+ encoding: 'utf-8',
2092
+ cwd: opts.cwd,
2093
+ timeout: opts.timeout,
2094
+ stdio: ['pipe', 'pipe', 'pipe'],
2095
+ }).trim();
2096
+ return result.length > 0;
2097
+ }
2098
+ catch {
2099
+ return false;
2100
+ }
2101
+ }
2102
+
2103
+ /**
2104
+ * Default status options.
2105
+ */
2106
+ const DEFAULT_STATUS_OPTIONS = {
2107
+ timeout: 10000,
2108
+ };
2109
+ /**
2110
+ * Gets the full repository status.
2111
+ *
2112
+ * @param options - Configuration for the status query
2113
+ * @returns Comprehensive repository status information
2114
+ *
2115
+ * @example
2116
+ * const status = getStatus()
2117
+ * if (!status.clean) {
2118
+ * console.log('Working tree has changes')
2119
+ * }
2120
+ */
2121
+ function getStatus(options = {}) {
2122
+ const opts = { ...DEFAULT_STATUS_OPTIONS, ...options };
2123
+ // Get porcelain status with branch info
2124
+ const output = execSync('git status --porcelain=v2 --branch', {
2125
+ encoding: 'utf-8',
2126
+ cwd: opts.cwd,
2127
+ timeout: opts.timeout,
2128
+ stdio: ['pipe', 'pipe', 'pipe'],
2129
+ });
2130
+ return parseStatus(output);
2131
+ }
2132
+ /**
2133
+ * Parses git status porcelain v2 output.
2134
+ *
2135
+ * @param output - Raw status output
2136
+ * @returns Parsed status
2137
+ */
2138
+ function parseStatus(output) {
2139
+ const lines = output.split('\n');
2140
+ let branch = null;
2141
+ let detached = false;
2142
+ let upstream;
2143
+ let ahead = 0;
2144
+ let behind = 0;
2145
+ const staged = [];
2146
+ const modified = [];
2147
+ const untracked = [];
2148
+ let hasConflicts = false;
2149
+ for (const line of lines) {
2150
+ if (!line)
2151
+ continue;
2152
+ // Branch headers
2153
+ if (startsWithPrefix(line, '# branch.head ')) {
2154
+ const branchName = line.slice(14);
2155
+ if (branchName === '(detached)') {
2156
+ detached = true;
2157
+ }
2158
+ else {
2159
+ branch = branchName;
2160
+ }
2161
+ }
2162
+ else if (startsWithPrefix(line, '# branch.upstream ')) {
2163
+ upstream = line.slice(18);
2164
+ }
2165
+ else if (startsWithPrefix(line, '# branch.ab ')) {
2166
+ const ab = parseAheadBehind(line.slice(12));
2167
+ ahead = ab.ahead;
2168
+ behind = ab.behind;
2169
+ }
2170
+ // Changed entries (ordinary changed)
2171
+ else if (line[0] === '1') {
2172
+ const entry = parseChangedEntry(line);
2173
+ if (entry) {
2174
+ if (entry.indexStatus) {
2175
+ staged.push(entry);
2176
+ }
2177
+ if (entry.workTreeStatus && entry.workTreeStatus !== 'untracked') {
2178
+ modified.push(entry);
2179
+ }
2180
+ }
2181
+ }
2182
+ // Renamed/copied entries
2183
+ else if (line[0] === '2') {
2184
+ const entry = parseRenamedEntry(line);
2185
+ if (entry) {
2186
+ if (entry.indexStatus) {
2187
+ staged.push(entry);
2188
+ }
2189
+ if (entry.workTreeStatus) {
2190
+ modified.push(entry);
2191
+ }
2192
+ }
2193
+ }
2194
+ // Unmerged entries
2195
+ else if (line[0] === 'u') {
2196
+ hasConflicts = true;
2197
+ const entry = parseUnmergedEntry(line);
2198
+ if (entry) {
2199
+ staged.push(entry);
2200
+ }
2201
+ }
2202
+ // Untracked entries
2203
+ else if (line[0] === '?') {
2204
+ const path = line.slice(2);
2205
+ untracked.push(path);
2206
+ }
2207
+ }
2208
+ const clean = staged.length === 0 && modified.length === 0 && untracked.length === 0 && !hasConflicts;
2209
+ return {
2210
+ branch,
2211
+ detached,
2212
+ upstream,
2213
+ ahead,
2214
+ behind,
2215
+ staged,
2216
+ modified,
2217
+ untracked,
2218
+ clean,
2219
+ hasConflicts,
2220
+ };
2221
+ }
2222
+ /**
2223
+ * Parses ahead/behind string.
2224
+ *
2225
+ * @param str - String like "+5 -2"
2226
+ * @returns Parsed values
2227
+ */
2228
+ function parseAheadBehind(str) {
2229
+ let ahead = 0;
2230
+ let behind = 0;
2231
+ const parts = str.split(' ');
2232
+ for (const part of parts) {
2233
+ if (part[0] === '+') {
2234
+ ahead = parseInt(part.slice(1), 10) || 0;
2235
+ }
2236
+ else if (part[0] === '-') {
2237
+ behind = parseInt(part.slice(1), 10) || 0;
2238
+ }
2239
+ }
2240
+ return { ahead, behind };
2241
+ }
2242
+ /**
2243
+ * Parses a changed entry line.
2244
+ *
2245
+ * @param line - Status line starting with '1'
2246
+ * @returns Parsed entry or null
2247
+ */
2248
+ function parseChangedEntry(line) {
2249
+ // Format: 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
2250
+ const parts = line.split(' ');
2251
+ if (parts.length < 9)
2252
+ return null;
2253
+ const xy = parts[1];
2254
+ const path = parts.slice(8).join(' ');
2255
+ const indexStatus = statusFromChar(xy[0]);
2256
+ const workTreeStatus = statusFromChar(xy[1]);
2257
+ return {
2258
+ path,
2259
+ indexStatus,
2260
+ workTreeStatus,
2261
+ };
2262
+ }
2263
+ /**
2264
+ * Parses a renamed entry line.
2265
+ *
2266
+ * @param line - Status line starting with '2'
2267
+ * @returns Parsed entry or null
2268
+ */
2269
+ function parseRenamedEntry(line) {
2270
+ // Format: 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><tab><origPath>
2271
+ const parts = line.split(' ');
2272
+ if (parts.length < 10)
2273
+ return null;
2274
+ const xy = parts[1];
2275
+ const pathPart = parts.slice(9).join(' ');
2276
+ // Split by tab
2277
+ const tabIndex = pathPart.indexOf('\t');
2278
+ const path = tabIndex >= 0 ? pathPart.slice(0, tabIndex) : pathPart;
2279
+ const origPath = tabIndex >= 0 ? pathPart.slice(tabIndex + 1) : undefined;
2280
+ const indexStatus = statusFromChar(xy[0]);
2281
+ const workTreeStatus = statusFromChar(xy[1]);
2282
+ return {
2283
+ path,
2284
+ indexStatus,
2285
+ workTreeStatus,
2286
+ origPath,
2287
+ };
2288
+ }
2289
+ /**
2290
+ * Parses an unmerged entry line.
2291
+ *
2292
+ * @param line - Status line starting with 'u'
2293
+ * @returns Parsed entry or null
2294
+ */
2295
+ function parseUnmergedEntry(line) {
2296
+ // Format: u <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
2297
+ const parts = line.split(' ');
2298
+ if (parts.length < 11)
2299
+ return null;
2300
+ const path = parts.slice(10).join(' ');
2301
+ return {
2302
+ path,
2303
+ indexStatus: 'unmerged',
2304
+ workTreeStatus: 'unmerged',
2305
+ };
2306
+ }
2307
+ /**
2308
+ * Converts status character to FileStatus.
2309
+ *
2310
+ * @param char - Status character
2311
+ * @returns FileStatus or null
2312
+ */
2313
+ function statusFromChar(char) {
2314
+ switch (char) {
2315
+ case 'M':
2316
+ return 'modified';
2317
+ case 'T':
2318
+ return 'modified'; // Type change
2319
+ case 'A':
2320
+ return 'added';
2321
+ case 'D':
2322
+ return 'deleted';
2323
+ case 'R':
2324
+ return 'renamed';
2325
+ case 'C':
2326
+ return 'copied';
2327
+ case 'U':
2328
+ return 'unmerged';
2329
+ case '?':
2330
+ return 'untracked';
2331
+ case '!':
2332
+ return 'ignored';
2333
+ case '.':
2334
+ return null;
2335
+ default:
2336
+ return null;
2337
+ }
2338
+ }
2339
+ /**
2340
+ * Checks if string starts with prefix (no regex).
2341
+ *
2342
+ * @param str - The string to check
2343
+ * @param prefix - The prefix to look for
2344
+ * @returns True if str starts with the given prefix
2345
+ */
2346
+ function startsWithPrefix(str, prefix) {
2347
+ if (prefix.length > str.length)
2348
+ return false;
2349
+ for (let i = 0; i < prefix.length; i++) {
2350
+ if (str[i] !== prefix[i])
2351
+ return false;
2352
+ }
2353
+ return true;
2354
+ }
2355
+ /**
2356
+ * Checks if the working tree is clean (no changes).
2357
+ *
2358
+ * @param options - Configuration for the status check
2359
+ * @returns True if working tree is clean with no uncommitted changes
2360
+ *
2361
+ * @example
2362
+ * if (!isClean()) {
2363
+ * throw new Error('Working tree has changes')
2364
+ * }
2365
+ */
2366
+ function isClean(options = {}) {
2367
+ const status = getStatus(options);
2368
+ return status.clean;
2369
+ }
2370
+ /**
2371
+ * Checks if the directory is a git repository.
2372
+ *
2373
+ * @param options - Status options
2374
+ * @returns True if in a git repository
2375
+ *
2376
+ * @example
2377
+ * if (!isGitRepository()) {
2378
+ * throw new Error('Not a git repository')
2379
+ * }
2380
+ */
2381
+ function isGitRepository(options = {}) {
2382
+ const opts = { ...DEFAULT_STATUS_OPTIONS, ...options };
2383
+ try {
2384
+ execSync('git rev-parse --is-inside-work-tree', {
2385
+ encoding: 'utf-8',
2386
+ cwd: opts.cwd,
2387
+ timeout: opts.timeout,
2388
+ stdio: ['pipe', 'pipe', 'pipe'],
2389
+ });
2390
+ return true;
2391
+ }
2392
+ catch {
2393
+ return false;
2394
+ }
2395
+ }
2396
+ /**
2397
+ * Gets the repository root directory.
2398
+ *
2399
+ * @param options - Status options
2400
+ * @returns Root directory path or null
2401
+ *
2402
+ * @example
2403
+ * const root = getRepositoryRoot()
2404
+ */
2405
+ function getRepositoryRoot(options = {}) {
2406
+ const opts = { ...DEFAULT_STATUS_OPTIONS, ...options };
2407
+ try {
2408
+ return execSync('git rev-parse --show-toplevel', {
2409
+ encoding: 'utf-8',
2410
+ cwd: opts.cwd,
2411
+ timeout: opts.timeout,
2412
+ stdio: ['pipe', 'pipe', 'pipe'],
2413
+ }).trim();
2414
+ }
2415
+ catch {
2416
+ return null;
2417
+ }
2418
+ }
2419
+ /**
2420
+ * Gets the current commit hash (HEAD).
2421
+ *
2422
+ * @param options - Status options
2423
+ * @returns Commit hash or null
2424
+ */
2425
+ function getHeadHash(options = {}) {
2426
+ const opts = { ...DEFAULT_STATUS_OPTIONS, ...options };
2427
+ try {
2428
+ return execSync('git rev-parse HEAD', {
2429
+ encoding: 'utf-8',
2430
+ cwd: opts.cwd,
2431
+ timeout: opts.timeout,
2432
+ stdio: ['pipe', 'pipe', 'pipe'],
2433
+ }).trim();
2434
+ }
2435
+ catch {
2436
+ return null;
2437
+ }
2438
+ }
2439
+ /**
2440
+ * Gets the short current commit hash.
2441
+ *
2442
+ * @param options - Status options
2443
+ * @returns Short hash or null
2444
+ */
2445
+ function getHeadShortHash(options = {}) {
2446
+ const opts = { ...DEFAULT_STATUS_OPTIONS, ...options };
2447
+ try {
2448
+ return execSync('git rev-parse --short HEAD', {
2449
+ encoding: 'utf-8',
2450
+ cwd: opts.cwd,
2451
+ timeout: opts.timeout,
2452
+ stdio: ['pipe', 'pipe', 'pipe'],
2453
+ }).trim();
2454
+ }
2455
+ catch {
2456
+ return null;
2457
+ }
2458
+ }
2459
+ /**
2460
+ * Checks if there are merge conflicts.
2461
+ *
2462
+ * @param options - Status options
2463
+ * @returns True if there are conflicts
2464
+ */
2465
+ function hasConflicts(options = {}) {
2466
+ const status = getStatus(options);
2467
+ return status.hasConflicts;
2468
+ }
2469
+ /**
2470
+ * Gets the number of commits ahead of upstream.
2471
+ *
2472
+ * @param options - Status options
2473
+ * @returns Number of commits ahead
2474
+ */
2475
+ function getAheadCount(options = {}) {
2476
+ const status = getStatus(options);
2477
+ return status.ahead;
2478
+ }
2479
+ /**
2480
+ * Gets the number of commits behind upstream.
2481
+ *
2482
+ * @param options - Status options
2483
+ * @returns Number of commits behind
2484
+ */
2485
+ function getBehindCount(options = {}) {
2486
+ const status = getStatus(options);
2487
+ return status.behind;
2488
+ }
2489
+ /**
2490
+ * Checks if the repository needs to be pushed.
2491
+ *
2492
+ * @param options - Status options
2493
+ * @returns True if there are unpushed commits
2494
+ */
2495
+ function needsPush(options = {}) {
2496
+ return getAheadCount(options) > 0;
2497
+ }
2498
+ /**
2499
+ * Checks if the repository needs to be pulled.
2500
+ *
2501
+ * @param options - Status options
2502
+ * @returns True if there are commits to pull
2503
+ */
2504
+ function needsPull(options = {}) {
2505
+ return getBehindCount(options) > 0;
2506
+ }
2507
+ /**
2508
+ * Gets list of staged file paths.
2509
+ *
2510
+ * @param options - Status options
2511
+ * @returns Array of staged file paths
2512
+ */
2513
+ function getStagedFiles(options = {}) {
2514
+ const status = getStatus(options);
2515
+ return status.staged.map((e) => e.path);
2516
+ }
2517
+ /**
2518
+ * Gets list of modified file paths (unstaged).
2519
+ *
2520
+ * @param options - Status options
2521
+ * @returns Array of modified file paths
2522
+ */
2523
+ function getModifiedFiles(options = {}) {
2524
+ const status = getStatus(options);
2525
+ return status.modified.map((e) => e.path);
2526
+ }
2527
+ /**
2528
+ * Gets list of untracked file paths.
2529
+ *
2530
+ * @param options - Status options
2531
+ * @returns Array of untracked file paths
2532
+ */
2533
+ function getUntrackedFiles(options = {}) {
2534
+ const status = getStatus(options);
2535
+ return status.untracked;
2536
+ }
2537
+
2538
+ /**
2539
+ * Default git client configuration.
2540
+ */
2541
+ const DEFAULT_GIT_CLIENT_CONFIG = {
2542
+ cwd: process.cwd(),
2543
+ timeout: 30000,
2544
+ throwOnError: true,
2545
+ };
2546
+ /**
2547
+ * Creates a git client for a specific working directory.
2548
+ *
2549
+ * @param config - Client configuration
2550
+ * @returns GitClient instance
2551
+ *
2552
+ * @example
2553
+ * const git = createGitClient({ cwd: '/path/to/repo' })
2554
+ * const status = git.getStatus()
2555
+ * const commits = git.getCommitsSince('v1.0.0')
2556
+ */
2557
+ function createGitClient(config = {}) {
2558
+ const cwd = config.cwd ?? process.cwd();
2559
+ const timeout = config.timeout ?? DEFAULT_GIT_CLIENT_CONFIG.timeout;
2560
+ const opts = { cwd, timeout };
2561
+ return {
2562
+ cwd,
2563
+ timeout,
2564
+ // Log operations
2565
+ getCommitLog: (options) => getCommitLog({ ...opts, ...options }),
2566
+ getCommitsBetween: (from, to, options) => getCommitsBetween(from, to, { ...opts, ...options }),
2567
+ getCommitsSince: (since, options) => getCommitsSince(since, { ...opts, ...options }),
2568
+ getCommit: (hash) => getCommit(hash, opts),
2569
+ commitExists: (hash) => commitExists(hash, opts),
2570
+ // Tag operations
2571
+ getTags: (options) => getTags({ ...opts, ...options }),
2572
+ getTag: (name) => getTag(name, opts),
2573
+ createTag: (name, options) => createTag(name, { ...opts, ...options }),
2574
+ deleteTag: (name) => deleteTag(name, opts),
2575
+ tagExists: (name) => tagExists(name, opts),
2576
+ getLatestTag: (options) => getLatestTag({ ...opts, ...options }),
2577
+ getTagsForPackage: (packageName, options) => getTagsForPackage(packageName, { ...opts, ...options }),
2578
+ pushTag: (name, remote) => pushTag(name, remote, opts),
2579
+ // Commit operations
2580
+ createCommit: (message, options) => commit(message, { ...opts, ...options }),
2581
+ stage: (files, options) => stage(files, { ...opts, ...options }),
2582
+ unstage: (files) => unstage(files, opts),
2583
+ stageAll: () => stageAll(opts),
2584
+ amendCommit: (message, options) => amendCommit(message, { ...opts, ...options }),
2585
+ createEmptyCommit: (message, options) => createEmptyCommit(message, { ...opts, ...options }),
2586
+ getHead: () => getHead(opts),
2587
+ getCurrentBranch: () => getCurrentBranch(opts),
2588
+ hasStagedChanges: () => hasStagedChanges(opts),
2589
+ hasUnstagedChanges: () => hasUnstagedChanges(opts),
2590
+ hasUntrackedFiles: () => hasUntrackedFiles(opts),
2591
+ // Status operations
2592
+ getStatus: () => getStatus(opts),
2593
+ isClean: () => isClean(opts),
2594
+ isGitRepository: () => isGitRepository(opts),
2595
+ getRepositoryRoot: () => getRepositoryRoot(opts),
2596
+ getHeadHash: () => getHeadHash(opts),
2597
+ getHeadShortHash: () => getHeadShortHash(opts),
2598
+ hasConflicts: () => hasConflicts(opts),
2599
+ getAheadCount: () => getAheadCount(opts),
2600
+ getBehindCount: () => getBehindCount(opts),
2601
+ needsPush: () => needsPush(opts),
2602
+ needsPull: () => needsPull(opts),
2603
+ getStagedFiles: () => getStagedFiles(opts),
2604
+ getModifiedFiles: () => getModifiedFiles(opts),
2605
+ getUntrackedFiles: () => getUntrackedFiles(opts),
2606
+ // Ref operations
2607
+ getRefs: () => getRefs(opts),
2608
+ getBranches: () => getBranches(opts),
2609
+ getRemoteBranches: (remote) => getRemoteBranches(opts, remote),
2610
+ fetch: (remote, options) => fetch(opts, remote, options),
2611
+ pull: (remote, branch) => pull(opts, remote, branch),
2612
+ push: (remote, branch, options) => push(opts, remote, branch, options),
2613
+ };
2614
+ }
2615
+ // ============================================================================
2616
+ // Additional ref operations used by the client
2617
+ // ============================================================================
2618
+ /**
2619
+ * Gets all refs from the repository.
2620
+ *
2621
+ * @param options - Configuration object containing cwd and timeout
2622
+ * @param options.cwd - Working directory for the git command
2623
+ * @param options.timeout - Command timeout in milliseconds
2624
+ * @returns Array of GitRef objects representing all refs in the repository
2625
+ */
2626
+ function getRefs(options) {
2627
+ try {
2628
+ const output = execSync('git show-ref', {
2629
+ encoding: 'utf-8',
2630
+ cwd: options.cwd,
2631
+ timeout: options.timeout,
2632
+ stdio: ['pipe', 'pipe', 'pipe'],
2633
+ });
2634
+ const refs = [];
2635
+ const lines = output.split('\n');
2636
+ for (const line of lines) {
2637
+ const trimmed = line.trim();
2638
+ if (!trimmed)
2639
+ continue;
2640
+ // Format: <hash> <refname>
2641
+ const spaceIndex = trimmed.indexOf(' ');
2642
+ if (spaceIndex === -1)
2643
+ continue;
2644
+ const hash = trimmed.slice(0, spaceIndex);
2645
+ const fullName = trimmed.slice(spaceIndex + 1);
2646
+ refs.push(createGitRef({ fullName, commitHash: hash }));
2647
+ }
2648
+ return refs;
2649
+ }
2650
+ catch {
2651
+ return [];
2652
+ }
2653
+ }
2654
+ /**
2655
+ * Gets local branches.
2656
+ *
2657
+ * @param options - Configuration object containing cwd and timeout
2658
+ * @param options.cwd - Working directory for the git command
2659
+ * @param options.timeout - Command timeout in milliseconds
2660
+ * @returns Array of GitRef objects representing local branches
2661
+ */
2662
+ function getBranches(options) {
2663
+ const refs = getRefs(options);
2664
+ return refs.filter((ref) => ref.type === 'branch');
2665
+ }
2666
+ /**
2667
+ * Gets remote branches.
2668
+ *
2669
+ * @param options - Configuration object containing cwd and timeout
2670
+ * @param options.cwd - Working directory for the git command
2671
+ * @param options.timeout - Command timeout in milliseconds
2672
+ * @param remote - Optional remote name to filter branches by
2673
+ * @returns Array of GitRef objects representing remote branches
2674
+ */
2675
+ function getRemoteBranches(options, remote) {
2676
+ const refs = getRefs(options);
2677
+ return refs.filter((ref) => {
2678
+ if (ref.type !== 'remote')
2679
+ return false;
2680
+ if (remote && ref.remote !== remote)
2681
+ return false;
2682
+ return true;
2683
+ });
2684
+ }
2685
+ /**
2686
+ * Fetches from remote.
2687
+ *
2688
+ * @param options - Configuration object containing cwd and timeout
2689
+ * @param options.cwd - Working directory for the git command
2690
+ * @param options.timeout - Command timeout in milliseconds
2691
+ * @param remote - Remote name to fetch from (defaults to 'origin')
2692
+ * @param fetchOptions - Additional fetch configuration
2693
+ * @param fetchOptions.prune - Whether to prune deleted remote branches
2694
+ * @param fetchOptions.tags - Whether to fetch tags
2695
+ * @returns True if fetch succeeded
2696
+ */
2697
+ function fetch(options, remote = 'origin', fetchOptions) {
2698
+ const args = ['fetch', remote];
2699
+ if (fetchOptions?.prune) {
2700
+ args.push('--prune');
2701
+ }
2702
+ if (fetchOptions?.tags) {
2703
+ args.push('--tags');
2704
+ }
2705
+ try {
2706
+ execSync(`git ${args.join(' ')}`, {
2707
+ encoding: 'utf-8',
2708
+ cwd: options.cwd,
2709
+ timeout: options.timeout * 3, // Allow more time for network
2710
+ stdio: ['pipe', 'pipe', 'pipe'],
2711
+ });
2712
+ return true;
2713
+ }
2714
+ catch {
2715
+ return false;
2716
+ }
2717
+ }
2718
+ /**
2719
+ * Pulls from remote.
2720
+ *
2721
+ * @param options - Configuration object containing cwd and timeout
2722
+ * @param options.cwd - Working directory for the git command
2723
+ * @param options.timeout - Command timeout in milliseconds
2724
+ * @param remote - Remote name to pull from (defaults to 'origin')
2725
+ * @param branch - Optional branch name to pull
2726
+ * @returns True if pull succeeded
2727
+ */
2728
+ function pull(options, remote = 'origin', branch) {
2729
+ const args = ['pull', remote];
2730
+ if (branch) {
2731
+ args.push(branch);
2732
+ }
2733
+ try {
2734
+ execSync(`git ${args.join(' ')}`, {
2735
+ encoding: 'utf-8',
2736
+ cwd: options.cwd,
2737
+ timeout: options.timeout * 3,
2738
+ stdio: ['pipe', 'pipe', 'pipe'],
2739
+ });
2740
+ return true;
2741
+ }
2742
+ catch {
2743
+ return false;
2744
+ }
2745
+ }
2746
+ /**
2747
+ * Pushes to remote.
2748
+ *
2749
+ * @param options - Configuration object containing cwd and timeout
2750
+ * @param options.cwd - Working directory for the git command
2751
+ * @param options.timeout - Command timeout in milliseconds
2752
+ * @param remote - Remote name to push to (defaults to 'origin')
2753
+ * @param branch - Optional branch name to push
2754
+ * @param pushOptions - Additional push configuration
2755
+ * @param pushOptions.force - Whether to force push
2756
+ * @param pushOptions.setUpstream - Whether to set upstream tracking
2757
+ * @returns True if push succeeded
2758
+ */
2759
+ function push(options, remote = 'origin', branch, pushOptions) {
2760
+ const args = ['push', remote];
2761
+ if (branch) {
2762
+ args.push(branch);
2763
+ }
2764
+ if (pushOptions?.force) {
2765
+ args.push('--force');
2766
+ }
2767
+ if (pushOptions?.setUpstream) {
2768
+ args.push('--set-upstream');
2769
+ }
2770
+ try {
2771
+ execSync(`git ${args.join(' ')}`, {
2772
+ encoding: 'utf-8',
2773
+ cwd: options.cwd,
2774
+ timeout: options.timeout * 3,
2775
+ stdio: ['pipe', 'pipe', 'pipe'],
2776
+ });
2777
+ return true;
2778
+ }
2779
+ catch {
2780
+ return false;
2781
+ }
2782
+ }
2783
+
2784
+ export { DEFAULT_COMMIT_OPTIONS, DEFAULT_GIT_CLIENT_CONFIG, DEFAULT_LOG_OPTIONS, DEFAULT_STATUS_OPTIONS, DEFAULT_TAG_OPTIONS, amendCommit, amendCommitNoEdit, buildRefName, buildTagName, commit, commitExists, compareRefsByName, compareTagsByVersion, createAnnotatedTag, createEmptyCommit, createGitClient, createGitCommit, createGitRef, createLightweightTag, createTag, deleteTag, escapeAuthor, escapeFilePath, escapeGitArg, escapeGitMessage, escapeGitPath, escapeGitRef, escapeGitTagPattern, extractPackageFromTag, extractScope, extractType, extractVersionFromTag, filterRefsByRemote, filterRefsByType, getAheadCount, getBehindCount, getCommit, getCommitLog, getCommitsBetween, getCommitsSince, getCurrentBranch, getHead, getHeadHash, getHeadShortHash, getLatestTag, getModifiedFiles, getRemote, getRepositoryRoot, getShortHash, getStagedFiles, getStatus, getTag, getTags, getTagsForPackage, getUntrackedFiles, hasConflicts, hasStagedChanges, hasUnstagedChanges, hasUntrackedFiles, isAnnotatedTag, isBranchRef, isClean, isGitRepository, isHeadRef, isLightweightTag, isMergeCommit, isRemoteRef, isRootCommit, isSameCommit, isTagRef, needsPull, needsPush, pushTag, stage, stageAll, tagExists, unstage };
2785
+ //# sourceMappingURL=index.esm.js.map