@hominis/fireforge 0.9.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 (316) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +294 -0
  3. package/README.md +435 -0
  4. package/dist/bin/fireforge.d.ts +10 -0
  5. package/dist/bin/fireforge.js +29 -0
  6. package/dist/src/cli.d.ts +33 -0
  7. package/dist/src/cli.js +180 -0
  8. package/dist/src/commands/bootstrap.d.ts +9 -0
  9. package/dist/src/commands/bootstrap.js +73 -0
  10. package/dist/src/commands/build.d.ts +11 -0
  11. package/dist/src/commands/build.js +102 -0
  12. package/dist/src/commands/config.d.ts +13 -0
  13. package/dist/src/commands/config.js +135 -0
  14. package/dist/src/commands/discard.d.ts +12 -0
  15. package/dist/src/commands/discard.js +84 -0
  16. package/dist/src/commands/doctor.d.ts +18 -0
  17. package/dist/src/commands/doctor.js +356 -0
  18. package/dist/src/commands/download.d.ts +11 -0
  19. package/dist/src/commands/download.js +127 -0
  20. package/dist/src/commands/export-all.d.ts +11 -0
  21. package/dist/src/commands/export-all.js +122 -0
  22. package/dist/src/commands/export-shared.d.ts +48 -0
  23. package/dist/src/commands/export-shared.js +208 -0
  24. package/dist/src/commands/export.d.ts +13 -0
  25. package/dist/src/commands/export.js +178 -0
  26. package/dist/src/commands/furnace/apply.d.ts +7 -0
  27. package/dist/src/commands/furnace/apply.js +80 -0
  28. package/dist/src/commands/furnace/create.d.ts +8 -0
  29. package/dist/src/commands/furnace/create.js +377 -0
  30. package/dist/src/commands/furnace/deploy.d.ts +8 -0
  31. package/dist/src/commands/furnace/deploy.js +338 -0
  32. package/dist/src/commands/furnace/diff.d.ts +7 -0
  33. package/dist/src/commands/furnace/diff.js +119 -0
  34. package/dist/src/commands/furnace/index.d.ts +16 -0
  35. package/dist/src/commands/furnace/index.js +121 -0
  36. package/dist/src/commands/furnace/list.d.ts +5 -0
  37. package/dist/src/commands/furnace/list.js +65 -0
  38. package/dist/src/commands/furnace/override.d.ts +8 -0
  39. package/dist/src/commands/furnace/override.js +188 -0
  40. package/dist/src/commands/furnace/preview.d.ts +7 -0
  41. package/dist/src/commands/furnace/preview.js +96 -0
  42. package/dist/src/commands/furnace/remove.d.ts +8 -0
  43. package/dist/src/commands/furnace/remove.js +159 -0
  44. package/dist/src/commands/furnace/scan.d.ts +5 -0
  45. package/dist/src/commands/furnace/scan.js +112 -0
  46. package/dist/src/commands/furnace/status.d.ts +7 -0
  47. package/dist/src/commands/furnace/status.js +137 -0
  48. package/dist/src/commands/furnace/validate.d.ts +6 -0
  49. package/dist/src/commands/furnace/validate.js +91 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +7 -0
  51. package/dist/src/commands/furnace/validation-output.js +22 -0
  52. package/dist/src/commands/import.d.ts +11 -0
  53. package/dist/src/commands/import.js +241 -0
  54. package/dist/src/commands/lint.d.ts +10 -0
  55. package/dist/src/commands/lint.js +118 -0
  56. package/dist/src/commands/package.d.ts +11 -0
  57. package/dist/src/commands/package.js +80 -0
  58. package/dist/src/commands/re-export.d.ts +12 -0
  59. package/dist/src/commands/re-export.js +242 -0
  60. package/dist/src/commands/rebase/abort.d.ts +7 -0
  61. package/dist/src/commands/rebase/abort.js +49 -0
  62. package/dist/src/commands/rebase/confirm.d.ts +18 -0
  63. package/dist/src/commands/rebase/confirm.js +33 -0
  64. package/dist/src/commands/rebase/continue.d.ts +7 -0
  65. package/dist/src/commands/rebase/continue.js +81 -0
  66. package/dist/src/commands/rebase/index.d.ts +22 -0
  67. package/dist/src/commands/rebase/index.js +127 -0
  68. package/dist/src/commands/rebase/patch-loop.d.ts +9 -0
  69. package/dist/src/commands/rebase/patch-loop.js +135 -0
  70. package/dist/src/commands/rebase/summary.d.ts +12 -0
  71. package/dist/src/commands/rebase/summary.js +43 -0
  72. package/dist/src/commands/rebase.d.ts +4 -0
  73. package/dist/src/commands/rebase.js +6 -0
  74. package/dist/src/commands/register.d.ts +13 -0
  75. package/dist/src/commands/register.js +67 -0
  76. package/dist/src/commands/reset.d.ts +11 -0
  77. package/dist/src/commands/reset.js +83 -0
  78. package/dist/src/commands/resolve.d.ts +9 -0
  79. package/dist/src/commands/resolve.js +124 -0
  80. package/dist/src/commands/run.d.ts +9 -0
  81. package/dist/src/commands/run.js +91 -0
  82. package/dist/src/commands/setup-support.d.ts +23 -0
  83. package/dist/src/commands/setup-support.js +310 -0
  84. package/dist/src/commands/setup.d.ts +11 -0
  85. package/dist/src/commands/setup.js +94 -0
  86. package/dist/src/commands/status.d.ts +11 -0
  87. package/dist/src/commands/status.js +268 -0
  88. package/dist/src/commands/test.d.ts +12 -0
  89. package/dist/src/commands/test.js +182 -0
  90. package/dist/src/commands/token-coverage.d.ts +5 -0
  91. package/dist/src/commands/token-coverage.js +57 -0
  92. package/dist/src/commands/token.d.ts +14 -0
  93. package/dist/src/commands/token.js +121 -0
  94. package/dist/src/commands/watch.d.ts +9 -0
  95. package/dist/src/commands/watch.js +112 -0
  96. package/dist/src/commands/wire.d.ts +13 -0
  97. package/dist/src/commands/wire.js +149 -0
  98. package/dist/src/core/ast-utils.d.ts +47 -0
  99. package/dist/src/core/ast-utils.js +57 -0
  100. package/dist/src/core/brand-validation.d.ts +7 -0
  101. package/dist/src/core/brand-validation.js +15 -0
  102. package/dist/src/core/branding.d.ts +49 -0
  103. package/dist/src/core/branding.js +229 -0
  104. package/dist/src/core/browser-wire.d.ts +40 -0
  105. package/dist/src/core/browser-wire.js +66 -0
  106. package/dist/src/core/build-prepare.d.ts +25 -0
  107. package/dist/src/core/build-prepare.js +93 -0
  108. package/dist/src/core/config-mutate.d.ts +15 -0
  109. package/dist/src/core/config-mutate.js +51 -0
  110. package/dist/src/core/config-paths.d.ts +28 -0
  111. package/dist/src/core/config-paths.js +65 -0
  112. package/dist/src/core/config-state.d.ts +28 -0
  113. package/dist/src/core/config-state.js +152 -0
  114. package/dist/src/core/config-validate.d.ts +11 -0
  115. package/dist/src/core/config-validate.js +141 -0
  116. package/dist/src/core/config.d.ts +39 -0
  117. package/dist/src/core/config.js +70 -0
  118. package/dist/src/core/file-lock.d.ts +11 -0
  119. package/dist/src/core/file-lock.js +80 -0
  120. package/dist/src/core/firefox-archive.d.ts +40 -0
  121. package/dist/src/core/firefox-archive.js +63 -0
  122. package/dist/src/core/firefox-cache.d.ts +23 -0
  123. package/dist/src/core/firefox-cache.js +134 -0
  124. package/dist/src/core/firefox-download.d.ts +21 -0
  125. package/dist/src/core/firefox-download.js +129 -0
  126. package/dist/src/core/firefox-extract.d.ts +21 -0
  127. package/dist/src/core/firefox-extract.js +53 -0
  128. package/dist/src/core/firefox.d.ts +34 -0
  129. package/dist/src/core/firefox.js +78 -0
  130. package/dist/src/core/furnace-apply-helpers.d.ts +21 -0
  131. package/dist/src/core/furnace-apply-helpers.js +244 -0
  132. package/dist/src/core/furnace-apply.d.ts +16 -0
  133. package/dist/src/core/furnace-apply.js +147 -0
  134. package/dist/src/core/furnace-config.d.ts +94 -0
  135. package/dist/src/core/furnace-config.js +372 -0
  136. package/dist/src/core/furnace-constants.d.ts +4 -0
  137. package/dist/src/core/furnace-constants.js +6 -0
  138. package/dist/src/core/furnace-registration-ast.d.ts +24 -0
  139. package/dist/src/core/furnace-registration-ast.js +218 -0
  140. package/dist/src/core/furnace-registration-remove.d.ts +14 -0
  141. package/dist/src/core/furnace-registration-remove.js +89 -0
  142. package/dist/src/core/furnace-registration-validate.d.ts +20 -0
  143. package/dist/src/core/furnace-registration-validate.js +40 -0
  144. package/dist/src/core/furnace-registration.d.ts +29 -0
  145. package/dist/src/core/furnace-registration.js +96 -0
  146. package/dist/src/core/furnace-rollback.d.ts +20 -0
  147. package/dist/src/core/furnace-rollback.js +66 -0
  148. package/dist/src/core/furnace-scanner.d.ts +40 -0
  149. package/dist/src/core/furnace-scanner.js +143 -0
  150. package/dist/src/core/furnace-stories.d.ts +37 -0
  151. package/dist/src/core/furnace-stories.js +185 -0
  152. package/dist/src/core/furnace-validate-accessibility.d.ts +6 -0
  153. package/dist/src/core/furnace-validate-accessibility.js +32 -0
  154. package/dist/src/core/furnace-validate-checks.d.ts +4 -0
  155. package/dist/src/core/furnace-validate-checks.js +7 -0
  156. package/dist/src/core/furnace-validate-compatibility.d.ts +6 -0
  157. package/dist/src/core/furnace-validate-compatibility.js +57 -0
  158. package/dist/src/core/furnace-validate-helpers.d.ts +28 -0
  159. package/dist/src/core/furnace-validate-helpers.js +129 -0
  160. package/dist/src/core/furnace-validate-registration.d.ts +37 -0
  161. package/dist/src/core/furnace-validate-registration.js +220 -0
  162. package/dist/src/core/furnace-validate-structure.d.ts +6 -0
  163. package/dist/src/core/furnace-validate-structure.js +66 -0
  164. package/dist/src/core/furnace-validate.d.ts +16 -0
  165. package/dist/src/core/furnace-validate.js +103 -0
  166. package/dist/src/core/git-base.d.ts +47 -0
  167. package/dist/src/core/git-base.js +50 -0
  168. package/dist/src/core/git-diff.d.ts +63 -0
  169. package/dist/src/core/git-diff.js +246 -0
  170. package/dist/src/core/git-file-ops.d.ts +65 -0
  171. package/dist/src/core/git-file-ops.js +141 -0
  172. package/dist/src/core/git-status.d.ts +65 -0
  173. package/dist/src/core/git-status.js +163 -0
  174. package/dist/src/core/git.d.ts +113 -0
  175. package/dist/src/core/git.js +363 -0
  176. package/dist/src/core/license-headers.d.ts +36 -0
  177. package/dist/src/core/license-headers.js +83 -0
  178. package/dist/src/core/mach-build-artifacts.d.ts +29 -0
  179. package/dist/src/core/mach-build-artifacts.js +117 -0
  180. package/dist/src/core/mach-mozconfig.d.ts +17 -0
  181. package/dist/src/core/mach-mozconfig.js +50 -0
  182. package/dist/src/core/mach-python.d.ts +16 -0
  183. package/dist/src/core/mach-python.js +126 -0
  184. package/dist/src/core/mach.d.ts +106 -0
  185. package/dist/src/core/mach.js +166 -0
  186. package/dist/src/core/manifest-helpers.d.ts +25 -0
  187. package/dist/src/core/manifest-helpers.js +96 -0
  188. package/dist/src/core/manifest-register.d.ts +30 -0
  189. package/dist/src/core/manifest-register.js +65 -0
  190. package/dist/src/core/manifest-rules.d.ts +39 -0
  191. package/dist/src/core/manifest-rules.js +151 -0
  192. package/dist/src/core/manifest-tokenizers.d.ts +34 -0
  193. package/dist/src/core/manifest-tokenizers.js +84 -0
  194. package/dist/src/core/parser-fallback.d.ts +36 -0
  195. package/dist/src/core/parser-fallback.js +43 -0
  196. package/dist/src/core/patch-apply-fuzz.d.ts +29 -0
  197. package/dist/src/core/patch-apply-fuzz.js +70 -0
  198. package/dist/src/core/patch-apply.d.ts +46 -0
  199. package/dist/src/core/patch-apply.js +235 -0
  200. package/dist/src/core/patch-export.d.ts +99 -0
  201. package/dist/src/core/patch-export.js +314 -0
  202. package/dist/src/core/patch-files.d.ts +11 -0
  203. package/dist/src/core/patch-files.js +51 -0
  204. package/dist/src/core/patch-lint.d.ts +72 -0
  205. package/dist/src/core/patch-lint.js +403 -0
  206. package/dist/src/core/patch-lock.d.ts +8 -0
  207. package/dist/src/core/patch-lock.js +29 -0
  208. package/dist/src/core/patch-manifest-consistency.d.ts +24 -0
  209. package/dist/src/core/patch-manifest-consistency.js +135 -0
  210. package/dist/src/core/patch-manifest-io.d.ts +36 -0
  211. package/dist/src/core/patch-manifest-io.js +77 -0
  212. package/dist/src/core/patch-manifest-query.d.ts +48 -0
  213. package/dist/src/core/patch-manifest-query.js +124 -0
  214. package/dist/src/core/patch-manifest-validate.d.ts +22 -0
  215. package/dist/src/core/patch-manifest-validate.js +72 -0
  216. package/dist/src/core/patch-manifest.d.ts +11 -0
  217. package/dist/src/core/patch-manifest.js +12 -0
  218. package/dist/src/core/patch-parse.d.ts +43 -0
  219. package/dist/src/core/patch-parse.js +143 -0
  220. package/dist/src/core/patch-transform.d.ts +21 -0
  221. package/dist/src/core/patch-transform.js +138 -0
  222. package/dist/src/core/rebase-session.d.ts +47 -0
  223. package/dist/src/core/rebase-session.js +65 -0
  224. package/dist/src/core/register-browser-content.d.ts +11 -0
  225. package/dist/src/core/register-browser-content.js +116 -0
  226. package/dist/src/core/register-module.d.ts +11 -0
  227. package/dist/src/core/register-module.js +76 -0
  228. package/dist/src/core/register-shared-css.d.ts +11 -0
  229. package/dist/src/core/register-shared-css.js +117 -0
  230. package/dist/src/core/register-test-manifest.d.ts +18 -0
  231. package/dist/src/core/register-test-manifest.js +99 -0
  232. package/dist/src/core/state-file.d.ts +4 -0
  233. package/dist/src/core/state-file.js +25 -0
  234. package/dist/src/core/token-coverage.d.ts +12 -0
  235. package/dist/src/core/token-coverage.js +74 -0
  236. package/dist/src/core/token-manager.d.ts +55 -0
  237. package/dist/src/core/token-manager.js +387 -0
  238. package/dist/src/core/wire-destroy.d.ts +21 -0
  239. package/dist/src/core/wire-destroy.js +103 -0
  240. package/dist/src/core/wire-dom-fragment.d.ts +23 -0
  241. package/dist/src/core/wire-dom-fragment.js +129 -0
  242. package/dist/src/core/wire-init.d.ts +23 -0
  243. package/dist/src/core/wire-init.js +201 -0
  244. package/dist/src/core/wire-subscript.d.ts +20 -0
  245. package/dist/src/core/wire-subscript.js +134 -0
  246. package/dist/src/core/wire-targets.d.ts +7 -0
  247. package/dist/src/core/wire-targets.js +9 -0
  248. package/dist/src/core/wire-utils.d.ts +88 -0
  249. package/dist/src/core/wire-utils.js +279 -0
  250. package/dist/src/errors/base.d.ts +60 -0
  251. package/dist/src/errors/base.js +87 -0
  252. package/dist/src/errors/build.d.ts +52 -0
  253. package/dist/src/errors/build.js +114 -0
  254. package/dist/src/errors/codes.d.ts +29 -0
  255. package/dist/src/errors/codes.js +30 -0
  256. package/dist/src/errors/config.d.ts +31 -0
  257. package/dist/src/errors/config.js +61 -0
  258. package/dist/src/errors/download.d.ts +42 -0
  259. package/dist/src/errors/download.js +95 -0
  260. package/dist/src/errors/furnace.d.ts +10 -0
  261. package/dist/src/errors/furnace.js +22 -0
  262. package/dist/src/errors/git.d.ts +41 -0
  263. package/dist/src/errors/git.js +99 -0
  264. package/dist/src/errors/patch.d.ts +10 -0
  265. package/dist/src/errors/patch.js +26 -0
  266. package/dist/src/errors/rebase.d.ts +20 -0
  267. package/dist/src/errors/rebase.js +30 -0
  268. package/dist/src/index.d.ts +21 -0
  269. package/dist/src/index.js +21 -0
  270. package/dist/src/types/cli.d.ts +14 -0
  271. package/dist/src/types/cli.js +2 -0
  272. package/dist/src/types/commands/index.d.ts +6 -0
  273. package/dist/src/types/commands/index.js +6 -0
  274. package/dist/src/types/commands/options.d.ts +239 -0
  275. package/dist/src/types/commands/options.js +6 -0
  276. package/dist/src/types/commands/patches.d.ts +89 -0
  277. package/dist/src/types/commands/patches.js +6 -0
  278. package/dist/src/types/commands/project.d.ts +71 -0
  279. package/dist/src/types/commands/project.js +6 -0
  280. package/dist/src/types/config.d.ts +101 -0
  281. package/dist/src/types/config.js +2 -0
  282. package/dist/src/types/furnace.d.ts +158 -0
  283. package/dist/src/types/furnace.js +2 -0
  284. package/dist/src/types/index.d.ts +6 -0
  285. package/dist/src/types/index.js +6 -0
  286. package/dist/src/utils/errors.d.ts +2 -0
  287. package/dist/src/utils/errors.js +15 -0
  288. package/dist/src/utils/fs.d.ts +72 -0
  289. package/dist/src/utils/fs.js +179 -0
  290. package/dist/src/utils/logger.d.ts +58 -0
  291. package/dist/src/utils/logger.js +120 -0
  292. package/dist/src/utils/options.d.ts +8 -0
  293. package/dist/src/utils/options.js +16 -0
  294. package/dist/src/utils/package-root.d.ts +10 -0
  295. package/dist/src/utils/package-root.js +53 -0
  296. package/dist/src/utils/parse.d.ts +110 -0
  297. package/dist/src/utils/parse.js +200 -0
  298. package/dist/src/utils/paths.d.ts +10 -0
  299. package/dist/src/utils/paths.js +43 -0
  300. package/dist/src/utils/platform.d.ts +38 -0
  301. package/dist/src/utils/platform.js +56 -0
  302. package/dist/src/utils/process.d.ts +80 -0
  303. package/dist/src/utils/process.js +188 -0
  304. package/dist/src/utils/regex.d.ts +24 -0
  305. package/dist/src/utils/regex.js +40 -0
  306. package/dist/src/utils/validation.d.ts +133 -0
  307. package/dist/src/utils/validation.js +250 -0
  308. package/package.json +106 -0
  309. package/templates/configs/common.mozconfig +24 -0
  310. package/templates/configs/darwin.mozconfig +10 -0
  311. package/templates/configs/linux.mozconfig +12 -0
  312. package/templates/configs/win32.mozconfig +14 -0
  313. package/templates/licenses/0BSD.md +14 -0
  314. package/templates/licenses/EUPL-1.2.md +294 -0
  315. package/templates/licenses/GPL-2.0-or-later.md +339 -0
  316. package/templates/licenses/MPL-2.0.md +383 -0
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Cache validation, invalidation, and download-to-cache logic.
4
+ */
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { createReadStream } from 'node:fs';
7
+ import { rename } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { pipeline } from 'node:stream/promises';
10
+ import { toError } from '../utils/errors.js';
11
+ import { pathExists, readJson, removeFile, writeJson } from '../utils/fs.js';
12
+ import { verbose } from '../utils/logger.js';
13
+ import { validateArchiveMetadata } from './firefox-archive.js';
14
+ import { downloadFile } from './firefox-download.js';
15
+ /**
16
+ * Computes the SHA-256 hex digest of a file.
17
+ * @param filePath - Path to the file
18
+ */
19
+ export async function sha256File(filePath) {
20
+ const hash = createHash('sha256');
21
+ const stream = createReadStream(filePath);
22
+ await pipeline(stream, hash);
23
+ return hash.digest('hex');
24
+ }
25
+ /**
26
+ * Ensures a valid cached archive exists, downloading it if needed.
27
+ * @param archive - Resolved archive descriptor
28
+ * @param cacheDir - Cache directory
29
+ * @param onProgress - Optional progress callback
30
+ */
31
+ export async function ensureCachedArchive(archive, cacheDir, onProgress) {
32
+ if (await validateCachedArchive(archive, cacheDir)) {
33
+ return;
34
+ }
35
+ await invalidateArchiveCache(archive, cacheDir);
36
+ await downloadToCache(archive, cacheDir, onProgress);
37
+ }
38
+ /**
39
+ * Validates a cached archive using sidecar metadata and SHA-256 checksum.
40
+ * @param archive - Resolved archive descriptor
41
+ * @param cacheDir - Cache directory
42
+ * @returns True if the cache entry is valid
43
+ */
44
+ async function validateCachedArchive(archive, cacheDir) {
45
+ const tarballPath = join(cacheDir, archive.filename);
46
+ const metadataPath = join(cacheDir, archive.metadataFilename);
47
+ if (!(await pathExists(tarballPath)) || !(await pathExists(metadataPath))) {
48
+ return false;
49
+ }
50
+ try {
51
+ const metadata = validateArchiveMetadata(await readJson(metadataPath));
52
+ if (metadata.product !== archive.product ||
53
+ metadata.archiveVersion !== archive.archiveVersion ||
54
+ metadata.url !== archive.url) {
55
+ return false;
56
+ }
57
+ if (metadata.contentLength !== undefined) {
58
+ const { stat } = await import('node:fs/promises');
59
+ const archiveStats = await stat(tarballPath);
60
+ if (archiveStats.size !== metadata.contentLength) {
61
+ return false;
62
+ }
63
+ }
64
+ if (metadata.sha256) {
65
+ const actualHash = await sha256File(tarballPath);
66
+ if (actualHash !== metadata.sha256) {
67
+ return false;
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+ catch (error) {
73
+ verbose(`Cache validation failed for ${tarballPath}; treating cache entry as invalid: ${toError(error).message}`);
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * Downloads an archive to cache using an atomic temp file and sidecar metadata.
79
+ * @param archive - Resolved archive descriptor
80
+ * @param cacheDir - Cache directory
81
+ * @param onProgress - Optional progress callback
82
+ */
83
+ async function downloadToCache(archive, cacheDir, onProgress) {
84
+ const tarballPath = join(cacheDir, archive.filename);
85
+ // Use a unique .part path so concurrent downloads for the same archive
86
+ // do not clobber each other's partial files.
87
+ const partPath = `${tarballPath}.part-${randomUUID()}`;
88
+ const metadataPath = join(cacheDir, archive.metadataFilename);
89
+ try {
90
+ const contentLength = await downloadFile(archive.url, partPath, onProgress);
91
+ await rename(partPath, tarballPath);
92
+ const sha256 = await sha256File(tarballPath);
93
+ await writeJson(metadataPath, {
94
+ requestedVersion: archive.requestedVersion,
95
+ product: archive.product,
96
+ archiveVersion: archive.archiveVersion,
97
+ url: archive.url,
98
+ ...(contentLength !== undefined ? { contentLength } : {}),
99
+ sha256,
100
+ downloadedAt: new Date().toISOString(),
101
+ });
102
+ }
103
+ catch (error) {
104
+ await removeFile(partPath);
105
+ await removeFile(tarballPath);
106
+ await removeFile(metadataPath);
107
+ throw error;
108
+ }
109
+ }
110
+ /**
111
+ * Removes cached tarball, metadata, and partial download files for an archive.
112
+ * @param archive - Resolved archive descriptor
113
+ * @param cacheDir - Cache directory
114
+ */
115
+ export async function invalidateArchiveCache(archive, cacheDir) {
116
+ const tarballPath = join(cacheDir, archive.filename);
117
+ const metadataPath = join(cacheDir, archive.metadataFilename);
118
+ // Clean up any partial download files (may have unique suffixes from
119
+ // concurrent download attempts).
120
+ const partPrefix = `${archive.filename}.part`;
121
+ try {
122
+ const { readdir } = await import('node:fs/promises');
123
+ const entries = await readdir(cacheDir);
124
+ await Promise.all(entries
125
+ .filter((name) => name.startsWith(partPrefix))
126
+ .map((name) => removeFile(join(cacheDir, name))));
127
+ }
128
+ catch {
129
+ // Cache dir may not exist yet — that's fine.
130
+ }
131
+ await removeFile(tarballPath);
132
+ await removeFile(metadataPath);
133
+ }
134
+ //# sourceMappingURL=firefox-cache.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Download with retry, stall detection, and progress tracking.
3
+ */
4
+ /**
5
+ * Progress callback for download operations.
6
+ */
7
+ export type ProgressCallback = (downloaded: number, total: number) => void;
8
+ /**
9
+ * Fetches a URL with timeout and bounded retry for transient failures.
10
+ *
11
+ * Non-retryable errors (e.g. 404) are thrown immediately.
12
+ */
13
+ export declare function fetchWithRetry(url: string): Promise<Response>;
14
+ /**
15
+ * Downloads a file from a URL with progress tracking, timeout, and retry.
16
+ * @param url - URL to download
17
+ * @param destPath - Destination file path
18
+ * @param onProgress - Optional progress callback
19
+ * @returns The content-length if available, otherwise undefined
20
+ */
21
+ export declare function downloadFile(url: string, destPath: string, onProgress?: ProgressCallback): Promise<number | undefined>;
@@ -0,0 +1,129 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Download with retry, stall detection, and progress tracking.
4
+ */
5
+ import { createWriteStream } from 'node:fs';
6
+ import { basename } from 'node:path';
7
+ import { Readable, Transform } from 'node:stream';
8
+ import { pipeline } from 'node:stream/promises';
9
+ import { DownloadError, VersionNotFoundError } from '../errors/download.js';
10
+ /** Default request timeout in milliseconds (60 seconds). */
11
+ const REQUEST_TIMEOUT_MS = 60_000;
12
+ /** Maximum number of download attempts. */
13
+ const MAX_ATTEMPTS = 3;
14
+ /** HTTP status codes that are retryable. */
15
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
16
+ /** Base delay between retries in milliseconds. */
17
+ const RETRY_BASE_DELAY_MS = 1_000;
18
+ /** Stall detection timeout — abort if no data received for this duration (30 seconds). */
19
+ const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
20
+ /**
21
+ * Fetches a URL with timeout and bounded retry for transient failures.
22
+ *
23
+ * Non-retryable errors (e.g. 404) are thrown immediately.
24
+ */
25
+ export async function fetchWithRetry(url) {
26
+ let lastError;
27
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => {
30
+ controller.abort();
31
+ }, REQUEST_TIMEOUT_MS);
32
+ try {
33
+ const response = await fetch(url, { signal: controller.signal });
34
+ if (response.status === 404) {
35
+ throw new VersionNotFoundError(basename(url).replace('.source.tar.xz', ''));
36
+ }
37
+ if (RETRYABLE_STATUS_CODES.has(response.status)) {
38
+ lastError = new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
39
+ }
40
+ else if (!response.ok) {
41
+ throw new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
42
+ }
43
+ else {
44
+ return response;
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (error instanceof VersionNotFoundError || error instanceof DownloadError) {
49
+ throw error;
50
+ }
51
+ // Network / timeout errors are retryable
52
+ lastError = error;
53
+ }
54
+ finally {
55
+ clearTimeout(timer);
56
+ }
57
+ if (attempt < MAX_ATTEMPTS - 1) {
58
+ await new Promise((resolve) => {
59
+ setTimeout(resolve, RETRY_BASE_DELAY_MS * (attempt + 1) ** 2);
60
+ });
61
+ }
62
+ }
63
+ if (lastError instanceof DownloadError) {
64
+ throw lastError;
65
+ }
66
+ throw new DownloadError(lastError instanceof Error ? lastError.message : 'Download failed after retries', url, lastError instanceof Error ? lastError : undefined);
67
+ }
68
+ /**
69
+ * Creates a Transform stream that aborts if no data is received within the stall timeout.
70
+ * The timer resets on each chunk. If it fires, the stream is destroyed with a DownloadError.
71
+ * @param url - URL being downloaded (for error messages)
72
+ * @param timeoutMs - Stall timeout in milliseconds
73
+ */
74
+ function createStallDetector(url, timeoutMs = DOWNLOAD_STALL_TIMEOUT_MS) {
75
+ let timer;
76
+ const detector = new Transform({
77
+ transform(chunk, _encoding, callback) {
78
+ if (timer !== undefined)
79
+ clearTimeout(timer);
80
+ timer = setTimeout(() => {
81
+ detector.destroy(new DownloadError(`Download stalled: no data received for ${Math.round(timeoutMs / 1000)} seconds`, url));
82
+ }, timeoutMs);
83
+ callback(null, chunk);
84
+ },
85
+ flush(callback) {
86
+ if (timer !== undefined)
87
+ clearTimeout(timer);
88
+ callback();
89
+ },
90
+ });
91
+ // Start the initial timer (covers the case where the first chunk never arrives).
92
+ timer = setTimeout(() => {
93
+ detector.destroy(new DownloadError(`Download stalled: no data received for ${Math.round(timeoutMs / 1000)} seconds`, url));
94
+ }, timeoutMs);
95
+ return detector;
96
+ }
97
+ /**
98
+ * Downloads a file from a URL with progress tracking, timeout, and retry.
99
+ * @param url - URL to download
100
+ * @param destPath - Destination file path
101
+ * @param onProgress - Optional progress callback
102
+ * @returns The content-length if available, otherwise undefined
103
+ */
104
+ export async function downloadFile(url, destPath, onProgress) {
105
+ const response = await fetchWithRetry(url);
106
+ if (!response.body) {
107
+ throw new DownloadError('No response body received', url);
108
+ }
109
+ const totalSize = parseInt(response.headers.get('content-length') ?? '0', 10);
110
+ let downloadedSize = 0;
111
+ const fileStream = createWriteStream(destPath);
112
+ const nodeStream = Readable.fromWeb(response.body);
113
+ const stallDetector = createStallDetector(url);
114
+ if (onProgress && totalSize > 0) {
115
+ const progress = new Transform({
116
+ transform(chunk, _encoding, callback) {
117
+ downloadedSize += chunk.length;
118
+ onProgress(downloadedSize, totalSize);
119
+ callback(null, chunk);
120
+ },
121
+ });
122
+ await pipeline(nodeStream, stallDetector, progress, fileStream);
123
+ }
124
+ else {
125
+ await pipeline(nodeStream, stallDetector, fileStream);
126
+ }
127
+ return totalSize > 0 ? totalSize : undefined;
128
+ }
129
+ //# sourceMappingURL=firefox-download.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Firefox source extraction and installed version detection.
3
+ */
4
+ /**
5
+ * Extracts a tar.xz archive.
6
+ * @param archivePath - Path to the archive
7
+ * @param destDir - Destination directory
8
+ */
9
+ export declare function extractTarXz(archivePath: string, destDir: string): Promise<void>;
10
+ /**
11
+ * Gets the Firefox version from an existing source directory.
12
+ * @param engineDir - Path to the engine directory
13
+ * @returns Firefox version string
14
+ */
15
+ export declare function getFirefoxVersion(engineDir: string): Promise<string | undefined>;
16
+ /**
17
+ * Formats bytes into a human-readable string.
18
+ * @param bytes - Number of bytes
19
+ * @returns Formatted string (e.g., "1.5 GB")
20
+ */
21
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Firefox source extraction and installed version detection.
4
+ */
5
+ import { join } from 'node:path';
6
+ import { ExtractionError } from '../errors/download.js';
7
+ import { ensureDir, pathExists } from '../utils/fs.js';
8
+ import { exec, executableExists } from '../utils/process.js';
9
+ /**
10
+ * Extracts a tar.xz archive.
11
+ * @param archivePath - Path to the archive
12
+ * @param destDir - Destination directory
13
+ */
14
+ export async function extractTarXz(archivePath, destDir) {
15
+ if (!(await executableExists('tar'))) {
16
+ throw new ExtractionError(archivePath, new Error('The "tar" command was not found. Please install tar (or ensure it is on your PATH) and try again.'));
17
+ }
18
+ await ensureDir(destDir);
19
+ const result = await exec('tar', ['-xf', archivePath, '-C', destDir]);
20
+ if (result.exitCode !== 0) {
21
+ throw new ExtractionError(archivePath, new Error(`tar exited with code ${result.exitCode}:\n${result.stderr}`));
22
+ }
23
+ }
24
+ /**
25
+ * Gets the Firefox version from an existing source directory.
26
+ * @param engineDir - Path to the engine directory
27
+ * @returns Firefox version string
28
+ */
29
+ export async function getFirefoxVersion(engineDir) {
30
+ const versionPath = join(engineDir, 'browser', 'config', 'version.txt');
31
+ if (!(await pathExists(versionPath))) {
32
+ return undefined;
33
+ }
34
+ const { readText } = await import('../utils/fs.js');
35
+ const version = await readText(versionPath);
36
+ return version.trim();
37
+ }
38
+ /**
39
+ * Formats bytes into a human-readable string.
40
+ * @param bytes - Number of bytes
41
+ * @returns Formatted string (e.g., "1.5 GB")
42
+ */
43
+ export function formatBytes(bytes) {
44
+ const units = ['B', 'KB', 'MB', 'GB'];
45
+ let size = bytes;
46
+ let unitIndex = 0;
47
+ while (size >= 1024 && unitIndex < units.length - 1) {
48
+ size /= 1024;
49
+ unitIndex++;
50
+ }
51
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
52
+ }
53
+ //# sourceMappingURL=firefox-extract.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Firefox source download, extraction, and cache management.
3
+ *
4
+ * Re-exports from focused sub-modules and provides the top-level
5
+ * {@link downloadFirefoxSource} orchestrator.
6
+ */
7
+ import type { FirefoxProduct } from '../types/config.js';
8
+ import type { ProgressCallback } from './firefox-download.js';
9
+ export { resolveArchive } from './firefox-archive.js';
10
+ export type { ProgressCallback } from './firefox-download.js';
11
+ export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
12
+ /**
13
+ * Gets the download URL for a Firefox source tarball.
14
+ * @param version - Firefox version (e.g., "140.0esr")
15
+ * @param product - Firefox product type
16
+ * @returns Full URL to the source tarball
17
+ */
18
+ export declare function getDownloadUrl(version: string, product?: FirefoxProduct): string;
19
+ /**
20
+ * Gets the filename for a Firefox source tarball.
21
+ * @param version - Firefox version
22
+ * @param product - Firefox product type
23
+ * @returns Tarball filename
24
+ */
25
+ export declare function getTarballFilename(version: string, product?: FirefoxProduct): string;
26
+ /**
27
+ * Downloads and extracts Firefox source.
28
+ * @param version - Firefox version to download
29
+ * @param product - Firefox product type
30
+ * @param destDir - Destination directory for extracted source
31
+ * @param cacheDir - Directory to store downloaded tarball
32
+ * @param onProgress - Optional progress callback
33
+ */
34
+ export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Firefox source download, extraction, and cache management.
4
+ *
5
+ * Re-exports from focused sub-modules and provides the top-level
6
+ * {@link downloadFirefoxSource} orchestrator.
7
+ */
8
+ import { randomUUID } from 'node:crypto';
9
+ import { rename } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import { ensureDir, removeDir } from '../utils/fs.js';
12
+ import { resolveArchive } from './firefox-archive.js';
13
+ import { ensureCachedArchive, invalidateArchiveCache } from './firefox-cache.js';
14
+ import { extractTarXz } from './firefox-extract.js';
15
+ // ── Re-exports (preserve public API) ──
16
+ export { resolveArchive } from './firefox-archive.js';
17
+ export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
18
+ /**
19
+ * Gets the download URL for a Firefox source tarball.
20
+ * @param version - Firefox version (e.g., "140.0esr")
21
+ * @param product - Firefox product type
22
+ * @returns Full URL to the source tarball
23
+ */
24
+ export function getDownloadUrl(version, product = 'firefox') {
25
+ return resolveArchive(version, product).url;
26
+ }
27
+ /**
28
+ * Gets the filename for a Firefox source tarball.
29
+ * @param version - Firefox version
30
+ * @param product - Firefox product type
31
+ * @returns Tarball filename
32
+ */
33
+ export function getTarballFilename(version, product = 'firefox') {
34
+ return resolveArchive(version, product).filename;
35
+ }
36
+ /**
37
+ * Downloads and extracts Firefox source.
38
+ * @param version - Firefox version to download
39
+ * @param product - Firefox product type
40
+ * @param destDir - Destination directory for extracted source
41
+ * @param cacheDir - Directory to store downloaded tarball
42
+ * @param onProgress - Optional progress callback
43
+ */
44
+ export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress) {
45
+ const archive = resolveArchive(version, product);
46
+ const tarballPath = join(cacheDir, archive.filename);
47
+ // Ensure cache directory exists
48
+ await ensureDir(cacheDir);
49
+ await ensureCachedArchive(archive, cacheDir, onProgress);
50
+ // Extract to a unique temporary directory so concurrent downloads for
51
+ // the same destination do not clobber each other.
52
+ const tempDir = `${destDir}.tmp-${randomUUID()}`;
53
+ try {
54
+ await extractTarXz(tarballPath, tempDir);
55
+ }
56
+ catch (error) {
57
+ await removeDir(tempDir);
58
+ await invalidateArchiveCache(archive, cacheDir);
59
+ throw error;
60
+ }
61
+ // Firefox source extracts to a subdirectory (e.g., firefox-140.0/)
62
+ // Find it dynamically since ESR versions may have different naming
63
+ const { readdir } = await import('node:fs/promises');
64
+ const entries = await readdir(tempDir, { withFileTypes: true });
65
+ const extractedSubdir = entries.find((entry) => entry.isDirectory() && entry.name.startsWith('firefox-'));
66
+ if (extractedSubdir) {
67
+ const extractedDir = join(tempDir, extractedSubdir.name);
68
+ await removeDir(destDir);
69
+ await rename(extractedDir, destDir);
70
+ await removeDir(tempDir);
71
+ }
72
+ else {
73
+ // If no subdirectory, the temp dir is the source
74
+ await removeDir(destDir);
75
+ await rename(tempDir, destDir);
76
+ }
77
+ }
78
+ //# sourceMappingURL=firefox.js.map
@@ -0,0 +1,21 @@
1
+ import type { CustomComponentConfig, DryRunAction, OverrideComponentConfig, StepError } from '../types/furnace.js';
2
+ import { type RollbackJournal } from './furnace-rollback.js';
3
+ /** Computes stable checksums for the source files that define a component. */
4
+ export declare function computeComponentChecksums(componentDir: string): Promise<Record<string, string>>;
5
+ /** Compares current component file checksums against the previously recorded state. */
6
+ export declare function hasComponentChanged(componentDir: string, previousChecksums: Record<string, string>): Promise<boolean>;
7
+ /** Applies a custom component into the engine tree and captures registration step errors. */
8
+ export declare function applyCustomComponent(engineDir: string, name: string, componentDir: string, config: CustomComponentConfig, dryRun?: boolean, rollbackJournal?: RollbackJournal): Promise<{
9
+ affectedPaths: string[];
10
+ stepErrors: StepError[];
11
+ actions?: DryRunAction[];
12
+ }>;
13
+ /** Applies an override component by copying its matching files onto the engine tree. */
14
+ export declare function applyOverrideComponent(engineDir: string, name: string, componentDir: string, config: OverrideComponentConfig, dryRun?: boolean, rollbackJournal?: RollbackJournal): Promise<{
15
+ affectedPaths: string[];
16
+ actions?: DryRunAction[];
17
+ }>;
18
+ /** Extracts per-component checksums from the flattened state-file checksum map. */
19
+ export declare function extractComponentChecksums(allChecksums: Record<string, string> | undefined, type: string, name: string): Record<string, string>;
20
+ /** Prefixes component checksums so they can be stored in the flattened state format. */
21
+ export declare function prefixChecksums(checksums: Record<string, string>, type: string, name: string): Record<string, string>;