@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,403 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { join } from 'node:path';
3
+ import { toError } from '../utils/errors.js';
4
+ import { pathExists, readText } from '../utils/fs.js';
5
+ import { verbose } from '../utils/logger.js';
6
+ import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
7
+ import { loadFurnaceConfig } from './furnace-config.js';
8
+ import { getLicenseHeader, hasAnyLicenseHeader } from './license-headers.js';
9
+ // ---------------------------------------------------------------------------
10
+ // Helpers
11
+ // ---------------------------------------------------------------------------
12
+ const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
13
+ /**
14
+ * Returns true if the filename looks like a JS/MJS/JSM file.
15
+ * Handles `.sys.mjs` as well.
16
+ */
17
+ function isJsFile(file) {
18
+ return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
19
+ }
20
+ /**
21
+ * Detects comment style from file extension for license header checks.
22
+ */
23
+ export function commentStyleForFile(file) {
24
+ if (file.endsWith('.css'))
25
+ return 'css';
26
+ if (file.endsWith('.ftl'))
27
+ return 'hash';
28
+ if (isJsFile(file))
29
+ return 'js';
30
+ return null;
31
+ }
32
+ /**
33
+ * Extracts new-file paths from a unified diff by scanning for `new file mode` markers.
34
+ */
35
+ export function detectNewFilesInDiff(diffContent) {
36
+ const newFiles = new Set();
37
+ const lines = diffContent.split('\n');
38
+ let currentFile = null;
39
+ for (const line of lines) {
40
+ if (line.startsWith('diff --git')) {
41
+ const match = /^diff --git a\/.+ b\/(.+)$/.exec(line);
42
+ currentFile = match?.[1] ?? null;
43
+ continue;
44
+ }
45
+ if (line.startsWith('new file mode') && currentFile) {
46
+ newFiles.add(currentFile);
47
+ }
48
+ }
49
+ return newFiles;
50
+ }
51
+ /**
52
+ * Extracts added lines per file from a unified diff.
53
+ * Returns a map of file path → array of added line contents (without the leading `+`).
54
+ */
55
+ function extractAddedLinesPerFile(diffContent) {
56
+ const result = new Map();
57
+ const lines = diffContent.split('\n');
58
+ let currentFile = null;
59
+ let inHunk = false;
60
+ for (const line of lines) {
61
+ if (line.startsWith('diff --git')) {
62
+ const match = /^diff --git a\/.+ b\/(.+)$/.exec(line);
63
+ currentFile = match?.[1] ?? null;
64
+ inHunk = false;
65
+ continue;
66
+ }
67
+ if (line.startsWith('@@')) {
68
+ inHunk = true;
69
+ continue;
70
+ }
71
+ if (inHunk && currentFile && line.startsWith('+') && !line.startsWith('+++')) {
72
+ let arr = result.get(currentFile);
73
+ if (!arr) {
74
+ arr = [];
75
+ result.set(currentFile, arr);
76
+ }
77
+ arr.push(line.slice(1));
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // CSS lint (existing — now with severity)
84
+ // ---------------------------------------------------------------------------
85
+ /**
86
+ * Lints patched CSS files for raw color values and non-tokenized custom properties.
87
+ *
88
+ * @param repoDir - Absolute path to the engine (repository) directory
89
+ * @param affectedFiles - File paths (relative to repoDir) affected by the patch
90
+ * @returns Array of lint issues found
91
+ */
92
+ export async function lintPatchedCss(repoDir, affectedFiles) {
93
+ const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
94
+ if (cssFiles.length === 0)
95
+ return [];
96
+ // Load furnace config gracefully — skip token-prefix check if unavailable
97
+ let tokenPrefix;
98
+ let tokenAllowlist;
99
+ try {
100
+ const root = join(repoDir, '..');
101
+ const config = await loadFurnaceConfig(root);
102
+ if (config.tokenPrefix) {
103
+ tokenPrefix = config.tokenPrefix;
104
+ tokenAllowlist = new Set(config.tokenAllowlist ?? []);
105
+ }
106
+ }
107
+ catch (error) {
108
+ verbose(`Skipping furnace token-prefix lint hints because furnace.json could not be loaded: ${toError(error).message}`);
109
+ }
110
+ const issues = [];
111
+ for (const file of cssFiles) {
112
+ const filePath = join(repoDir, file);
113
+ if (!(await pathExists(filePath)))
114
+ continue;
115
+ const rawCss = await readText(filePath);
116
+ // Strip block comments before scanning
117
+ const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
118
+ // Check for raw color values
119
+ if (hasRawCssColors(cssContent)) {
120
+ issues.push({
121
+ file,
122
+ check: 'raw-color-value',
123
+ message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
124
+ severity: 'warning',
125
+ });
126
+ }
127
+ // Check for non-tokenized custom properties
128
+ if (tokenPrefix) {
129
+ const varPattern = /var\(\s*(--[\w-]+)/g;
130
+ let match;
131
+ while ((match = varPattern.exec(cssContent)) !== null) {
132
+ const prop = match[1];
133
+ if (prop && !prop.startsWith(tokenPrefix) && !tokenAllowlist?.has(prop)) {
134
+ issues.push({
135
+ file,
136
+ check: 'token-prefix-violation',
137
+ message: `CSS references var(${prop}) which does not match the required token prefix "${tokenPrefix}". Use a design token or add to tokenAllowlist.`,
138
+ severity: 'error',
139
+ });
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return issues;
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // License header lint
148
+ // ---------------------------------------------------------------------------
149
+ /**
150
+ * Checks new files for required license headers.
151
+ *
152
+ * @param repoDir - Absolute path to the engine directory
153
+ * @param newFiles - New file paths (relative to repoDir)
154
+ * @param config - Project configuration
155
+ * @returns Array of lint issues
156
+ */
157
+ export async function lintNewFileHeaders(repoDir, newFiles, config) {
158
+ const license = config.license ?? 'MPL-2.0';
159
+ const issues = [];
160
+ for (const file of newFiles) {
161
+ const style = commentStyleForFile(file);
162
+ if (!style)
163
+ continue;
164
+ const filePath = join(repoDir, file);
165
+ if (!(await pathExists(filePath)))
166
+ continue;
167
+ const content = await readText(filePath);
168
+ const expectedHeader = getLicenseHeader(license, style);
169
+ if (!content.startsWith(expectedHeader)) {
170
+ issues.push({
171
+ file,
172
+ check: 'missing-license-header',
173
+ message: `New file is missing the required ${license} license header.`,
174
+ severity: 'error',
175
+ });
176
+ }
177
+ }
178
+ return issues;
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // JS lint
182
+ // ---------------------------------------------------------------------------
183
+ /**
184
+ * Lints patched JS/MJS files for import conventions, file size, JSDoc, and
185
+ * observer topic naming.
186
+ *
187
+ * @param repoDir - Absolute path to the engine directory
188
+ * @param affectedFiles - File paths (relative to repoDir)
189
+ * @param newFiles - Set of files that are newly created in this patch
190
+ * @param config - Project configuration
191
+ * @returns Array of lint issues
192
+ */
193
+ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config) {
194
+ const jsFiles = affectedFiles.filter(isJsFile);
195
+ if (jsFiles.length === 0)
196
+ return [];
197
+ const issues = [];
198
+ const binaryName = config.binaryName.toLowerCase();
199
+ for (const file of jsFiles) {
200
+ const filePath = join(repoDir, file);
201
+ if (!(await pathExists(filePath)))
202
+ continue;
203
+ const content = await readText(filePath);
204
+ const isNew = newFiles.has(file);
205
+ const isSysMjs = file.endsWith('.sys.mjs');
206
+ // 1. Relative import check
207
+ const strippedContent = stripJsComments(content);
208
+ const relativeImportPattern = /(?:ChromeUtils\.import(?:ESModule)?|Cu\.import)\s*\(\s*["'](?:\.\.?\/)/gm;
209
+ const esRelativePattern = /\bimport\s+.*?\s+from\s+["'](?:\.\.?\/)/gm;
210
+ if (relativeImportPattern.test(strippedContent) || esRelativePattern.test(strippedContent)) {
211
+ issues.push({
212
+ file,
213
+ check: 'relative-import',
214
+ message: `Relative imports are not allowed. Use "resource:///modules/${config.binaryName}/" for .sys.mjs or "chrome://browser/content/" for subscripts.`,
215
+ severity: 'error',
216
+ });
217
+ }
218
+ // 2. File size check (new files only)
219
+ if (isNew) {
220
+ const lineCount = content.split('\n').length;
221
+ if (lineCount > 650) {
222
+ issues.push({
223
+ file,
224
+ check: 'file-too-large',
225
+ message: `New file has ${lineCount} lines (recommended max: 650). Consider decomposing.`,
226
+ severity: 'warning',
227
+ });
228
+ }
229
+ }
230
+ // 3. JSDoc on exports (new .sys.mjs files only)
231
+ if (isNew && isSysMjs) {
232
+ const lines = content.split('\n');
233
+ for (let i = 0; i < lines.length; i++) {
234
+ const line = lines[i] ?? '';
235
+ if (/^export\s+(function|class|const|let|var)\s/.test(line)) {
236
+ // Walk backwards to find JSDoc
237
+ let hasJsDoc = false;
238
+ for (let j = i - 1; j >= 0; j--) {
239
+ const prev = (lines[j] ?? '').trim();
240
+ if (prev === '')
241
+ continue;
242
+ if (prev.endsWith('*/')) {
243
+ hasJsDoc = true;
244
+ }
245
+ break;
246
+ }
247
+ if (!hasJsDoc) {
248
+ issues.push({
249
+ file,
250
+ check: 'missing-jsdoc',
251
+ message: `Export at line ${i + 1} is missing a JSDoc comment with @param/@returns.`,
252
+ severity: 'warning',
253
+ });
254
+ }
255
+ }
256
+ }
257
+ }
258
+ // 4. Observer topic naming
259
+ const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)]*["']([^"']+)["']/g;
260
+ let topicMatch;
261
+ while ((topicMatch = topicPattern.exec(strippedContent)) !== null) {
262
+ const topic = topicMatch[1];
263
+ if (!topic)
264
+ continue;
265
+ // Only flag topics that contain the binaryName but don't follow convention
266
+ if (topic.toLowerCase().includes(binaryName) && !/^[\w]+-[a-z]+-[a-z]+/.test(topic)) {
267
+ issues.push({
268
+ file,
269
+ check: 'observer-topic-naming',
270
+ message: `Observer topic "${topic}" should follow "${binaryName}-<noun>-<verb>" naming convention.`,
271
+ severity: 'warning',
272
+ });
273
+ }
274
+ }
275
+ }
276
+ return issues;
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Modification comment lint
280
+ // ---------------------------------------------------------------------------
281
+ /**
282
+ * Checks that modifications to existing (non-new) JS/MJS files include at
283
+ * least one `// BINARYNAME:` comment in the added lines.
284
+ *
285
+ * @param diffContent - Raw unified diff string
286
+ * @param config - Project configuration
287
+ * @returns Array of lint issues
288
+ */
289
+ export function lintModificationComments(diffContent, config) {
290
+ const newFiles = detectNewFilesInDiff(diffContent);
291
+ const addedLines = extractAddedLinesPerFile(diffContent);
292
+ const issues = [];
293
+ const marker = `// ${config.binaryName.toUpperCase()}:`;
294
+ for (const [file, lines] of addedLines) {
295
+ // Only check JS/MJS files that are modifications (not new files)
296
+ if (!isJsFile(file) || newFiles.has(file))
297
+ continue;
298
+ const hasMarker = lines.some((line) => line.toUpperCase().includes(marker.toUpperCase()));
299
+ if (!hasMarker && lines.length > 0) {
300
+ issues.push({
301
+ file,
302
+ check: 'missing-modification-comment',
303
+ message: `Modified upstream file lacks a "${marker}" comment marking your changes.`,
304
+ severity: 'warning',
305
+ });
306
+ }
307
+ }
308
+ return issues;
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Patch size lint (moved from export-shared.ts warnLargePatch)
312
+ // ---------------------------------------------------------------------------
313
+ /**
314
+ * Checks patch size and emits advisory warnings.
315
+ */
316
+ export function lintPatchSize(filesAffected, lineCount) {
317
+ const issues = [];
318
+ if (filesAffected.length > 5) {
319
+ issues.push({
320
+ file: '(patch)',
321
+ check: 'large-patch-files',
322
+ message: `Patch affects ${filesAffected.length} files (recommended: ≤5). Consider splitting into smaller, focused patches.`,
323
+ severity: 'warning',
324
+ });
325
+ }
326
+ if (lineCount > 300) {
327
+ issues.push({
328
+ file: '(patch)',
329
+ check: 'large-patch-lines',
330
+ message: `Patch is ${lineCount} lines (recommended: ≤300). Consider splitting into smaller, focused patches.`,
331
+ severity: 'warning',
332
+ });
333
+ }
334
+ return issues;
335
+ }
336
+ // ---------------------------------------------------------------------------
337
+ // Modified file header lint
338
+ // ---------------------------------------------------------------------------
339
+ /**
340
+ * Checks that modified (non-new) files with a supported extension still
341
+ * start with a recognized license header.
342
+ *
343
+ * @param repoDir - Engine root directory
344
+ * @param affectedFiles - All files affected by the patch
345
+ * @param newFiles - Set of newly created files (excluded from this check)
346
+ * @returns Warning-level lint issues for files missing any recognized header
347
+ */
348
+ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles) {
349
+ const issues = [];
350
+ for (const file of affectedFiles) {
351
+ if (newFiles.has(file))
352
+ continue;
353
+ const style = commentStyleForFile(file);
354
+ if (!style)
355
+ continue;
356
+ const filePath = join(repoDir, file);
357
+ if (!(await pathExists(filePath)))
358
+ continue;
359
+ const content = await readText(filePath);
360
+ if (!hasAnyLicenseHeader(content, style)) {
361
+ issues.push({
362
+ file,
363
+ check: 'modified-file-missing-header',
364
+ message: 'Modified upstream file appears to be missing a recognized license header.',
365
+ severity: 'warning',
366
+ });
367
+ }
368
+ }
369
+ return issues;
370
+ }
371
+ // ---------------------------------------------------------------------------
372
+ // Orchestrator
373
+ // ---------------------------------------------------------------------------
374
+ /**
375
+ * Runs all patch lint checks and returns combined issues.
376
+ *
377
+ * @param repoDir - Absolute path to the engine directory
378
+ * @param affectedFiles - File paths (relative to repoDir) affected by the patch
379
+ * @param diffContent - Raw unified diff string
380
+ * @param config - Project configuration
381
+ * @returns Array of all lint issues found
382
+ */
383
+ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config) {
384
+ const newFiles = detectNewFilesInDiff(diffContent);
385
+ const lineCount = diffContent.split('\n').length;
386
+ const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
387
+ lintPatchedCss(repoDir, affectedFiles),
388
+ lintNewFileHeaders(repoDir, [...newFiles], config),
389
+ lintPatchedJs(repoDir, affectedFiles, newFiles, config),
390
+ lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
391
+ ]);
392
+ const modCommentIssues = lintModificationComments(diffContent, config);
393
+ const sizeIssues = lintPatchSize(affectedFiles, lineCount);
394
+ return [
395
+ ...sizeIssues,
396
+ ...cssIssues,
397
+ ...headerIssues,
398
+ ...modifiedHeaderIssues,
399
+ ...jsIssues,
400
+ ...modCommentIssues,
401
+ ];
402
+ }
403
+ //# sourceMappingURL=patch-lint.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Filesystem-based lock for serializing patch directory mutations.
3
+ */
4
+ /**
5
+ * Runs a patch directory mutation while holding an exclusive filesystem lock.
6
+ * This serializes filename allocation and manifest writes across parallel exports.
7
+ */
8
+ export declare function withPatchDirectoryLock<T>(patchesDir: string, operation: () => Promise<T>): Promise<T>;
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Filesystem-based lock for serializing patch directory mutations.
4
+ */
5
+ import { join } from 'node:path';
6
+ import { PatchError } from '../errors/patch.js';
7
+ import { toError } from '../utils/errors.js';
8
+ import { withFileLock } from './file-lock.js';
9
+ const PATCH_DIRECTORY_LOCK = '.fireforge-patches.lock';
10
+ /**
11
+ * Runs a patch directory mutation while holding an exclusive filesystem lock.
12
+ * This serializes filename allocation and manifest writes across parallel exports.
13
+ */
14
+ export async function withPatchDirectoryLock(patchesDir, operation) {
15
+ const lockDir = join(patchesDir, PATCH_DIRECTORY_LOCK);
16
+ return withFileLock(lockDir, operation, {
17
+ onTimeoutMessage: `Timed out waiting for another patch export to finish in ${patchesDir}.\n` +
18
+ `If no other fireforge process is running, the lock may be stale. ` +
19
+ `Remove it manually:\n rm -rf "${lockDir}"`,
20
+ onStaleLockMessage: (ageMs) => `Removing stale patch lock (age: ${Math.round(ageMs / 1000)}s). ` +
21
+ 'A previous fireforge process may have crashed.',
22
+ }).catch((error) => {
23
+ if (error instanceof PatchError) {
24
+ throw error;
25
+ }
26
+ throw new PatchError(toError(error).message);
27
+ });
28
+ }
29
+ //# sourceMappingURL=patch-lock.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Manifest consistency checks and rebuild/recovery operations.
3
+ */
4
+ import type { PatchesManifest } from '../types/commands/index.js';
5
+ /** Consistency issue codes for manifest validation. */
6
+ export interface PatchManifestConsistencyIssue {
7
+ code: 'manifest-invalid' | 'manifest-missing' | 'missing-patch-file' | 'untracked-patch-file' | 'files-affected-mismatch' | 'duplicate-manifest-entry';
8
+ filename: string;
9
+ message: string;
10
+ }
11
+ /**
12
+ * Validates that patches.json and the patch directory describe the same patch set.
13
+ * @param patchesDir - Path to the patches directory
14
+ * @returns Consistency issues between manifest metadata and on-disk patch files
15
+ */
16
+ export declare function validatePatchesManifestConsistency(patchesDir: string): Promise<PatchManifestConsistencyIssue[]>;
17
+ /**
18
+ * Rebuilds patches.json from the patch files currently present on disk.
19
+ * Existing metadata is preserved when possible; missing entries are recovered
20
+ * from filename structure, patch contents, and file mtimes.
21
+ * @param patchesDir - Path to the patches directory
22
+ * @param fallbackSourceEsrVersion - ESR version to use for recovered entries
23
+ */
24
+ export declare function rebuildPatchesManifest(patchesDir: string, fallbackSourceEsrVersion: string): Promise<PatchesManifest>;
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Manifest consistency checks and rebuild/recovery operations.
4
+ */
5
+ import { stat } from 'node:fs/promises';
6
+ import { discoverPatches, getAllTargetFilesFromPatch } from './patch-files.js';
7
+ import { loadPatchesManifestState, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest-io.js';
8
+ import { inferPatchMetadataFromFilename } from './patch-manifest-validate.js';
9
+ /**
10
+ * Validates that patches.json and the patch directory describe the same patch set.
11
+ * @param patchesDir - Path to the patches directory
12
+ * @returns Consistency issues between manifest metadata and on-disk patch files
13
+ */
14
+ export async function validatePatchesManifestConsistency(patchesDir) {
15
+ const manifestState = await loadPatchesManifestState(patchesDir);
16
+ const patches = await discoverPatches(patchesDir);
17
+ const issues = [];
18
+ if (manifestState.parseError) {
19
+ issues.push({
20
+ code: 'manifest-invalid',
21
+ filename: PATCHES_MANIFEST,
22
+ message: `patches.json exists but could not be parsed: ${manifestState.parseError.message}`,
23
+ });
24
+ return issues;
25
+ }
26
+ if (!manifestState.exists) {
27
+ if (patches.length > 0) {
28
+ issues.push({
29
+ code: 'manifest-missing',
30
+ filename: PATCHES_MANIFEST,
31
+ message: `patches.json is missing while ${patches.length} patch file(s) exist.`,
32
+ });
33
+ }
34
+ return issues;
35
+ }
36
+ const manifest = manifestState.manifest;
37
+ if (!manifest) {
38
+ return issues;
39
+ }
40
+ const patchByFilename = new Map(patches.map((patch) => [patch.filename, patch]));
41
+ const seenManifestEntries = new Set();
42
+ for (const metadata of manifest.patches) {
43
+ if (seenManifestEntries.has(metadata.filename)) {
44
+ issues.push({
45
+ code: 'duplicate-manifest-entry',
46
+ filename: metadata.filename,
47
+ message: `patches.json contains duplicate metadata entries for ${metadata.filename}.`,
48
+ });
49
+ continue;
50
+ }
51
+ seenManifestEntries.add(metadata.filename);
52
+ const patch = patchByFilename.get(metadata.filename);
53
+ if (!patch) {
54
+ issues.push({
55
+ code: 'missing-patch-file',
56
+ filename: metadata.filename,
57
+ message: `${metadata.filename} is listed in patches.json but the patch file is missing.`,
58
+ });
59
+ continue;
60
+ }
61
+ const declaredFiles = normalizeFiles(metadata.filesAffected);
62
+ const actualFiles = normalizeFiles(await getAllTargetFilesFromPatch(patch.path));
63
+ if (!sameStringArray(declaredFiles, actualFiles)) {
64
+ issues.push({
65
+ code: 'files-affected-mismatch',
66
+ filename: metadata.filename,
67
+ message: `${metadata.filename} declares [${declaredFiles.join(', ')}] in patches.json ` +
68
+ `but the patch file targets [${actualFiles.join(', ')}].`,
69
+ });
70
+ }
71
+ patchByFilename.delete(metadata.filename);
72
+ }
73
+ for (const orphanPatch of patchByFilename.values()) {
74
+ issues.push({
75
+ code: 'untracked-patch-file',
76
+ filename: orphanPatch.filename,
77
+ message: `${orphanPatch.filename} exists on disk but is not tracked in patches.json.`,
78
+ });
79
+ }
80
+ return issues;
81
+ }
82
+ /**
83
+ * Rebuilds patches.json from the patch files currently present on disk.
84
+ * Existing metadata is preserved when possible; missing entries are recovered
85
+ * from filename structure, patch contents, and file mtimes.
86
+ * @param patchesDir - Path to the patches directory
87
+ * @param fallbackSourceEsrVersion - ESR version to use for recovered entries
88
+ */
89
+ export async function rebuildPatchesManifest(patchesDir, fallbackSourceEsrVersion) {
90
+ const manifestState = await loadPatchesManifestState(patchesDir);
91
+ const existingEntries = new Map();
92
+ if (manifestState.manifest) {
93
+ for (const entry of manifestState.manifest.patches) {
94
+ existingEntries.set(entry.filename, entry);
95
+ }
96
+ }
97
+ const patches = await discoverPatches(patchesDir);
98
+ const rebuiltPatches = [];
99
+ const highestFiniteOrder = patches.reduce((highest, patch) => {
100
+ return Number.isFinite(patch.order) ? Math.max(highest, patch.order) : highest;
101
+ }, 0);
102
+ let nextRecoveredOrder = highestFiniteOrder + 1;
103
+ for (const patch of patches) {
104
+ const existing = existingEntries.get(patch.filename);
105
+ const filesAffected = normalizeFiles(await getAllTargetFilesFromPatch(patch.path));
106
+ const patchStats = await stat(patch.path);
107
+ const inferred = inferPatchMetadataFromFilename(patch.filename);
108
+ const recoveredOrder = Number.isFinite(patch.order) ? patch.order : nextRecoveredOrder++;
109
+ rebuiltPatches.push({
110
+ filename: patch.filename,
111
+ order: recoveredOrder,
112
+ category: existing?.category ?? inferred.category,
113
+ name: existing?.name ?? inferred.name,
114
+ description: existing?.description ??
115
+ `Recovered manifest entry for ${patch.filename}. Review description and ESR version.`,
116
+ createdAt: existing?.createdAt ?? new Date(patchStats.mtimeMs).toISOString(),
117
+ sourceEsrVersion: existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
118
+ filesAffected,
119
+ });
120
+ }
121
+ rebuiltPatches.sort((left, right) => left.order - right.order || left.filename.localeCompare(right.filename));
122
+ const rebuiltManifest = {
123
+ version: 1,
124
+ patches: rebuiltPatches,
125
+ };
126
+ await savePatchesManifest(patchesDir, rebuiltManifest);
127
+ return rebuiltManifest;
128
+ }
129
+ function normalizeFiles(files) {
130
+ return Array.from(new Set(files)).sort((left, right) => left.localeCompare(right));
131
+ }
132
+ function sameStringArray(left, right) {
133
+ return left.length === right.length && left.every((value, index) => value === right[index]);
134
+ }
135
+ //# sourceMappingURL=patch-manifest-consistency.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Manifest I/O: load, save, and add operations for patches.json.
3
+ */
4
+ import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
5
+ /** Filename for the patches manifest */
6
+ export declare const PATCHES_MANIFEST = "patches.json";
7
+ /** Internal state returned by loadPatchesManifestState. */
8
+ export interface LoadedManifestState {
9
+ exists: boolean;
10
+ manifest: PatchesManifest | null;
11
+ parseError: Error | undefined;
12
+ }
13
+ /**
14
+ * Loads and validates the patches manifest, returning full state information.
15
+ * @param patchesDir - Path to the patches directory
16
+ */
17
+ export declare function loadPatchesManifestState(patchesDir: string): Promise<LoadedManifestState>;
18
+ /**
19
+ * Loads the patches manifest if it exists.
20
+ * @param patchesDir - Path to the patches directory
21
+ * @returns PatchesManifest or null if not found
22
+ */
23
+ export declare function loadPatchesManifest(patchesDir: string): Promise<PatchesManifest | null>;
24
+ /**
25
+ * Saves the patches manifest.
26
+ * @param patchesDir - Path to the patches directory
27
+ * @param manifest - Manifest to save
28
+ */
29
+ export declare function savePatchesManifest(patchesDir: string, manifest: PatchesManifest): Promise<void>;
30
+ /**
31
+ * Adds or updates a patch entry in the manifest.
32
+ * @param patchesDir - Path to the patches directory
33
+ * @param metadata - Patch metadata to add/update
34
+ * @param removeFilenames - Optional filenames to remove in the same read-modify-write cycle
35
+ */
36
+ export declare function addPatchToManifest(patchesDir: string, metadata: PatchMetadata, removeFilenames?: string[]): Promise<void>;