@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,208 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { join } from 'node:path';
3
+ import { confirm, select, text } from '@clack/prompts';
4
+ import { addLicenseHeaderToFile, getLicenseHeader } from '../core/license-headers.js';
5
+ import { findAllPatchesForFiles } from '../core/patch-export.js';
6
+ import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, } from '../core/patch-lint.js';
7
+ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
8
+ import { pathExists, readText } from '../utils/fs.js';
9
+ import { cancel, info, isCancel, warn } from '../utils/logger.js';
10
+ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../utils/validation.js';
11
+ /**
12
+ * Runs the full patch lint pipeline and reports results.
13
+ * Warnings are always displayed. Errors block the export unless skipLint is true.
14
+ *
15
+ * @param engineDir - Engine root directory
16
+ * @param filesAffected - Files touched by the patch
17
+ * @param diffContent - Raw unified diff string
18
+ * @param config - Project configuration
19
+ * @param skipLint - If true, downgrade errors to warnings
20
+ */
21
+ export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint) {
22
+ const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config);
23
+ if (issues.length === 0)
24
+ return;
25
+ const errors = issues.filter((i) => i.severity === 'error');
26
+ const warnings = issues.filter((i) => i.severity === 'warning');
27
+ for (const issue of warnings) {
28
+ warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
29
+ }
30
+ if (errors.length > 0) {
31
+ for (const issue of errors) {
32
+ if (skipLint) {
33
+ warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
34
+ }
35
+ else {
36
+ warn(`ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
37
+ }
38
+ }
39
+ if (!skipLint) {
40
+ throw new GeneralError(`Patch lint found ${errors.length} error(s) that must be fixed before exporting.\n` +
41
+ 'Use --skip-lint to bypass this check.');
42
+ }
43
+ info(`Lint: ${errors.length} error(s) downgraded to warnings (--skip-lint)`);
44
+ }
45
+ const warnCount = warnings.length + (skipLint ? errors.length : 0);
46
+ if (warnCount > 0) {
47
+ info(`Patch lint: ${warnCount} warning(s)`);
48
+ }
49
+ }
50
+ /**
51
+ * Resolves patch metadata interactively or from flags, with shared validation.
52
+ * @param options - Export command options
53
+ * @param isInteractive - Whether interactive prompts are allowed
54
+ * @param commandName - Command name for error/help text
55
+ */
56
+ export async function promptExportPatchMetadata(options, isInteractive, commandName) {
57
+ let patchName = options.name;
58
+ if (patchName) {
59
+ const validationError = validatePatchName(patchName);
60
+ if (validationError) {
61
+ throw new InvalidArgumentError(validationError, '--name');
62
+ }
63
+ }
64
+ if (!patchName && !isInteractive) {
65
+ throw new InvalidArgumentError('The --name flag is required in non-interactive mode', `Use: fireforge ${commandName} ${commandName === 'export' ? '<paths...> ' : ''}--name "my-patch-name" --category ui`);
66
+ }
67
+ if (!patchName) {
68
+ const nameResult = await text({
69
+ message: 'Enter a name for this patch:',
70
+ placeholder: commandName === 'export' ? 'my-change' : 'my-changes',
71
+ validate: (value) => validatePatchName((value ?? '').trim()),
72
+ });
73
+ if (isCancel(nameResult)) {
74
+ cancel('Export cancelled');
75
+ return null;
76
+ }
77
+ patchName = String(nameResult).trim();
78
+ }
79
+ let category = options.category;
80
+ if (category) {
81
+ if (!isValidPatchCategory(category)) {
82
+ throw new InvalidArgumentError(`Invalid category. Must be one of: ${PATCH_CATEGORIES.join(', ')}`, '--category');
83
+ }
84
+ }
85
+ else if (!isInteractive) {
86
+ throw new InvalidArgumentError('The --category flag is required in non-interactive mode', `Use: fireforge ${commandName} ${commandName === 'export' ? '<paths...> ' : ''}--name "name" --category <${PATCH_CATEGORIES.join('|')}>`);
87
+ }
88
+ else {
89
+ const categoryResult = await select({
90
+ message: 'Select a category for this patch:',
91
+ options: [
92
+ { value: 'branding', label: 'branding - Logo, icons, names, about pages' },
93
+ { value: 'ui', label: 'ui - User interface changes' },
94
+ { value: 'privacy', label: 'privacy - Telemetry, tracking, data collection' },
95
+ { value: 'security', label: 'security - Security hardening, policies' },
96
+ { value: 'infra', label: 'infra - Build system, tooling, CI, configuration' },
97
+ ],
98
+ });
99
+ if (isCancel(categoryResult)) {
100
+ cancel('Export cancelled');
101
+ return null;
102
+ }
103
+ category = categoryResult;
104
+ }
105
+ let description = options.description ?? '';
106
+ if (!description && isInteractive) {
107
+ const descResult = await text({
108
+ message: 'Enter a description (optional):',
109
+ placeholder: 'Brief description of what this patch does',
110
+ });
111
+ if (!isCancel(descResult)) {
112
+ description = String(descResult);
113
+ }
114
+ }
115
+ return {
116
+ patchName,
117
+ selectedCategory: category,
118
+ description,
119
+ };
120
+ }
121
+ /**
122
+ * Confirms whether an export may supersede existing patches.
123
+ * @param patchesDir - Patches directory
124
+ * @param filesAffected - Files touched by the pending export
125
+ * @param supersede - Explicit supersede flag from CLI options
126
+ * @param isInteractive - Whether interactive prompts are allowed
127
+ * @param s - Active spinner handle to stop before prompting
128
+ */
129
+ export async function confirmSupersedePatches(patchesDir, filesAffected, supersede, isInteractive, s) {
130
+ const wouldSupersede = await findAllPatchesForFiles(patchesDir, filesAffected);
131
+ if (wouldSupersede.length === 0 || supersede) {
132
+ return true;
133
+ }
134
+ s.stop();
135
+ const count = wouldSupersede.length;
136
+ warn(`This export would supersede ${count} existing patch${count === 1 ? '' : 'es'}:`);
137
+ for (const patch of wouldSupersede) {
138
+ warn(` - ${patch.filename}`);
139
+ }
140
+ if (!isInteractive) {
141
+ throw new GeneralError(`Refusing to supersede ${count} patch${count === 1 ? '' : 'es'} in non-interactive mode. ` +
142
+ 'Use --supersede to confirm, or use "fireforge re-export" to update existing patches in place.');
143
+ }
144
+ const confirmed = await confirm({
145
+ message: `Supersede ${count} patch${count === 1 ? '' : 'es'}? This cannot be undone.`,
146
+ initialValue: false,
147
+ });
148
+ if (isCancel(confirmed) || !confirmed) {
149
+ cancel('Export cancelled');
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ /**
155
+ * Detects new files missing license headers and offers to add them.
156
+ *
157
+ * In interactive mode the user is prompted before any files are modified.
158
+ * In non-interactive mode the function is a no-op — the existing lint error
159
+ * will block the export instead.
160
+ *
161
+ * @param engineDir - Absolute path to engine directory
162
+ * @param diffContent - Current unified diff
163
+ * @param config - Project configuration
164
+ * @param isInteractive - Whether interactive prompts are available
165
+ * @returns true if files were modified on disk (caller must regenerate diff)
166
+ */
167
+ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isInteractive) {
168
+ const license = config.license ?? 'MPL-2.0';
169
+ const newFiles = detectNewFilesInDiff(diffContent);
170
+ if (newFiles.size === 0)
171
+ return false;
172
+ const filesToFix = [];
173
+ for (const file of newFiles) {
174
+ const style = commentStyleForFile(file);
175
+ if (!style)
176
+ continue;
177
+ const filePath = join(engineDir, file);
178
+ if (!(await pathExists(filePath)))
179
+ continue;
180
+ const content = await readText(filePath);
181
+ const expectedHeader = getLicenseHeader(license, style);
182
+ if (!content.startsWith(expectedHeader)) {
183
+ filesToFix.push(file);
184
+ }
185
+ }
186
+ if (filesToFix.length === 0)
187
+ return false;
188
+ if (!isInteractive)
189
+ return false;
190
+ const fileList = filesToFix.map((f) => ` - ${f}`).join('\n');
191
+ info(`${filesToFix.length} new file(s) missing the ${license} license header:\n${fileList}`);
192
+ const confirmed = await confirm({
193
+ message: `Add ${license} headers to ${filesToFix.length} file(s)?`,
194
+ initialValue: true,
195
+ });
196
+ if (isCancel(confirmed) || !confirmed)
197
+ return false;
198
+ for (const file of filesToFix) {
199
+ const style = commentStyleForFile(file);
200
+ if (!style)
201
+ continue;
202
+ const filePath = join(engineDir, file);
203
+ await addLicenseHeaderToFile(filePath, license, style);
204
+ info(`Added ${license} header to ${file}`);
205
+ }
206
+ return true;
207
+ }
208
+ //# sourceMappingURL=export-shared.js.map
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ import type { ExportOptions } from '../types/commands/index.js';
4
+ /**
5
+ * Runs the export command to export file changes as a patch.
6
+ * Accepts one or more file/directory paths and bundles them into a single patch.
7
+ * @param projectRoot - Root directory of the project
8
+ * @param files - File or directory paths to export (relative to engine/)
9
+ * @param options - Export options
10
+ */
11
+ export declare function exportCommand(projectRoot: string, files: string[], options: ExportOptions): Promise<void>;
12
+ /** Registers the export command on the CLI program. */
13
+ export declare function registerExport(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -0,0 +1,178 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { Option } from 'commander';
5
+ import { getProjectPaths, loadConfig } from '../core/config.js';
6
+ import { getStatusWithCodes, isGitRepository } from '../core/git.js';
7
+ import { generateBinaryFilePatch, generateFullFilePatch } from '../core/git-diff.js';
8
+ import { isBinaryFile } from '../core/git-file-ops.js';
9
+ import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } from '../core/git-status.js';
10
+ import { extractAffectedFiles } from '../core/patch-apply.js';
11
+ import { commitExportedPatch } from '../core/patch-export.js';
12
+ import { GeneralError } from '../errors/base.js';
13
+ import { toError } from '../utils/errors.js';
14
+ import { ensureDir, pathExists } from '../utils/fs.js';
15
+ import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
16
+ import { pickDefined } from '../utils/options.js';
17
+ import { PATCH_CATEGORIES } from '../utils/validation.js';
18
+ import { autoFixLicenseHeaders, confirmSupersedePatches, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
19
+ async function collectExportFiles(paths, files) {
20
+ const collectedFiles = new Set();
21
+ let fileStatuses;
22
+ let untrackedFiles;
23
+ for (const inputPath of files) {
24
+ const fullInputPath = join(paths.engine, inputPath);
25
+ let isDirectory = false;
26
+ try {
27
+ const fileStat = await stat(fullInputPath);
28
+ isDirectory = fileStat.isDirectory();
29
+ }
30
+ catch (error) {
31
+ verbose(`Treating ${inputPath} as a file because directory stat failed: ${toError(error).message}`);
32
+ }
33
+ if (isDirectory) {
34
+ const dirPath = inputPath.endsWith('/') ? inputPath.slice(0, -1) : inputPath;
35
+ const modifiedFiles = await getModifiedFilesInDir(paths.engine, dirPath);
36
+ const dirUntrackedFiles = await getUntrackedFilesInDir(paths.engine, dirPath);
37
+ for (const f of modifiedFiles)
38
+ collectedFiles.add(f);
39
+ for (const f of dirUntrackedFiles)
40
+ collectedFiles.add(f);
41
+ }
42
+ else {
43
+ if (inputPath.endsWith('/')) {
44
+ throw new GeneralError(`"${inputPath}" is not a valid file or directory.`);
45
+ }
46
+ if (!fileStatuses) {
47
+ fileStatuses = await getStatusWithCodes(paths.engine);
48
+ }
49
+ const fileStatus = fileStatuses.find((s) => s.file === inputPath);
50
+ if (!fileStatus) {
51
+ if (!untrackedFiles) {
52
+ untrackedFiles = await getUntrackedFiles(paths.engine);
53
+ }
54
+ if (!untrackedFiles.includes(inputPath)) {
55
+ throw new GeneralError(`File "${inputPath}" has no changes to export.\n\n` +
56
+ 'Run "fireforge status" to see modified files.');
57
+ }
58
+ }
59
+ collectedFiles.add(inputPath);
60
+ }
61
+ }
62
+ return [...collectedFiles].sort();
63
+ }
64
+ async function generatePatchDiff(engineDir, allFiles) {
65
+ const diffs = [];
66
+ for (const file of allFiles) {
67
+ const fullPath = join(engineDir, file);
68
+ const isExistingBinary = (await pathExists(fullPath)) && (await isBinaryFile(engineDir, file));
69
+ const diff = isExistingBinary
70
+ ? await generateBinaryFilePatch(engineDir, file)
71
+ : await generateFullFilePatch(engineDir, file);
72
+ if (isExistingBinary) {
73
+ if (diff.trim()) {
74
+ info(`Including binary file: ${file}`);
75
+ }
76
+ else {
77
+ warn(`Skipping binary file with no diff: ${file}`);
78
+ }
79
+ }
80
+ if (diff.trim()) {
81
+ diffs.push(diff);
82
+ }
83
+ }
84
+ return diffs.join('\n');
85
+ }
86
+ /**
87
+ * Runs the export command to export file changes as a patch.
88
+ * Accepts one or more file/directory paths and bundles them into a single patch.
89
+ * @param projectRoot - Root directory of the project
90
+ * @param files - File or directory paths to export (relative to engine/)
91
+ * @param options - Export options
92
+ */
93
+ export async function exportCommand(projectRoot, files, options) {
94
+ intro('FireForge Export');
95
+ const paths = getProjectPaths(projectRoot);
96
+ // Check if engine exists
97
+ if (!(await pathExists(paths.engine))) {
98
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
99
+ }
100
+ // Check if it's a git repository
101
+ if (!(await isGitRepository(paths.engine))) {
102
+ throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
103
+ }
104
+ const allFiles = await collectExportFiles(paths, files);
105
+ if (allFiles.length === 0) {
106
+ const pathList = files.join(', ');
107
+ throw new GeneralError(`Paths "${pathList}" have no changes to export.\n\n` +
108
+ 'Run "fireforge status" to see modified files.');
109
+ }
110
+ let diff = await generatePatchDiff(paths.engine, allFiles);
111
+ if (!diff.trim()) {
112
+ throw new GeneralError('The specified paths have no diff content to export.');
113
+ }
114
+ // Ensure patches directory exists
115
+ await ensureDir(paths.patches);
116
+ const config = await loadConfig(projectRoot);
117
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
118
+ // Auto-fix missing license headers on new files (interactive only)
119
+ const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive);
120
+ if (headersAdded) {
121
+ diff = await generatePatchDiff(paths.engine, allFiles);
122
+ }
123
+ const metadata = await promptExportPatchMetadata(options, isInteractive, 'export');
124
+ if (!metadata)
125
+ return;
126
+ const { patchName, selectedCategory, description } = metadata;
127
+ const s = spinner('Exporting patch...');
128
+ try {
129
+ // Extract affected files from diff
130
+ const filesAffected = extractAffectedFiles(diff);
131
+ await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint);
132
+ // Check how many existing patches would be superseded
133
+ const shouldProceed = await confirmSupersedePatches(paths.patches, filesAffected, options.supersede, isInteractive, s);
134
+ if (!shouldProceed)
135
+ return;
136
+ const { patchFilename, superseded } = await commitExportedPatch({
137
+ patchesDir: paths.patches,
138
+ category: selectedCategory,
139
+ name: patchName,
140
+ description,
141
+ diff,
142
+ filesAffected,
143
+ sourceEsrVersion: config.firefox.version,
144
+ });
145
+ for (const oldPatch of superseded) {
146
+ info(`Superseded: ${oldPatch.filename}`);
147
+ }
148
+ s.stop(`Exported to ${patchFilename}`);
149
+ info(`\nPatch saved to: patches/${patchFilename}`);
150
+ if (filesAffected.length > 0) {
151
+ info(`Files affected: ${filesAffected.join(', ')}`);
152
+ }
153
+ outro('Export complete');
154
+ }
155
+ catch (error) {
156
+ s.error('Export failed');
157
+ throw error;
158
+ }
159
+ }
160
+ /** Registers the export command on the CLI program. */
161
+ export function registerExport(program, { getProjectRoot, withErrorHandling }) {
162
+ program
163
+ .command('export <paths...>')
164
+ .description('Export new changes as a patch (use re-export to update existing patches)')
165
+ .option('-n, --name <name>', 'Name for the patch')
166
+ .addOption(new Option('-c, --category <category>', 'Patch category').choices([...PATCH_CATEGORIES]))
167
+ .option('-d, --description <desc>', 'Description of the patch')
168
+ .option('--supersede', 'Allow superseding multiple existing patches')
169
+ .option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
170
+ .action(withErrorHandling(async (paths, options) => {
171
+ const { category, ...rest } = options;
172
+ await exportCommand(getProjectRoot(), paths, {
173
+ ...pickDefined(rest),
174
+ ...(category !== undefined ? { category: category } : {}),
175
+ });
176
+ }));
177
+ }
178
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,7 @@
1
+ import type { FurnaceApplyOptions } from '../../types/commands/index.js';
2
+ /**
3
+ * Runs the furnace apply command to apply all components to the engine.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param options - Apply options
6
+ */
7
+ export declare function furnaceApplyCommand(projectRoot: string, options?: FurnaceApplyOptions): Promise<void>;
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { getProjectPaths } from '../../core/config.js';
3
+ import { applyAllComponents } from '../../core/furnace-apply.js';
4
+ import { furnaceConfigExists, loadFurnaceConfig } from '../../core/furnace-config.js';
5
+ import { FurnaceError } from '../../errors/furnace.js';
6
+ import { pathExists } from '../../utils/fs.js';
7
+ import { error, info, intro, outro, spinner, success, warn } from '../../utils/logger.js';
8
+ /**
9
+ * Runs the furnace apply command to apply all components to the engine.
10
+ * @param projectRoot - Root directory of the project
11
+ * @param options - Apply options
12
+ */
13
+ export async function furnaceApplyCommand(projectRoot, options = {}) {
14
+ intro('Furnace Apply');
15
+ // Verify engine exists
16
+ const paths = getProjectPaths(projectRoot);
17
+ if (!(await pathExists(paths.engine))) {
18
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
19
+ }
20
+ // Load furnace config
21
+ if (!(await furnaceConfigExists(projectRoot))) {
22
+ throw new FurnaceError('No furnace.json found. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
23
+ }
24
+ const config = await loadFurnaceConfig(projectRoot);
25
+ const overrideCount = Object.keys(config.overrides).length;
26
+ const customCount = Object.keys(config.custom).length;
27
+ if (overrideCount === 0 && customCount === 0) {
28
+ info('No components to apply.');
29
+ outro('Done');
30
+ return;
31
+ }
32
+ const dryRun = options.dryRun ?? false;
33
+ const applySpinner = dryRun ? undefined : spinner('Applying components to engine...');
34
+ const result = await applyAllComponents(projectRoot, dryRun);
35
+ if (applySpinner) {
36
+ applySpinner.stop('Components applied');
37
+ }
38
+ // Report applied
39
+ for (const applied of result.applied) {
40
+ const prefix = dryRun ? '[dry-run] Would apply' : '';
41
+ const label = dryRun
42
+ ? `${prefix} ${applied.name} (${applied.type}) → ${applied.filesAffected.length} files`
43
+ : `${applied.name} (${applied.type}) → ${applied.filesAffected.length} files`;
44
+ if (dryRun) {
45
+ info(label);
46
+ }
47
+ else {
48
+ success(label);
49
+ }
50
+ if (applied.stepErrors && applied.stepErrors.length > 0) {
51
+ for (const stepErr of applied.stepErrors) {
52
+ warn(`${applied.name}: [${stepErr.step}] ${stepErr.error}`);
53
+ }
54
+ }
55
+ }
56
+ // Report skipped
57
+ for (const skipped of result.skipped) {
58
+ info(`${skipped.name} — ${skipped.reason}`);
59
+ }
60
+ // Report errors
61
+ for (const err of result.errors) {
62
+ error(`${err.name} — ${err.error}`);
63
+ }
64
+ const stepFailureCount = dryRun
65
+ ? 0
66
+ : result.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
67
+ const totalApplyFailures = result.errors.length + stepFailureCount;
68
+ if (totalApplyFailures > 0) {
69
+ throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
70
+ }
71
+ const appliedCount = result.applied.length;
72
+ const skippedCount = result.skipped.length;
73
+ if (dryRun) {
74
+ outro(`Dry run complete — would apply ${appliedCount}, skip ${skippedCount}`);
75
+ }
76
+ else {
77
+ outro(`Applied ${appliedCount}, skipped ${skippedCount}`);
78
+ }
79
+ }
80
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,8 @@
1
+ import type { FurnaceCreateOptions } from '../../types/commands/index.js';
2
+ /**
3
+ * Runs the furnace create command to scaffold a new custom component.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param name - Optional component tag name (prompted if not provided)
6
+ * @param options - CLI options for non-interactive mode
7
+ */
8
+ export declare function furnaceCreateCommand(projectRoot: string, name?: string, options?: FurnaceCreateOptions): Promise<void>;