@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,8 @@
1
+ import type { FurnaceOverrideOptions } from '../../types/commands/index.js';
2
+ /**
3
+ * Runs the furnace override command to fork an existing engine 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 furnaceOverrideCommand(projectRoot: string, name?: string, options?: FurnaceOverrideOptions): Promise<void>;
@@ -0,0 +1,188 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { select, text } from '@clack/prompts';
5
+ import { getProjectPaths, loadConfig } from '../../core/config.js';
6
+ import { ensureFurnaceConfig, getFurnacePaths, writeFurnaceConfig, } from '../../core/furnace-config.js';
7
+ import { getComponentDetails, scanWidgetsDirectory } from '../../core/furnace-scanner.js';
8
+ import { InvalidArgumentError } from '../../errors/base.js';
9
+ import { FurnaceError } from '../../errors/furnace.js';
10
+ import { copyFile, ensureDir, pathExists, writeJson } from '../../utils/fs.js';
11
+ import { cancel, intro, isCancel, note, outro } from '../../utils/logger.js';
12
+ /**
13
+ * Copies the source files needed for a new override into the workspace.
14
+ * @param srcDir - Original component directory in the engine checkout
15
+ * @param destDir - Destination override directory in the workspace
16
+ * @param overrideType - Requested override mode
17
+ * @returns Filenames copied into the override directory
18
+ */
19
+ async function copyOverrideFiles(srcDir, destDir, overrideType) {
20
+ await ensureDir(destDir);
21
+ const entries = await readdir(srcDir, { withFileTypes: true });
22
+ const copiedFiles = [];
23
+ for (const entry of entries) {
24
+ if (!entry.isFile())
25
+ continue;
26
+ if (overrideType === 'css-only') {
27
+ // Only copy .css files
28
+ if (entry.name.endsWith('.css')) {
29
+ await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
30
+ copiedFiles.push(entry.name);
31
+ }
32
+ }
33
+ else {
34
+ // Full override: copy .mjs and .css files
35
+ if (entry.name.endsWith('.mjs') || entry.name.endsWith('.css')) {
36
+ await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
37
+ copiedFiles.push(entry.name);
38
+ }
39
+ }
40
+ }
41
+ return copiedFiles;
42
+ }
43
+ /**
44
+ * Writes override metadata to disk and updates furnace.json with the new override entry.
45
+ * @param projectRoot - Root directory of the project
46
+ * @param destDir - Override component directory
47
+ * @param componentName - Component tag name
48
+ * @param overrideType - Override mode that was created
49
+ * @param description - Human-readable override description
50
+ * @param details - Source component metadata from the engine scan
51
+ * @param firefoxVersion - Firefox version recorded in the workspace config
52
+ * @param config - Mutable Furnace config object to update
53
+ */
54
+ async function saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, firefoxVersion, config) {
55
+ const overrideJson = {
56
+ type: overrideType,
57
+ description,
58
+ basePath: details.sourcePath,
59
+ baseVersion: firefoxVersion,
60
+ };
61
+ await writeJson(join(destDir, 'override.json'), overrideJson);
62
+ config.overrides[componentName] = {
63
+ type: overrideType,
64
+ description,
65
+ basePath: details.sourcePath,
66
+ baseVersion: firefoxVersion,
67
+ };
68
+ await writeFurnaceConfig(projectRoot, config);
69
+ }
70
+ /**
71
+ * Runs the furnace override command to fork an existing engine component.
72
+ * @param projectRoot - Root directory of the project
73
+ * @param name - Optional component tag name (prompted if not provided)
74
+ * @param options - CLI options for non-interactive mode
75
+ */
76
+ export async function furnaceOverrideCommand(projectRoot, name, options = {}) {
77
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
78
+ intro('Furnace Override');
79
+ // Load or create furnace.json
80
+ const config = await ensureFurnaceConfig(projectRoot);
81
+ const paths = getProjectPaths(projectRoot);
82
+ const furnacePaths = getFurnacePaths(projectRoot);
83
+ // Verify engine/ exists
84
+ if (!(await pathExists(paths.engine))) {
85
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
86
+ }
87
+ // --- Resolve component name ---
88
+ let componentName = name;
89
+ if (!componentName && isInteractive) {
90
+ // Scan for available components, filtering out already-overridden ones
91
+ const allComponents = await scanWidgetsDirectory(paths.engine);
92
+ const available = allComponents.filter((c) => !(c.tagName in config.overrides));
93
+ if (available.length === 0) {
94
+ throw new FurnaceError('No components available to override.');
95
+ }
96
+ const selected = await select({
97
+ message: 'Select a component to override:',
98
+ options: available.map((c) => ({
99
+ value: c.tagName,
100
+ label: c.tagName,
101
+ hint: [c.hasCSS && 'CSS', c.hasFTL && 'FTL', c.isRegistered && 'registered']
102
+ .filter(Boolean)
103
+ .join(', '),
104
+ })),
105
+ });
106
+ if (isCancel(selected)) {
107
+ cancel('Override cancelled');
108
+ return;
109
+ }
110
+ componentName = selected;
111
+ }
112
+ else if (!componentName) {
113
+ throw new InvalidArgumentError('Component name is required in non-interactive mode.\n' +
114
+ 'Usage: fireforge furnace override <name> -t <type> -d "description"', 'name');
115
+ }
116
+ // Validate component name to prevent path traversal
117
+ if (!/^[a-z][a-z0-9]*-[a-z0-9-]*$/.test(componentName)) {
118
+ throw new InvalidArgumentError(`Invalid component name "${componentName}": must contain a hyphen (required for custom elements), with only lowercase letters, digits, and hyphens.`, 'name');
119
+ }
120
+ // Check for existing override
121
+ if (componentName in config.overrides) {
122
+ throw new FurnaceError(`An override for "${componentName}" already exists in furnace.json`, componentName);
123
+ }
124
+ // Validate the component exists in engine
125
+ const details = await getComponentDetails(paths.engine, componentName);
126
+ if (!details) {
127
+ throw new FurnaceError(`Component "${componentName}" not found in the engine source tree.`, componentName);
128
+ }
129
+ // --- Resolve override type ---
130
+ let overrideType = options.type;
131
+ if (!overrideType && isInteractive) {
132
+ const typeResult = await select({
133
+ message: 'Override type:',
134
+ options: [
135
+ {
136
+ value: 'css-only',
137
+ label: 'CSS only — restyle the component',
138
+ },
139
+ {
140
+ value: 'full',
141
+ label: 'Full override — modify styling and behavior',
142
+ },
143
+ ],
144
+ });
145
+ if (isCancel(typeResult)) {
146
+ cancel('Override cancelled');
147
+ return;
148
+ }
149
+ overrideType = typeResult;
150
+ }
151
+ else if (!overrideType) {
152
+ throw new InvalidArgumentError('Override type is required in non-interactive mode. Use -t css-only or -t full.', 'type');
153
+ }
154
+ if (overrideType === 'css-only' && !details.hasCSS) {
155
+ throw new FurnaceError(`Component "${componentName}" does not have any CSS files to override with --type css-only.`, componentName);
156
+ }
157
+ // --- Resolve description ---
158
+ let description = options.description ?? '';
159
+ if (!description && isInteractive) {
160
+ const descResult = await text({
161
+ message: 'Description (optional):',
162
+ placeholder: 'What are you changing about this component?',
163
+ });
164
+ if (!isCancel(descResult)) {
165
+ description = String(descResult);
166
+ }
167
+ }
168
+ // --- Copy original files ---
169
+ const srcDir = join(paths.engine, details.sourcePath);
170
+ const destDir = join(furnacePaths.overridesDir, componentName);
171
+ if (await pathExists(destDir)) {
172
+ throw new FurnaceError(`Directory already exists: components/overrides/${componentName}`, componentName);
173
+ }
174
+ const forgeConfig = await loadConfig(projectRoot);
175
+ const copiedFiles = await copyOverrideFiles(srcDir, destDir, overrideType);
176
+ await saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, forgeConfig.firefox.version, config);
177
+ // --- Success ---
178
+ note(`Files copied to components/overrides/${componentName}/:\n` +
179
+ copiedFiles.map((f) => ` ${f}`).join('\n') +
180
+ '\n override.json' +
181
+ '\n\n' +
182
+ 'Next steps:\n' +
183
+ ` 1. Edit the copied files in components/overrides/${componentName}/\n` +
184
+ ' 2. Run "fireforge furnace preview" to see changes\n' +
185
+ ' 3. Run "fireforge build" to apply and build', componentName);
186
+ outro('Override created');
187
+ }
188
+ //# sourceMappingURL=override.js.map
@@ -0,0 +1,7 @@
1
+ import type { FurnacePreviewOptions } from '../../types/commands/index.js';
2
+ /**
3
+ * Runs the furnace preview command to start Storybook for component preview.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param options - Command options
6
+ */
7
+ export declare function furnacePreviewCommand(projectRoot: string, options?: FurnacePreviewOptions): Promise<void>;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { join } from 'node:path';
3
+ import { getProjectPaths } from '../../core/config.js';
4
+ import { furnaceConfigExists, loadFurnaceConfig } from '../../core/furnace-config.js';
5
+ import { cleanStories, syncStories } from '../../core/furnace-stories.js';
6
+ import { runMach, runMachCapture } from '../../core/mach.js';
7
+ import { FurnaceError } from '../../errors/furnace.js';
8
+ import { pathExists } from '../../utils/fs.js';
9
+ import { info, intro, outro, spinner } from '../../utils/logger.js';
10
+ /**
11
+ * Builds a targeted Storybook failure message from captured mach output.
12
+ * @param output - Combined stdout and stderr from the Storybook command
13
+ * @param installRequested - Whether the caller requested a dependency reinstall first
14
+ * @returns User-facing guidance for the specific failure mode
15
+ */
16
+ function buildStorybookFailureMessage(output, installRequested) {
17
+ const installHint = installRequested
18
+ ? 'Try running "python3 ./mach storybook upgrade" manually in the engine directory.'
19
+ : 'Run "fireforge furnace preview --install" to bootstrap Storybook dependencies, or run "python3 ./mach storybook upgrade" manually in engine/.';
20
+ if (/(ENOENT|No such file or directory)/i.test(output) && /storybook|backend/i.test(output)) {
21
+ return ('Storybook failed because the Firefox checkout appears to be missing Storybook workspace files or backend dependencies.\n\n' +
22
+ installHint);
23
+ }
24
+ return ('Storybook failed to start. Check the output above for the specific Firefox-side error.\n\n' +
25
+ installHint);
26
+ }
27
+ /**
28
+ * Runs the furnace preview command to start Storybook for component preview.
29
+ * @param projectRoot - Root directory of the project
30
+ * @param options - Command options
31
+ */
32
+ export async function furnacePreviewCommand(projectRoot, options = {}) {
33
+ intro('Furnace Preview (Storybook)');
34
+ // Verify engine exists
35
+ const paths = getProjectPaths(projectRoot);
36
+ if (!(await pathExists(paths.engine))) {
37
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
38
+ }
39
+ // Load furnace config
40
+ if (!(await furnaceConfigExists(projectRoot))) {
41
+ throw new FurnaceError('No furnace.json found. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
42
+ }
43
+ const config = await loadFurnaceConfig(projectRoot);
44
+ const stockCount = config.stock.length;
45
+ const overrideCount = Object.keys(config.overrides).length;
46
+ const customCount = Object.keys(config.custom).length;
47
+ const totalCount = stockCount + overrideCount + customCount;
48
+ if (totalCount === 0) {
49
+ info('No components to preview.');
50
+ outro('Done');
51
+ return;
52
+ }
53
+ const storybookRoot = join(paths.engine, 'browser', 'components', 'storybook');
54
+ if (!(await pathExists(storybookRoot))) {
55
+ throw new FurnaceError('This Firefox checkout does not contain browser/components/storybook. Furnace preview requires the upstream Storybook workspace to exist before stories can be synced.');
56
+ }
57
+ let previewResult;
58
+ let storiesSynced = false;
59
+ try {
60
+ // Sync story files
61
+ const syncSpinner = spinner('Syncing component stories...');
62
+ const result = await syncStories(projectRoot);
63
+ storiesSynced = true;
64
+ const created = result.created.length;
65
+ const updated = result.updated.length;
66
+ const total = created + updated;
67
+ syncSpinner.stop(`Synced ${total} stories (${created} new, ${updated} updated)`);
68
+ // Force-reinstall Storybook dependencies if requested
69
+ if (options.install) {
70
+ const installSpinner = spinner('Reinstalling Storybook dependencies...');
71
+ const installCode = await runMach(['storybook', 'upgrade'], paths.engine);
72
+ if (installCode !== 0) {
73
+ installSpinner.stop('Failed to reinstall Storybook dependencies');
74
+ throw new FurnaceError('Storybook dependency reinstallation failed. Try running "python3 ./mach storybook upgrade" manually in the engine directory.');
75
+ }
76
+ installSpinner.stop('Storybook dependencies reinstalled');
77
+ }
78
+ // Start Storybook
79
+ info('Starting Storybook...');
80
+ info('Press Ctrl+C to stop\n');
81
+ previewResult = await runMachCapture(['storybook'], paths.engine);
82
+ }
83
+ finally {
84
+ if (storiesSynced) {
85
+ await cleanStories(paths.engine);
86
+ }
87
+ }
88
+ if (previewResult.exitCode !== 0 &&
89
+ previewResult.exitCode !== 130 &&
90
+ previewResult.exitCode !== 143) {
91
+ const combinedOutput = `${previewResult.stdout}\n${previewResult.stderr}`;
92
+ throw new FurnaceError(buildStorybookFailureMessage(combinedOutput, options.install ?? false));
93
+ }
94
+ outro('Storybook stopped');
95
+ }
96
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1,8 @@
1
+ import type { FurnaceRemoveOptions } from '../../types/commands/index.js';
2
+ /**
3
+ * Runs the furnace remove command to remove a component from the workspace.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param name - Component tag name to remove
6
+ * @param options - CLI options
7
+ */
8
+ export declare function furnaceRemoveCommand(projectRoot: string, name: string, options?: FurnaceRemoveOptions): Promise<void>;
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { readdir, unlink } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { confirm } from '@clack/prompts';
5
+ import { getProjectPaths, loadConfig } from '../../core/config.js';
6
+ import { getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
7
+ import { removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
8
+ import { deregisterTestManifest } from '../../core/manifest-register.js';
9
+ import { FurnaceError } from '../../errors/furnace.js';
10
+ import { toError } from '../../utils/errors.js';
11
+ import { pathExists, readText, removeDir, writeText } from '../../utils/fs.js';
12
+ import { cancel, info, intro, isCancel, outro, warn } from '../../utils/logger.js';
13
+ /**
14
+ * Finds which section a component belongs to in the furnace config.
15
+ * @returns The component type, or undefined if not found
16
+ */
17
+ function findComponentType(config, name) {
18
+ if (config.stock.includes(name))
19
+ return 'stock';
20
+ if (name in config.overrides)
21
+ return 'override';
22
+ if (name in config.custom)
23
+ return 'custom';
24
+ return undefined;
25
+ }
26
+ /**
27
+ * Removes generated browser mochitest files associated with a custom component.
28
+ * @param name - Custom component tag name
29
+ * @param projectRoot - Root directory of the project
30
+ */
31
+ async function cleanupCustomTestFiles(name, projectRoot) {
32
+ try {
33
+ const forgeConfig = await loadConfig(projectRoot);
34
+ const paths = getProjectPaths(projectRoot);
35
+ const binaryName = forgeConfig.binaryName;
36
+ const strippedName = name.startsWith('moz-') ? name.slice(4) : name;
37
+ const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
38
+ ? strippedName.slice(binaryName.length + 1)
39
+ : strippedName;
40
+ const underscored = withoutBinaryPrefix.replace(/-/g, '_');
41
+ const testFileName = `browser_${binaryName}_${underscored}.js`;
42
+ const testDir = join(paths.engine, 'browser/base/content/test', binaryName);
43
+ if (await pathExists(testDir)) {
44
+ const testFilePath = join(testDir, testFileName);
45
+ if (await pathExists(testFilePath)) {
46
+ await unlink(testFilePath);
47
+ info(`Deleted test file: ${testFileName}`);
48
+ }
49
+ const tomlPath = join(testDir, 'browser.toml');
50
+ if (await pathExists(tomlPath)) {
51
+ const toml = await readText(tomlPath);
52
+ const entryPattern = `["${testFileName}"]`;
53
+ if (toml.includes(entryPattern)) {
54
+ const updated = toml.replace(new RegExp(`\\n?\\n?\\["${testFileName}"\\]\\n?`), '\n');
55
+ await writeText(tomlPath, updated);
56
+ }
57
+ }
58
+ const remaining = await readdir(testDir);
59
+ const hasTests = remaining.some((f) => f.startsWith('browser_') && f.endsWith('.js'));
60
+ if (!hasTests) {
61
+ await removeDir(testDir);
62
+ info(`Deleted empty test directory: browser/base/content/test/${binaryName}/`);
63
+ if (await deregisterTestManifest(paths.engine, binaryName)) {
64
+ info('Deregistered test manifest from browser/base/moz.build');
65
+ }
66
+ }
67
+ }
68
+ }
69
+ catch (error) {
70
+ warn(`Could not clean up test files — ${toError(error).message}. Remove them manually if needed.`);
71
+ }
72
+ }
73
+ /**
74
+ * Runs the furnace remove command to remove a component from the workspace.
75
+ * @param projectRoot - Root directory of the project
76
+ * @param name - Component tag name to remove
77
+ * @param options - CLI options
78
+ */
79
+ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
80
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
81
+ intro('Furnace Remove');
82
+ const config = await loadFurnaceConfig(projectRoot);
83
+ const furnacePaths = getFurnacePaths(projectRoot);
84
+ // Find which section the component belongs to
85
+ const type = findComponentType(config, name);
86
+ if (!type) {
87
+ throw new FurnaceError(`Component "${name}" not found in furnace.json. Run "fireforge furnace list" to see registered components.`, name);
88
+ }
89
+ // Require --force in non-interactive mode to prevent silent removals
90
+ if (!isInteractive && !options.force) {
91
+ throw new FurnaceError(`Cannot remove "${name}" in non-interactive mode without --force flag.`, name);
92
+ }
93
+ // Confirm removal (skip if --force)
94
+ if (!options.force && isInteractive) {
95
+ const confirmed = await confirm({
96
+ message: `Remove ${type} component "${name}"?`,
97
+ });
98
+ if (isCancel(confirmed) || !confirmed) {
99
+ cancel('Remove cancelled');
100
+ return;
101
+ }
102
+ }
103
+ // Delete component directory for override and custom types
104
+ const paths = getProjectPaths(projectRoot);
105
+ if (type === 'override') {
106
+ const dir = join(furnacePaths.overridesDir, name);
107
+ if (await pathExists(dir)) {
108
+ await removeDir(dir);
109
+ info(`Deleted components/overrides/${name}/`);
110
+ }
111
+ // Clean up deployed files in engine
112
+ const overrideConfig = config.overrides[name];
113
+ if (overrideConfig?.basePath) {
114
+ const engineDir = join(paths.engine, overrideConfig.basePath);
115
+ if (await pathExists(engineDir)) {
116
+ warn(`Deployed files may remain in engine/${overrideConfig.basePath}. Run "fireforge reset -f && fireforge import" to clean.`);
117
+ }
118
+ }
119
+ }
120
+ else if (type === 'custom') {
121
+ const customConfig = config.custom[name];
122
+ if (customConfig?.register) {
123
+ await removeCustomElementRegistration(paths.engine, name);
124
+ info(`Deregistered ${name} from customElements.js`);
125
+ }
126
+ await removeJarMnEntries(paths.engine, name);
127
+ info(`Removed ${name} entries from toolkit/content/jar.mn`);
128
+ const dir = join(furnacePaths.customDir, name);
129
+ if (await pathExists(dir)) {
130
+ await removeDir(dir);
131
+ info(`Deleted components/custom/${name}/`);
132
+ }
133
+ // Clean up deployed files in engine
134
+ if (customConfig?.targetPath) {
135
+ const engineDir = join(paths.engine, customConfig.targetPath);
136
+ if (await pathExists(engineDir)) {
137
+ await removeDir(engineDir);
138
+ info(`Deleted deployed files from engine/${customConfig.targetPath}/`);
139
+ }
140
+ }
141
+ }
142
+ if (type === 'custom') {
143
+ await cleanupCustomTestFiles(name, projectRoot);
144
+ }
145
+ // Remove entry from furnace.json
146
+ if (type === 'stock') {
147
+ config.stock = config.stock.filter((s) => s !== name);
148
+ }
149
+ else if (type === 'override') {
150
+ config.overrides = Object.fromEntries(Object.entries(config.overrides).filter(([key]) => key !== name));
151
+ }
152
+ else {
153
+ config.custom = Object.fromEntries(Object.entries(config.custom).filter(([key]) => key !== name));
154
+ }
155
+ await writeFurnaceConfig(projectRoot, config);
156
+ info(`Removed "${name}" from furnace.json`);
157
+ outro('Component removed');
158
+ }
159
+ //# sourceMappingURL=remove.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Runs the furnace scan command to discover MozLitElement components.
3
+ * @param projectRoot - Root directory of the project
4
+ */
5
+ export declare function furnaceScanCommand(projectRoot: string): Promise<void>;
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { confirm, multiselect } from '@clack/prompts';
3
+ import { getProjectPaths } from '../../core/config.js';
4
+ import { ensureFurnaceConfig, furnaceConfigExists, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
5
+ import { scanWidgetsDirectory } from '../../core/furnace-scanner.js';
6
+ import { FurnaceError } from '../../errors/furnace.js';
7
+ import { pathExists } from '../../utils/fs.js';
8
+ import { cancel, info, intro, isCancel, note, outro, spinner, success, } from '../../utils/logger.js';
9
+ /**
10
+ * Prompts the user to add newly discovered stock components to furnace.json.
11
+ * @param components - Components discovered in the engine scan
12
+ * @param tracked - Existing Furnace tracking map keyed by tag name
13
+ * @param projectRoot - Root directory of the project
14
+ */
15
+ async function promptAddComponents(components, tracked, projectRoot) {
16
+ const untrackedComponents = components.filter((c) => !tracked.has(c.tagName));
17
+ const shouldAdd = await confirm({ message: 'Add components to furnace.json?' });
18
+ if (isCancel(shouldAdd) || !shouldAdd) {
19
+ if (isCancel(shouldAdd)) {
20
+ cancel('Cancelled');
21
+ }
22
+ outro('Scan complete');
23
+ return;
24
+ }
25
+ const selected = await multiselect({
26
+ message: 'Select components to add as stock',
27
+ options: untrackedComponents.map((c) => {
28
+ const features = [];
29
+ if (c.hasCSS)
30
+ features.push('CSS');
31
+ if (c.hasFTL)
32
+ features.push('FTL');
33
+ if (c.isRegistered)
34
+ features.push('registered');
35
+ const label = features.length > 0 ? `${c.tagName} — ${features.join(', ')}` : c.tagName;
36
+ return { value: c.tagName, label };
37
+ }),
38
+ });
39
+ if (isCancel(selected)) {
40
+ cancel('Cancelled');
41
+ outro('Scan complete');
42
+ return;
43
+ }
44
+ const config = await ensureFurnaceConfig(projectRoot);
45
+ const toAdd = selected.filter((s) => !config.stock.includes(s));
46
+ config.stock.push(...toAdd);
47
+ await writeFurnaceConfig(projectRoot, config);
48
+ success(`Added ${selected.length} component${selected.length === 1 ? '' : 's'} to furnace.json`);
49
+ }
50
+ /**
51
+ * Runs the furnace scan command to discover MozLitElement components.
52
+ * @param projectRoot - Root directory of the project
53
+ */
54
+ export async function furnaceScanCommand(projectRoot) {
55
+ intro('Furnace Scan');
56
+ const paths = getProjectPaths(projectRoot);
57
+ if (!(await pathExists(paths.engine))) {
58
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
59
+ }
60
+ const s = spinner('Scanning engine for components...');
61
+ const components = await scanWidgetsDirectory(paths.engine);
62
+ s.stop(`Found ${components.length} component${components.length === 1 ? '' : 's'}`);
63
+ // Build tracking info from furnace.json if it exists
64
+ const tracked = new Map();
65
+ if (await furnaceConfigExists(projectRoot)) {
66
+ const config = await loadFurnaceConfig(projectRoot);
67
+ for (const name of config.stock) {
68
+ tracked.set(name, 'stock');
69
+ }
70
+ for (const name of Object.keys(config.overrides)) {
71
+ tracked.set(name, 'override');
72
+ }
73
+ for (const name of Object.keys(config.custom)) {
74
+ tracked.set(name, 'custom');
75
+ }
76
+ }
77
+ // Display each component
78
+ for (const component of components) {
79
+ const features = [];
80
+ if (component.hasCSS)
81
+ features.push('CSS');
82
+ if (component.hasFTL)
83
+ features.push('FTL');
84
+ if (component.isRegistered)
85
+ features.push('registered');
86
+ let line = component.tagName;
87
+ if (features.length > 0) {
88
+ line += ` — ${features.join(', ')}`;
89
+ }
90
+ const type = tracked.get(component.tagName);
91
+ if (type) {
92
+ line += ` [${type}]`;
93
+ }
94
+ info(line);
95
+ }
96
+ // Summary
97
+ let trackedCount = 0;
98
+ for (const component of components) {
99
+ if (tracked.has(component.tagName)) {
100
+ trackedCount++;
101
+ }
102
+ }
103
+ const untrackedCount = components.length - trackedCount;
104
+ note(`Total: ${components.length} Tracked: ${trackedCount} Untracked: ${untrackedCount}`, 'Summary');
105
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
106
+ if (isInteractive && untrackedCount > 0) {
107
+ await promptAddComponents(components, tracked, projectRoot);
108
+ return;
109
+ }
110
+ outro('Scan complete');
111
+ }
112
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs the furnace status command to show an overview of Furnace state.
3
+ * When a component name is provided, shows detailed registration status.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param name - Optional component name for detailed status
6
+ */
7
+ export declare function furnaceStatusCommand(projectRoot: string, name?: string): Promise<void>;