@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,152 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Project state file management (.fireforge/state.json).
4
+ */
5
+ import { ConfigError } from '../errors/config.js';
6
+ import { toError } from '../utils/errors.js';
7
+ import { pathExists, readJson, writeJson } from '../utils/fs.js';
8
+ import { warn } from '../utils/logger.js';
9
+ import { isObject, isString } from '../utils/validation.js';
10
+ import { getProjectPaths } from './config-paths.js';
11
+ import { quarantineStateFile, withStateFileLock } from './state-file.js';
12
+ function sanitizeProjectState(data) {
13
+ if (!isObject(data)) {
14
+ return {
15
+ state: {},
16
+ issues: ['the root value must be a JSON object'],
17
+ recoveredFields: [],
18
+ };
19
+ }
20
+ const state = {};
21
+ const issues = [];
22
+ const recoveredFields = [];
23
+ const stringFields = [
24
+ 'brand',
25
+ 'buildMode',
26
+ 'lastBuild',
27
+ 'downloadedVersion',
28
+ 'baseCommit',
29
+ ];
30
+ for (const key of stringFields) {
31
+ const value = data[key];
32
+ if (value === undefined) {
33
+ continue;
34
+ }
35
+ if (!isString(value)) {
36
+ issues.push(`field "${key}" must be a string`);
37
+ continue;
38
+ }
39
+ if (key === 'buildMode' && !['dev', 'debug', 'release'].includes(value)) {
40
+ issues.push('field "buildMode" must be one of: dev, debug, release');
41
+ continue;
42
+ }
43
+ if (key === 'buildMode') {
44
+ state.buildMode = value;
45
+ }
46
+ else {
47
+ state[key] = value;
48
+ }
49
+ recoveredFields.push(key);
50
+ }
51
+ const pendingResolution = data['pendingResolution'];
52
+ if (pendingResolution !== undefined) {
53
+ if (isObject(pendingResolution) &&
54
+ isString(pendingResolution['patchFilename']) &&
55
+ isString(pendingResolution['originalError'])) {
56
+ state.pendingResolution = {
57
+ patchFilename: pendingResolution['patchFilename'],
58
+ originalError: pendingResolution['originalError'],
59
+ };
60
+ recoveredFields.push('pendingResolution');
61
+ }
62
+ else {
63
+ issues.push('field "pendingResolution" must be an object with string fields "patchFilename" and "originalError"');
64
+ }
65
+ }
66
+ return { state, issues, recoveredFields };
67
+ }
68
+ /**
69
+ * Validates a parsed project state object and returns a typed FireForgeState.
70
+ * @param data - Parsed JSON state data
71
+ * @returns Validated FireForgeState
72
+ */
73
+ export function validateFireForgeState(data) {
74
+ const result = sanitizeProjectState(data);
75
+ if (result.issues.length > 0) {
76
+ throw new ConfigError(`Invalid FireForge state: ${result.issues.join('; ')}`);
77
+ }
78
+ return result.state;
79
+ }
80
+ async function recoverInvalidProjectState(statePath, result, alreadyLocked = false) {
81
+ const recover = async () => {
82
+ const quarantinedFile = await quarantineStateFile(statePath);
83
+ if (result.recoveredFields.length > 0) {
84
+ await writeJson(statePath, result.state);
85
+ }
86
+ const quarantineMessage = quarantinedFile
87
+ ? ` Quarantined the original file as ${quarantinedFile}.`
88
+ : '';
89
+ const recoveryMessage = result.recoveredFields.length > 0
90
+ ? ` Recovered valid field${result.recoveredFields.length === 1 ? '' : 's'}: ${result.recoveredFields.join(', ')}.`
91
+ : ' No valid state fields could be recovered; using defaults.';
92
+ warn(`State file (.fireforge/state.json) was invalid: ${result.issues.join('; ')}.${recoveryMessage}${quarantineMessage} ` +
93
+ 'Run "fireforge doctor" to check project health.');
94
+ return result.state;
95
+ };
96
+ return alreadyLocked ? recover() : withStateFileLock(statePath, recover);
97
+ }
98
+ async function loadStateFromPath(statePath, alreadyLocked = false) {
99
+ if (!(await pathExists(statePath))) {
100
+ return {};
101
+ }
102
+ try {
103
+ const data = await readJson(statePath);
104
+ const result = sanitizeProjectState(data);
105
+ if (result.issues.length === 0) {
106
+ return result.state;
107
+ }
108
+ return await recoverInvalidProjectState(statePath, result, alreadyLocked);
109
+ }
110
+ catch (error) {
111
+ return await recoverInvalidProjectState(statePath, {
112
+ state: {},
113
+ issues: [`the file could not be parsed: ${toError(error).message}`],
114
+ recoveredFields: [],
115
+ }, alreadyLocked);
116
+ }
117
+ }
118
+ /**
119
+ * Loads the fireforge state, or returns defaults if it doesn't exist.
120
+ * @param root - Root directory of the project
121
+ * @returns FireForge state
122
+ */
123
+ export async function loadState(root) {
124
+ const paths = getProjectPaths(root);
125
+ return loadStateFromPath(paths.state);
126
+ }
127
+ /**
128
+ * Saves the fireforge state.
129
+ * @param root - Root directory of the project
130
+ * @param state - State to save
131
+ */
132
+ export async function saveState(root, state) {
133
+ const paths = getProjectPaths(root);
134
+ const validatedState = validateFireForgeState(state);
135
+ await withStateFileLock(paths.state, async () => {
136
+ await writeJson(paths.state, validatedState);
137
+ });
138
+ }
139
+ /**
140
+ * Updates specific fields in the fireforge state.
141
+ * @param root - Root directory of the project
142
+ * @param updates - Fields to update, or a transactional updater function
143
+ */
144
+ export async function updateState(root, updates) {
145
+ const paths = getProjectPaths(root);
146
+ await withStateFileLock(paths.state, async () => {
147
+ const current = await loadStateFromPath(paths.state, true);
148
+ const nextState = typeof updates === 'function' ? updates(current) : { ...current, ...updates };
149
+ await writeJson(paths.state, validateFireForgeState(nextState));
150
+ });
151
+ }
152
+ //# sourceMappingURL=config-state.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Config schema validation for fireforge.json.
3
+ */
4
+ import type { FireForgeConfig } from '../types/config.js';
5
+ /**
6
+ * Validates a raw config object and returns a typed FireForgeConfig.
7
+ * @param data - Raw data to validate
8
+ * @returns Validated FireForgeConfig
9
+ * @throws Error if validation fails
10
+ */
11
+ export declare function validateConfig(data: unknown): FireForgeConfig;
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Config schema validation for fireforge.json.
4
+ */
5
+ import { ConfigError } from '../errors/config.js';
6
+ import { verbose } from '../utils/logger.js';
7
+ import { parseObject } from '../utils/parse.js';
8
+ import { isContainedRelativePath } from '../utils/paths.js';
9
+ import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
10
+ import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
11
+ /**
12
+ * Validates a raw config object and returns a typed FireForgeConfig.
13
+ * @param data - Raw data to validate
14
+ * @returns Validated FireForgeConfig
15
+ * @throws Error if validation fails
16
+ */
17
+ export function validateConfig(data) {
18
+ let rec;
19
+ try {
20
+ rec = parseObject(data, 'Config');
21
+ }
22
+ catch {
23
+ throw new ConfigError('Config must be an object');
24
+ }
25
+ // Required string fields
26
+ const name = requireConfigString(rec, 'name');
27
+ const vendor = requireConfigString(rec, 'vendor');
28
+ const appId = requireConfigString(rec, 'appId');
29
+ const binaryName = requireConfigString(rec, 'binaryName');
30
+ if (binaryName.includes('..') || binaryName.includes('/') || binaryName.includes('\\')) {
31
+ throw new ConfigError('Config field "binaryName" must not contain path separators or ".."');
32
+ }
33
+ if (!isValidAppId(appId)) {
34
+ throw new ConfigError('Config field "appId" must be a valid reverse-domain identifier (e.g., "org.example.browser")');
35
+ }
36
+ // Firefox config
37
+ let firefoxRec;
38
+ try {
39
+ firefoxRec = rec.object('firefox');
40
+ }
41
+ catch {
42
+ throw new ConfigError('Config field "firefox" must be an object');
43
+ }
44
+ const firefoxVersion = requireConfigString(firefoxRec, 'version', 'firefox.version');
45
+ if (!isValidFirefoxVersion(firefoxVersion)) {
46
+ throw new ConfigError('Config field "firefox.version" must be a valid Firefox version (e.g., "145.0")');
47
+ }
48
+ const firefoxProduct = requireConfigString(firefoxRec, 'product', 'firefox.product');
49
+ const validProducts = ['firefox', 'firefox-esr', 'firefox-beta'];
50
+ if (!validProducts.includes(firefoxProduct)) {
51
+ throw new ConfigError(`Config field "firefox.product" must be one of: ${validProducts.join(', ')}`);
52
+ }
53
+ // Cross-field validation: product and version must be compatible
54
+ const compatError = validateFirefoxProductVersionCompatibility(firefoxVersion, firefoxProduct);
55
+ if (compatError) {
56
+ throw new ConfigError(compatError);
57
+ }
58
+ // Optional configs
59
+ const config = {
60
+ name,
61
+ vendor,
62
+ appId,
63
+ binaryName,
64
+ firefox: {
65
+ version: firefoxVersion,
66
+ product: firefoxProduct,
67
+ },
68
+ };
69
+ // Build
70
+ const buildRec = optionalConfigObject(rec, 'build');
71
+ if (buildRec) {
72
+ config.build = {};
73
+ const jobs = buildRec.raw('jobs');
74
+ if (jobs !== undefined) {
75
+ if (typeof jobs !== 'number' || !Number.isInteger(jobs) || jobs <= 0) {
76
+ throw new ConfigError('Config field "build.jobs" must be a positive integer');
77
+ }
78
+ config.build.jobs = jobs;
79
+ }
80
+ }
81
+ // Wire
82
+ const wireRec = optionalConfigObject(rec, 'wire');
83
+ if (wireRec) {
84
+ config.wire = {};
85
+ const subscriptDir = optionalConfigString(wireRec, 'subscriptDir', 'wire.subscriptDir');
86
+ if (subscriptDir !== undefined) {
87
+ if (!isContainedRelativePath(subscriptDir)) {
88
+ throw new ConfigError('Config field "wire.subscriptDir" must stay within engine/');
89
+ }
90
+ config.wire.subscriptDir = subscriptDir;
91
+ }
92
+ }
93
+ // License
94
+ const licenseRaw = rec.raw('license');
95
+ if (licenseRaw !== undefined) {
96
+ if (typeof licenseRaw !== 'string') {
97
+ throw new ConfigError('Config field "license" must be a string');
98
+ }
99
+ if (!isValidProjectLicense(licenseRaw)) {
100
+ throw new ConfigError(`Config field "license" must be one of: ${PROJECT_LICENSES.join(', ')}`);
101
+ }
102
+ config.license = licenseRaw;
103
+ }
104
+ // Warn on unknown root keys
105
+ const knownRootKeys = new Set(SUPPORTED_CONFIG_ROOT_KEYS);
106
+ for (const key of rec.keys()) {
107
+ if (!knownRootKeys.has(key)) {
108
+ verbose(`Unknown config key "${key}" in fireforge.json — it will be ignored.`);
109
+ }
110
+ }
111
+ return config;
112
+ }
113
+ // ── Internal helpers (wrap parseObject errors with ConfigError) ──
114
+ function requireConfigString(rec, key, label) {
115
+ const value = rec.raw(key);
116
+ if (typeof value !== 'string') {
117
+ throw new ConfigError(`Config field "${label ?? key}" must be a string`);
118
+ }
119
+ return value;
120
+ }
121
+ function optionalConfigString(rec, key, label) {
122
+ const value = rec.raw(key);
123
+ if (value === undefined)
124
+ return undefined;
125
+ if (typeof value !== 'string') {
126
+ throw new ConfigError(`Config field "${label}" must be a string`);
127
+ }
128
+ return value;
129
+ }
130
+ function optionalConfigObject(rec, key) {
131
+ const value = rec.raw(key);
132
+ if (value === undefined)
133
+ return undefined;
134
+ try {
135
+ return rec.object(key);
136
+ }
137
+ catch {
138
+ throw new ConfigError(`Config field "${key}" must be an object`);
139
+ }
140
+ }
141
+ //# sourceMappingURL=config-validate.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Project configuration — barrel module.
3
+ *
4
+ * Re-exports from focused sub-modules:
5
+ * config-paths.ts — constants and project path derivation
6
+ * config-validate.ts — fireforge.json schema validation
7
+ * config-mutate.ts — immutable config mutation
8
+ * config-state.ts — state file management
9
+ */
10
+ import type { FireForgeConfig } from '../types/config.js';
11
+ export { mutateConfig } from './config-mutate.js';
12
+ export { CONFIG_FILENAME, CONFIGS_DIR, ENGINE_DIR, FIREFORGE_DIR, getProjectPaths, PATCHES_DIR, SRC_DIR, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
13
+ export { loadState, saveState, updateState, validateFireForgeState } from './config-state.js';
14
+ export { validateConfig } from './config-validate.js';
15
+ /**
16
+ * Checks if a fireforge.json exists in the given directory.
17
+ * @param root - Root directory to check
18
+ * @returns True if fireforge.json exists
19
+ */
20
+ export declare function configExists(root: string): Promise<boolean>;
21
+ /**
22
+ * Loads and validates the fireforge.json configuration.
23
+ * @param root - Root directory of the project
24
+ * @returns Validated FireForgeConfig
25
+ * @throws Error if config doesn't exist or is invalid
26
+ */
27
+ export declare function loadConfig(root: string): Promise<FireForgeConfig>;
28
+ /**
29
+ * Writes a configuration to fireforge.json.
30
+ * @param root - Root directory of the project
31
+ * @param config - Configuration to write
32
+ */
33
+ export declare function writeConfig(root: string, config: FireForgeConfig): Promise<void>;
34
+ /**
35
+ * Writes a raw config document to fireforge.json.
36
+ * This is used by CLI `config --force`, where callers may intentionally write
37
+ * keys or value shapes outside the validated FireForgeConfig schema.
38
+ */
39
+ export declare function writeConfigDocument(root: string, config: FireForgeConfig | Record<string, unknown>): Promise<void>;
@@ -0,0 +1,70 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Project configuration — barrel module.
4
+ *
5
+ * Re-exports from focused sub-modules:
6
+ * config-paths.ts — constants and project path derivation
7
+ * config-validate.ts — fireforge.json schema validation
8
+ * config-mutate.ts — immutable config mutation
9
+ * config-state.ts — state file management
10
+ */
11
+ import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
12
+ import { toError } from '../utils/errors.js';
13
+ import { pathExists, readJson, writeJson } from '../utils/fs.js';
14
+ import { getProjectPaths } from './config-paths.js';
15
+ import { validateConfig } from './config-validate.js';
16
+ // ---- re-exports ----
17
+ export { mutateConfig } from './config-mutate.js';
18
+ export { CONFIG_FILENAME, CONFIGS_DIR, ENGINE_DIR, FIREFORGE_DIR, getProjectPaths, PATCHES_DIR, SRC_DIR, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
19
+ export { loadState, saveState, updateState, validateFireForgeState } from './config-state.js';
20
+ export { validateConfig } from './config-validate.js';
21
+ // ---- config I/O (stays here because it bridges paths + validation) ----
22
+ /**
23
+ * Checks if a fireforge.json exists in the given directory.
24
+ * @param root - Root directory to check
25
+ * @returns True if fireforge.json exists
26
+ */
27
+ export async function configExists(root) {
28
+ const paths = getProjectPaths(root);
29
+ return pathExists(paths.config);
30
+ }
31
+ /**
32
+ * Loads and validates the fireforge.json configuration.
33
+ * @param root - Root directory of the project
34
+ * @returns Validated FireForgeConfig
35
+ * @throws Error if config doesn't exist or is invalid
36
+ */
37
+ export async function loadConfig(root) {
38
+ const paths = getProjectPaths(root);
39
+ if (!(await pathExists(paths.config))) {
40
+ throw new ConfigNotFoundError(paths.config);
41
+ }
42
+ try {
43
+ const data = await readJson(paths.config);
44
+ return validateConfig(data);
45
+ }
46
+ catch (error) {
47
+ if (error instanceof ConfigError) {
48
+ throw error;
49
+ }
50
+ throw new ConfigError(`Invalid fireforge.json at ${paths.config}: ${toError(error).message}`);
51
+ }
52
+ }
53
+ /**
54
+ * Writes a configuration to fireforge.json.
55
+ * @param root - Root directory of the project
56
+ * @param config - Configuration to write
57
+ */
58
+ export async function writeConfig(root, config) {
59
+ await writeConfigDocument(root, config);
60
+ }
61
+ /**
62
+ * Writes a raw config document to fireforge.json.
63
+ * This is used by CLI `config --force`, where callers may intentionally write
64
+ * keys or value shapes outside the validated FireForgeConfig schema.
65
+ */
66
+ export async function writeConfigDocument(root, config) {
67
+ const paths = getProjectPaths(root);
68
+ await writeJson(paths.config, config);
69
+ }
70
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,11 @@
1
+ export interface FileLockOptions {
2
+ timeoutMs?: number;
3
+ pollMs?: number;
4
+ staleMs?: number;
5
+ onTimeoutMessage?: string;
6
+ onStaleLockMessage?: (ageMs: number) => string | undefined;
7
+ }
8
+ /** Derives the sibling lock-directory path used to guard a file-based resource. */
9
+ export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
10
+ /** Runs an async operation while holding a directory lock, with stale-lock recovery. */
11
+ export declare function withFileLock<T>(lockPath: string, operation: () => Promise<T>, options?: FileLockOptions): Promise<T>;
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { mkdir, rm, stat } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { toError } from '../utils/errors.js';
5
+ import { ensureDir } from '../utils/fs.js';
6
+ import { verbose, warn } from '../utils/logger.js';
7
+ const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
8
+ const DEFAULT_LOCK_POLL_MS = 50;
9
+ const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => {
12
+ setTimeout(resolve, ms);
13
+ });
14
+ }
15
+ /** Derives the sibling lock-directory path used to guard a file-based resource. */
16
+ export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
17
+ return `${filePath}${suffix}`;
18
+ }
19
+ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
20
+ try {
21
+ const lockStat = await stat(lockPath);
22
+ const ageMs = Date.now() - lockStat.mtimeMs;
23
+ if (ageMs <= staleMs) {
24
+ return false;
25
+ }
26
+ const staleMessage = onStaleLockMessage?.(ageMs);
27
+ if (staleMessage) {
28
+ warn(staleMessage);
29
+ }
30
+ await rm(lockPath, { recursive: true, force: true });
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ verbose(`Stale lock check failed for ${lockPath}: ${toError(error).message}`);
35
+ return true;
36
+ }
37
+ }
38
+ /** Runs an async operation while holding a directory lock, with stale-lock recovery. */
39
+ export async function withFileLock(lockPath, operation, options = {}) {
40
+ const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
41
+ const pollMs = options.pollMs ?? DEFAULT_LOCK_POLL_MS;
42
+ const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
43
+ const deadline = Date.now() + timeoutMs;
44
+ let attemptedStaleRecovery = false;
45
+ await ensureDir(dirname(lockPath));
46
+ for (;;) {
47
+ try {
48
+ await mkdir(lockPath);
49
+ break;
50
+ }
51
+ catch (error) {
52
+ const isAlreadyLocked = typeof error === 'object' &&
53
+ error !== null &&
54
+ 'code' in error &&
55
+ typeof error.code === 'string' &&
56
+ error.code === 'EEXIST';
57
+ if (!isAlreadyLocked) {
58
+ throw error;
59
+ }
60
+ if (!attemptedStaleRecovery) {
61
+ attemptedStaleRecovery = true;
62
+ if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
63
+ continue;
64
+ }
65
+ }
66
+ if (Date.now() >= deadline) {
67
+ throw new Error(options.onTimeoutMessage ??
68
+ `Timed out waiting for file lock ${lockPath}. Remove the lock directory if it is stale.`, { cause: error });
69
+ }
70
+ await sleep(pollMs);
71
+ }
72
+ }
73
+ try {
74
+ return await operation();
75
+ }
76
+ finally {
77
+ await rm(lockPath, { recursive: true, force: true });
78
+ }
79
+ }
80
+ //# sourceMappingURL=file-lock.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Archive metadata validation and archive identity resolution.
3
+ */
4
+ import type { FirefoxProduct } from '../types/config.js';
5
+ /**
6
+ * Resolved archive descriptor for URL generation and cache storage.
7
+ */
8
+ export interface ResolvedArchive {
9
+ requestedVersion: string;
10
+ product: FirefoxProduct;
11
+ archiveVersion: string;
12
+ url: string;
13
+ filename: string;
14
+ metadataFilename: string;
15
+ }
16
+ /**
17
+ * Sidecar metadata stored alongside a cached archive.
18
+ */
19
+ export interface ArchiveMetadata {
20
+ requestedVersion: string;
21
+ product: FirefoxProduct;
22
+ archiveVersion: string;
23
+ url: string;
24
+ contentLength?: number | undefined;
25
+ sha256?: string | undefined;
26
+ downloadedAt: string;
27
+ }
28
+ /**
29
+ * Validates raw JSON data as ArchiveMetadata.
30
+ * @param data - Unknown data to validate
31
+ * @returns Validated ArchiveMetadata
32
+ */
33
+ export declare function validateArchiveMetadata(data: unknown): ArchiveMetadata;
34
+ /**
35
+ * Resolves archive identity for URL generation and cache storage.
36
+ * @param version - Requested Firefox version
37
+ * @param product - Firefox product type
38
+ * @returns Resolved archive descriptor
39
+ */
40
+ export declare function resolveArchive(version: string, product?: FirefoxProduct): ResolvedArchive;
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Archive metadata validation and archive identity resolution.
4
+ */
5
+ import { ConfigError } from '../errors/config.js';
6
+ import { parseObject } from '../utils/parse.js';
7
+ import { isValidFirefoxProduct } from '../utils/validation.js';
8
+ /**
9
+ * Base URL for Firefox releases on archive.mozilla.org.
10
+ */
11
+ const ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/firefox/releases';
12
+ /**
13
+ * Validates raw JSON data as ArchiveMetadata.
14
+ * @param data - Unknown data to validate
15
+ * @returns Validated ArchiveMetadata
16
+ */
17
+ export function validateArchiveMetadata(data) {
18
+ const rec = parseObject(data, 'Archive metadata');
19
+ const requestedVersion = rec.string('requestedVersion');
20
+ const product = rec.stringEnum('product', (v) => isValidFirefoxProduct(v), 'a supported Firefox product');
21
+ const archiveVersion = rec.string('archiveVersion');
22
+ const url = rec.string('url');
23
+ const downloadedAt = rec.string('downloadedAt');
24
+ const contentLength = rec.optionalNonNegativeInteger('contentLength');
25
+ const sha256 = rec.optionalString('sha256');
26
+ return {
27
+ requestedVersion,
28
+ product,
29
+ archiveVersion,
30
+ url,
31
+ downloadedAt,
32
+ ...(contentLength !== undefined ? { contentLength } : {}),
33
+ ...(sha256 !== undefined ? { sha256 } : {}),
34
+ };
35
+ }
36
+ /**
37
+ * Resolves archive identity for URL generation and cache storage.
38
+ * @param version - Requested Firefox version
39
+ * @param product - Firefox product type
40
+ * @returns Resolved archive descriptor
41
+ */
42
+ export function resolveArchive(version, product = 'firefox') {
43
+ // Reject versions containing path traversal characters
44
+ if (version.includes('/') || version.includes('..') || version.includes('\\')) {
45
+ throw new ConfigError(`Invalid Firefox version "${version}": contains disallowed characters`, 'firefox.version');
46
+ }
47
+ // ESR status is determined solely by the product field. Config validation
48
+ // ensures product and version are consistent, so we never need to infer
49
+ // ESR from the version string independently.
50
+ const cleanVersion = version.replace(/esr$/i, '');
51
+ const isEsr = product === 'firefox-esr';
52
+ const archiveVersion = isEsr ? `${cleanVersion}esr` : cleanVersion;
53
+ const safeProduct = product.replace(/[^a-z0-9-]/gi, '-');
54
+ return {
55
+ requestedVersion: version,
56
+ product,
57
+ archiveVersion,
58
+ url: `${ARCHIVE_BASE_URL}/${archiveVersion}/source/firefox-${archiveVersion}.source.tar.xz`,
59
+ filename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz`,
60
+ metadataFilename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz.json`,
61
+ };
62
+ }
63
+ //# sourceMappingURL=firefox-archive.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Cache validation, invalidation, and download-to-cache logic.
3
+ */
4
+ import type { ResolvedArchive } from './firefox-archive.js';
5
+ import type { ProgressCallback } from './firefox-download.js';
6
+ /**
7
+ * Computes the SHA-256 hex digest of a file.
8
+ * @param filePath - Path to the file
9
+ */
10
+ export declare function sha256File(filePath: string): Promise<string>;
11
+ /**
12
+ * Ensures a valid cached archive exists, downloading it if needed.
13
+ * @param archive - Resolved archive descriptor
14
+ * @param cacheDir - Cache directory
15
+ * @param onProgress - Optional progress callback
16
+ */
17
+ export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
18
+ /**
19
+ * Removes cached tarball, metadata, and partial download files for an archive.
20
+ * @param archive - Resolved archive descriptor
21
+ * @param cacheDir - Cache directory
22
+ */
23
+ export declare function invalidateArchiveCache(archive: ResolvedArchive, cacheDir: string): Promise<void>;