@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,268 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { join } from 'node:path';
3
+ import { isBrandingManagedPath } from '../core/branding.js';
4
+ import { getProjectPaths, loadConfig } from '../core/config.js';
5
+ import { getStatusWithCodes, isGitRepository } from '../core/git.js';
6
+ import { getUntrackedFilesInDir } from '../core/git-status.js';
7
+ import { isFileRegistered, matchesRegistrablePattern } from '../core/manifest-rules.js';
8
+ import { computePatchedContent } from '../core/patch-apply.js';
9
+ import { loadPatchesManifest } from '../core/patch-manifest.js';
10
+ import { GeneralError } from '../errors/base.js';
11
+ import { toError } from '../utils/errors.js';
12
+ import { pathExists, readText } from '../utils/fs.js';
13
+ import { info, intro, outro, verbose, warn } from '../utils/logger.js';
14
+ /**
15
+ * Status code descriptions for git status.
16
+ */
17
+ const STATUS_DESCRIPTIONS = {
18
+ M: 'modified',
19
+ A: 'added',
20
+ D: 'deleted',
21
+ R: 'renamed',
22
+ C: 'copied',
23
+ U: 'unmerged',
24
+ '?': 'untracked',
25
+ '!': 'ignored',
26
+ };
27
+ /**
28
+ * Gets a human-readable description for a git status code.
29
+ */
30
+ function getStatusDescription(code) {
31
+ return STATUS_DESCRIPTIONS[code] ?? 'changed';
32
+ }
33
+ function getPrimaryStatusCode(status) {
34
+ if (status.includes('?'))
35
+ return '?';
36
+ if (status.includes('!'))
37
+ return '!';
38
+ for (const code of status) {
39
+ if (code !== ' ') {
40
+ return code;
41
+ }
42
+ }
43
+ return status;
44
+ }
45
+ function isNewFileStatus(status) {
46
+ const code = getPrimaryStatusCode(status);
47
+ return code === '?' || code === 'A';
48
+ }
49
+ function groupFilesByStatus(files) {
50
+ const grouped = new Map();
51
+ for (const { status, file } of files) {
52
+ const code = getPrimaryStatusCode(status);
53
+ const existing = grouped.get(code) ?? [];
54
+ existing.push(file);
55
+ grouped.set(code, existing);
56
+ }
57
+ return grouped;
58
+ }
59
+ function printStatusGroups(files) {
60
+ const grouped = groupFilesByStatus(files);
61
+ for (const [status, fileList] of grouped) {
62
+ const description = getStatusDescription(status);
63
+ warn(`${description}:`);
64
+ for (const file of fileList) {
65
+ info(` ${file}`);
66
+ }
67
+ }
68
+ }
69
+ async function printUnregisteredWarnings(files, projectRoot, binaryName) {
70
+ const newFiles = files.filter((f) => isNewFileStatus(f.status));
71
+ if (newFiles.length === 0)
72
+ return;
73
+ const registrableFiles = newFiles.filter((f) => matchesRegistrablePattern(f.file, binaryName));
74
+ const registrationChecks = await Promise.all(registrableFiles.map(async (f) => ({
75
+ file: f.file,
76
+ registered: await isFileRegistered(projectRoot, f.file),
77
+ })));
78
+ const unregistered = registrationChecks.filter((f) => !f.registered);
79
+ if (unregistered.length > 0) {
80
+ info('');
81
+ warn('Potentially unregistered files:');
82
+ for (const f of unregistered) {
83
+ info(` ${f.file} — run 'fireforge register ${f.file}'`);
84
+ }
85
+ }
86
+ }
87
+ /**
88
+ * Renders raw worktree status as machine-parseable porcelain-style output.
89
+ * Each line is: STATUS<tab>FILE
90
+ */
91
+ function renderRawStatus(files) {
92
+ for (const { status, file } of files) {
93
+ process.stdout.write(`${status.trim()}\t${file}\n`);
94
+ }
95
+ }
96
+ /**
97
+ * Expands collapsed untracked directory entries into individual file entries.
98
+ * Git status may report an entire untracked directory as a single entry (e.g. "?? dir/").
99
+ * This function expands those into individual file entries so each file can be classified.
100
+ */
101
+ async function expandDirectoryEntries(files, engineDir) {
102
+ const expanded = [];
103
+ for (const entry of files) {
104
+ if (entry.file.endsWith('/') && entry.status.includes('?')) {
105
+ const individualFiles = await getUntrackedFilesInDir(engineDir, entry.file);
106
+ for (const f of individualFiles) {
107
+ expanded.push({ status: '??', file: f });
108
+ }
109
+ }
110
+ else {
111
+ expanded.push(entry);
112
+ }
113
+ }
114
+ return expanded;
115
+ }
116
+ /**
117
+ * Classifies files into patch-backed, unmanaged, or branding buckets.
118
+ */
119
+ async function classifyFiles(files, engineDir, patchesDir, binaryName) {
120
+ const manifest = await loadPatchesManifest(patchesDir);
121
+ // Build set of all patch-claimed file paths
122
+ const patchClaimedFiles = new Set();
123
+ if (manifest) {
124
+ for (const patch of manifest.patches) {
125
+ for (const f of patch.filesAffected) {
126
+ patchClaimedFiles.add(f);
127
+ }
128
+ }
129
+ }
130
+ const results = [];
131
+ for (const entry of files) {
132
+ // Branding check first
133
+ if (isBrandingManagedPath(entry.file, binaryName)) {
134
+ results.push({ ...entry, classification: 'branding' });
135
+ continue;
136
+ }
137
+ // Not in any patch → unmanaged
138
+ if (!patchClaimedFiles.has(entry.file)) {
139
+ results.push({ ...entry, classification: 'unmanaged' });
140
+ continue;
141
+ }
142
+ // File is claimed by a patch — compare content
143
+ const primaryCode = getPrimaryStatusCode(entry.status);
144
+ if (primaryCode === 'D') {
145
+ // Deleted file: patch-backed only if patch expects deletion
146
+ const expected = await computePatchedContent(patchesDir, engineDir, entry.file);
147
+ results.push({
148
+ ...entry,
149
+ classification: expected === null ? 'patch-backed' : 'unmanaged',
150
+ });
151
+ continue;
152
+ }
153
+ // File exists on disk — compare actual vs expected
154
+ try {
155
+ const [expected, actual] = await Promise.all([
156
+ computePatchedContent(patchesDir, engineDir, entry.file),
157
+ readText(join(engineDir, entry.file)),
158
+ ]);
159
+ results.push({
160
+ ...entry,
161
+ classification: actual === expected ? 'patch-backed' : 'unmanaged',
162
+ });
163
+ }
164
+ catch (error) {
165
+ verbose(`Treating ${entry.file} as unmanaged because patch-backed classification failed: ${toError(error).message}`);
166
+ // If we can't read the file, treat as unmanaged
167
+ results.push({ ...entry, classification: 'unmanaged' });
168
+ }
169
+ }
170
+ return results;
171
+ }
172
+ /**
173
+ * Runs the status command to show modified files.
174
+ * @param projectRoot - Root directory of the project
175
+ * @param options - Status display options
176
+ */
177
+ export async function statusCommand(projectRoot, options = {}) {
178
+ if (options.raw && options.unmanaged) {
179
+ throw new GeneralError('Cannot use --raw and --unmanaged together.');
180
+ }
181
+ if (!options.raw) {
182
+ intro('FireForge Status');
183
+ }
184
+ const paths = getProjectPaths(projectRoot);
185
+ const config = await loadConfig(projectRoot);
186
+ // Check if engine exists
187
+ if (!(await pathExists(paths.engine))) {
188
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
189
+ }
190
+ // Check if it's a git repository
191
+ if (!(await isGitRepository(paths.engine))) {
192
+ throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
193
+ }
194
+ const rawFiles = await getStatusWithCodes(paths.engine);
195
+ const files = await expandDirectoryEntries(rawFiles, paths.engine);
196
+ if (files.length === 0) {
197
+ info('No modified files');
198
+ outro('Working tree clean');
199
+ return;
200
+ }
201
+ // Raw mode: existing behavior
202
+ if (options.raw) {
203
+ renderRawStatus(files);
204
+ return;
205
+ }
206
+ // Patch-aware classification
207
+ const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName);
208
+ const unmanagedFiles = classified.filter((f) => f.classification === 'unmanaged');
209
+ const patchBackedFiles = classified.filter((f) => f.classification === 'patch-backed');
210
+ const brandingFiles = classified.filter((f) => f.classification === 'branding');
211
+ // --unmanaged mode: only show unmanaged
212
+ if (options.unmanaged) {
213
+ info(`${unmanagedFiles.length} unmanaged file${unmanagedFiles.length === 1 ? '' : 's'} (${files.length} total modified):\n`);
214
+ if (unmanagedFiles.length > 0) {
215
+ printStatusGroups(unmanagedFiles);
216
+ await printUnregisteredWarnings(unmanagedFiles, projectRoot, config.binaryName);
217
+ }
218
+ else {
219
+ info('No unmanaged changes');
220
+ }
221
+ outro(unmanagedFiles.length === 0
222
+ ? 'No unmanaged changes'
223
+ : `${unmanagedFiles.length} unmanaged change${unmanagedFiles.length === 1 ? '' : 's'}`);
224
+ return;
225
+ }
226
+ // Default mode: three-bucket display
227
+ info(`${files.length} modified file${files.length === 1 ? '' : 's'}:\n`);
228
+ if (unmanagedFiles.length > 0) {
229
+ warn('Unmanaged changes:');
230
+ printStatusGroups(unmanagedFiles);
231
+ await printUnregisteredWarnings(unmanagedFiles, projectRoot, config.binaryName);
232
+ }
233
+ if (patchBackedFiles.length > 0) {
234
+ if (unmanagedFiles.length > 0)
235
+ info('');
236
+ warn('Patch-backed materialized changes:');
237
+ printStatusGroups(patchBackedFiles);
238
+ }
239
+ if (brandingFiles.length > 0) {
240
+ if (unmanagedFiles.length > 0 || patchBackedFiles.length > 0)
241
+ info('');
242
+ warn('Tool-managed branding changes:');
243
+ printStatusGroups(brandingFiles);
244
+ }
245
+ if (unmanagedFiles.length === 0 && patchBackedFiles.length === 0 && brandingFiles.length === 0) {
246
+ info('No changes');
247
+ }
248
+ const parts = [];
249
+ if (unmanagedFiles.length > 0)
250
+ parts.push(`${unmanagedFiles.length} unmanaged`);
251
+ if (patchBackedFiles.length > 0)
252
+ parts.push(`${patchBackedFiles.length} patch-backed`);
253
+ if (brandingFiles.length > 0)
254
+ parts.push(`${brandingFiles.length} branding`);
255
+ outro(parts.join(', '));
256
+ }
257
+ /** Registers the status command on the CLI program. */
258
+ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
259
+ program
260
+ .command('status')
261
+ .description('Show modified files in engine/')
262
+ .option('--raw', 'Show raw worktree status without patch classification')
263
+ .option('--unmanaged', 'Show only unmanaged changes (not covered by patches or tools)')
264
+ .action(withErrorHandling(async (options) => {
265
+ await statusCommand(getProjectRoot(), options);
266
+ }));
267
+ }
268
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,12 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ import type { TestOptions } from '../types/commands/index.js';
4
+ /**
5
+ * Runs the test command to execute mach tests.
6
+ * @param projectRoot - Root directory of the project
7
+ * @param testPaths - Test file or directory paths
8
+ * @param options - Test options
9
+ */
10
+ export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions): Promise<void>;
11
+ /** Registers the test command on the CLI program. */
12
+ export declare function registerTest(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -0,0 +1,182 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { join } from 'node:path';
3
+ import { isBrandingSetup, setupBranding } from '../core/branding.js';
4
+ import { getProjectPaths, loadConfig } from '../core/config.js';
5
+ import { cleanStories } from '../core/furnace-stories.js';
6
+ import { buildArtifactMismatchMessage, buildUI, generateMozconfig, hasBuildArtifacts, testWithOutput, } from '../core/mach.js';
7
+ import { GeneralError } from '../errors/base.js';
8
+ import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
9
+ import { pathExists } from '../utils/fs.js';
10
+ import { info, intro, spinner } from '../utils/logger.js';
11
+ import { pickDefined } from '../utils/options.js';
12
+ /**
13
+ * Strips the "engine/" prefix from a path if present.
14
+ * Users may specify paths like "engine/browser/modules/..." from the project
15
+ * root, but mach test expects paths relative to the engine directory.
16
+ * @param testPath - Path as provided by the user
17
+ * @returns Path relative to the engine directory
18
+ */
19
+ function normalizeTestPath(testPath) {
20
+ if (testPath.startsWith('engine/')) {
21
+ return testPath.slice('engine/'.length);
22
+ }
23
+ if (testPath.startsWith('engine\\')) {
24
+ return testPath.slice('engine\\'.length);
25
+ }
26
+ return testPath;
27
+ }
28
+ async function assertTestPathsExist(engineDir, testPaths) {
29
+ const missingPaths = [];
30
+ for (const testPath of testPaths) {
31
+ if (!(await pathExists(join(engineDir, testPath)))) {
32
+ missingPaths.push(testPath);
33
+ }
34
+ }
35
+ if (missingPaths.length === 0) {
36
+ return;
37
+ }
38
+ throw new GeneralError(`Test path${missingPaths.length === 1 ? '' : 's'} not found under engine/: ${missingPaths.join(', ')}\n\n` +
39
+ 'If you expected these files to come from your patch stack, run "fireforge import" first.');
40
+ }
41
+ function buildUnknownTestMessage(testPaths) {
42
+ return (`mach could not discover the requested test path${testPaths.length === 1 ? '' : 's'}: ${testPaths.join(', ')}\n\n` +
43
+ 'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
44
+ 'Check the nearest test manifest (for example browser.toml or xpcshell.toml), confirm the file is listed under the correct test type, and make sure each parent moz.build registers that manifest before retrying.');
45
+ }
46
+ function buildStaleBuildMessage() {
47
+ return ('Firefox test runtime appears to be using stale build artifacts.\n\n' +
48
+ 'The failing output referenced missing branding or distribution resources, which usually means the current obj-* build does not match recent engine or branding changes.\n\n' +
49
+ 'Re-run "fireforge build --ui" or "fireforge test --build" and then retry.');
50
+ }
51
+ function hasStaleBuildArtifactsSignal(output) {
52
+ return (/chrome:\/\/branding\/locale\/brand\.properties/i.test(output) ||
53
+ /resource:\/\/\/modules\/distribution\.sys\.mjs/i.test(output) ||
54
+ /browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
55
+ }
56
+ async function prepareIncrementalTestBuild(projectRoot) {
57
+ const config = await loadConfig(projectRoot);
58
+ const paths = getProjectPaths(projectRoot);
59
+ const brandingConfig = {
60
+ name: config.name,
61
+ vendor: config.vendor,
62
+ appId: config.appId,
63
+ binaryName: config.binaryName,
64
+ };
65
+ if (!(await isBrandingSetup(paths.engine, brandingConfig))) {
66
+ const brandingSpinner = spinner('Setting up branding...');
67
+ try {
68
+ await setupBranding(paths.engine, brandingConfig);
69
+ brandingSpinner.stop('Branding configured');
70
+ }
71
+ catch (error) {
72
+ brandingSpinner.error('Failed to set up branding');
73
+ throw error;
74
+ }
75
+ }
76
+ const mozconfigSpinner = spinner('Generating mozconfig...');
77
+ try {
78
+ await generateMozconfig(paths.configs, paths.engine, config);
79
+ mozconfigSpinner.stop('mozconfig generated');
80
+ }
81
+ catch (error) {
82
+ mozconfigSpinner.error('Failed to generate mozconfig');
83
+ throw error;
84
+ }
85
+ await cleanStories(paths.engine);
86
+ return { engineDir: paths.engine };
87
+ }
88
+ function handleNonZeroTestExit(result, normalizedPaths) {
89
+ if (result.exitCode === 0 || result.exitCode === 130)
90
+ return;
91
+ const combinedOutput = `${result.stdout}\n${result.stderr}`;
92
+ if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
93
+ throw new GeneralError(buildUnknownTestMessage(normalizedPaths));
94
+ }
95
+ if (hasStaleBuildArtifactsSignal(combinedOutput)) {
96
+ throw new GeneralError(buildStaleBuildMessage());
97
+ }
98
+ if (/invalid filename/i.test(combinedOutput) ||
99
+ /chrome:\/\/mochitests.*not found/i.test(combinedOutput)) {
100
+ info('Hint: The test file may not be registered in browser.toml or jar.mn.');
101
+ info('Run "fireforge register <test-path>" to register it.');
102
+ }
103
+ throw new BuildError(`Tests failed with exit code ${result.exitCode}. Check the output above for details.`, 'mach test');
104
+ }
105
+ /**
106
+ * Runs the test command to execute mach tests.
107
+ * @param projectRoot - Root directory of the project
108
+ * @param testPaths - Test file or directory paths
109
+ * @param options - Test options
110
+ */
111
+ export async function testCommand(projectRoot, testPaths, options = {}) {
112
+ intro('FireForge Test');
113
+ const paths = getProjectPaths(projectRoot);
114
+ // Check if engine exists
115
+ if (!(await pathExists(paths.engine))) {
116
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
117
+ }
118
+ // Check for build artifacts before running tests
119
+ const buildCheck = await hasBuildArtifacts(paths.engine);
120
+ if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
121
+ throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
122
+ }
123
+ const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Tests');
124
+ if (mismatchMessage) {
125
+ throw new GeneralError(mismatchMessage);
126
+ }
127
+ if (!buildCheck.exists) {
128
+ const detail = buildCheck.objDir
129
+ ? `Build artifacts incomplete in ${buildCheck.objDir}/`
130
+ : 'No build artifacts found (obj-*/ directory missing)';
131
+ throw new GeneralError(`Tests require a completed build. ${detail}\n\n` +
132
+ "Run 'fireforge build' first, then run 'fireforge test'.");
133
+ }
134
+ // Run incremental build if requested
135
+ if (options.build) {
136
+ const { engineDir } = await prepareIncrementalTestBuild(projectRoot);
137
+ const s = spinner('Running incremental build...');
138
+ const buildExitCode = await buildUI(engineDir);
139
+ if (buildExitCode !== 0) {
140
+ s.error('Pre-test build failed');
141
+ throw new BuildError('Pre-test build failed', 'mach build faster');
142
+ }
143
+ s.stop('Build complete');
144
+ info('');
145
+ }
146
+ // Normalize test paths (strip engine/ prefix if present)
147
+ const normalizedPaths = testPaths.map(normalizeTestPath);
148
+ await assertTestPathsExist(paths.engine, normalizedPaths);
149
+ // Build extra args
150
+ const extraArgs = [];
151
+ if (options.headless) {
152
+ extraArgs.push('--headless');
153
+ }
154
+ // Log what we're doing
155
+ if (normalizedPaths.length > 0) {
156
+ info(`Running tests: ${normalizedPaths.join(', ')}`);
157
+ }
158
+ else {
159
+ info('Running all tests...');
160
+ }
161
+ info('');
162
+ let result;
163
+ try {
164
+ result = await testWithOutput(paths.engine, normalizedPaths, extraArgs);
165
+ }
166
+ catch (error) {
167
+ throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
168
+ }
169
+ handleNonZeroTestExit(result, normalizedPaths);
170
+ }
171
+ /** Registers the test command on the CLI program. */
172
+ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
173
+ program
174
+ .command('test [paths...]')
175
+ .description('Run tests via mach test')
176
+ .option('--headless', 'Run tests in headless mode')
177
+ .option('--build', 'Run incremental UI build before testing')
178
+ .action(withErrorHandling(async (paths, options) => {
179
+ await testCommand(getProjectRoot(), paths, pickDefined(options));
180
+ }));
181
+ }
182
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Measures design token coverage across modified CSS files.
3
+ * @param projectRoot - Root directory of the project
4
+ */
5
+ export declare function tokenCoverageCommand(projectRoot: string): Promise<void>;
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { getProjectPaths, loadConfig } from '../core/config.js';
3
+ import { getStatusWithCodes, isGitRepository } from '../core/git.js';
4
+ import { measureTokenCoverage } from '../core/token-coverage.js';
5
+ import { getTokensCssPath } from '../core/token-manager.js';
6
+ import { GeneralError } from '../errors/base.js';
7
+ import { pathExists } from '../utils/fs.js';
8
+ import { info, intro, outro, success, warn } from '../utils/logger.js';
9
+ /**
10
+ * Measures design token coverage across modified CSS files.
11
+ * @param projectRoot - Root directory of the project
12
+ */
13
+ export async function tokenCoverageCommand(projectRoot) {
14
+ intro('Token Coverage');
15
+ const paths = getProjectPaths(projectRoot);
16
+ if (!(await pathExists(paths.engine))) {
17
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
18
+ }
19
+ if (!(await isGitRepository(paths.engine))) {
20
+ throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
21
+ }
22
+ const config = await loadConfig(projectRoot);
23
+ const tokensCssPath = getTokensCssPath(config.binaryName);
24
+ const files = await getStatusWithCodes(paths.engine);
25
+ const cssFiles = files
26
+ .filter((f) => f.file.endsWith('.css') && f.file !== tokensCssPath)
27
+ .map((f) => f.file);
28
+ if (cssFiles.length === 0) {
29
+ info('No modified CSS files');
30
+ outro('Nothing to measure');
31
+ return;
32
+ }
33
+ const report = await measureTokenCoverage(paths.engine, cssFiles);
34
+ // Per-file breakdown
35
+ for (const entry of report.files) {
36
+ const parts = [
37
+ `tokens: ${entry.tokenUsages}`,
38
+ `allowlisted: ${entry.allowlisted}`,
39
+ `unknown: ${entry.unknownVars}`,
40
+ `raw colors: ${entry.rawColors}`,
41
+ ];
42
+ info(`${entry.file} ${parts.join(' | ')}`);
43
+ }
44
+ // Coverage calculation
45
+ const denominator = report.tokenUsages + report.unknownVarUsages + report.rawColorCount;
46
+ const coverage = denominator > 0 ? Math.round((report.tokenUsages / denominator) * 100) : 100;
47
+ info('');
48
+ const summary = `Token coverage: ${coverage}% (${report.tokenUsages} tokens / ${denominator} total) — ${report.rawColorCount} raw colors, ${report.unknownVarUsages} unknown vars`;
49
+ if (coverage === 100 && report.rawColorCount === 0) {
50
+ success(summary);
51
+ }
52
+ else {
53
+ warn(summary);
54
+ }
55
+ outro(`${report.filesScanned} CSS file${report.filesScanned === 1 ? '' : 's'} scanned`);
56
+ }
57
+ //# sourceMappingURL=token-coverage.js.map
@@ -0,0 +1,14 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ import type { TokenAddOptions } from '../types/commands/index.js';
4
+ /**
5
+ * Adds a design token to the CSS file and documentation.
6
+ *
7
+ * @param projectRoot - Root directory of the project
8
+ * @param tokenName - Full token name including prefix
9
+ * @param value - CSS value
10
+ * @param options - Command options
11
+ */
12
+ export declare function tokenAddCommand(projectRoot: string, tokenName: string, value: string, options: TokenAddOptions): Promise<void>;
13
+ /** Registers token management commands on the CLI program. */
14
+ export declare function registerToken(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -0,0 +1,121 @@
1
+ import { loadConfig } from '../core/config.js';
2
+ import { loadFurnaceConfig } from '../core/furnace-config.js';
3
+ import { addToken, getTokensCssPath, validateTokenAdd, } from '../core/token-manager.js';
4
+ import { InvalidArgumentError } from '../errors/base.js';
5
+ import { toError } from '../utils/errors.js';
6
+ import { info, intro, outro, success, warn } from '../utils/logger.js';
7
+ import { pickDefined } from '../utils/options.js';
8
+ import { normalizeTokenName } from '../utils/validation.js';
9
+ import { tokenCoverageCommand } from './token-coverage.js';
10
+ async function normalizeTokenNameForProject(projectRoot, rawTokenName) {
11
+ if (rawTokenName.startsWith('--')) {
12
+ return normalizeTokenName(rawTokenName);
13
+ }
14
+ try {
15
+ const furnaceConfig = await loadFurnaceConfig(projectRoot);
16
+ if (furnaceConfig.tokenPrefix) {
17
+ const strippedPrefix = furnaceConfig.tokenPrefix.replace(/^--/, '').replace(/-$/, '');
18
+ const strippedName = rawTokenName.replace(/^--/, '');
19
+ return `--${strippedPrefix}-${strippedName}`;
20
+ }
21
+ }
22
+ catch (error) {
23
+ warn(`Falling back to generic token normalization because furnace.json could not be loaded: ${toError(error).message}`);
24
+ }
25
+ return normalizeTokenName(rawTokenName);
26
+ }
27
+ /**
28
+ * Adds a design token to the CSS file and documentation.
29
+ *
30
+ * @param projectRoot - Root directory of the project
31
+ * @param tokenName - Full token name including prefix
32
+ * @param value - CSS value
33
+ * @param options - Command options
34
+ */
35
+ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
36
+ intro('Token Add');
37
+ // Normalize token name using the configured Furnace token prefix when the
38
+ // user supplied a bare token name like "canvas-gap".
39
+ tokenName = await normalizeTokenNameForProject(projectRoot, tokenName);
40
+ // Validate mode
41
+ const validModes = ['auto', 'static', 'override'];
42
+ if (!validModes.includes(options.mode)) {
43
+ throw new InvalidArgumentError(`Invalid mode "${options.mode}". Must be one of: ${validModes.join(', ')}`, 'mode');
44
+ }
45
+ if (options.dryRun) {
46
+ await validateTokenAdd(projectRoot, {
47
+ tokenName,
48
+ value,
49
+ category: options.category,
50
+ mode: options.mode,
51
+ ...(options.description !== undefined ? { description: options.description } : {}),
52
+ ...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
53
+ dryRun: true,
54
+ });
55
+ info('[dry-run] Would add token:');
56
+ info(` Name: ${tokenName}`);
57
+ info(` Value: ${value}`);
58
+ info(` Category: ${options.category}`);
59
+ info(` Mode: ${options.mode}`);
60
+ if (options.description)
61
+ info(` Description: ${options.description}`);
62
+ if (options.darkValue)
63
+ info(` Dark value: ${options.darkValue}`);
64
+ outro('Dry run complete');
65
+ return;
66
+ }
67
+ const result = await addToken(projectRoot, {
68
+ tokenName,
69
+ value,
70
+ category: options.category,
71
+ mode: options.mode,
72
+ ...(options.description !== undefined ? { description: options.description } : {}),
73
+ ...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
74
+ });
75
+ if (result.skipped) {
76
+ info(`Token ${tokenName} already exists (skipped)`);
77
+ }
78
+ else {
79
+ const forgeConfig = await loadConfig(projectRoot);
80
+ const tokensCssFile = getTokensCssPath(forgeConfig.binaryName).split('/').pop();
81
+ if (result.cssAdded)
82
+ success(`Added ${tokenName} to ${tokensCssFile}`);
83
+ if (result.docsAdded)
84
+ success(`Added ${tokenName} to SRC_TOKENS.md`);
85
+ if (result.unmappedAdded)
86
+ info(`Added to unmapped tokens table (literal value)`);
87
+ if (result.countUpdated)
88
+ info(`Updated mode count in documentation`);
89
+ }
90
+ outro('Done');
91
+ }
92
+ /** Registers token management commands on the CLI program. */
93
+ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
94
+ const token = program.command('token').description('Design token management');
95
+ token
96
+ .command('add <token-name> <value>')
97
+ .description('Add a design token to CSS and documentation')
98
+ .requiredOption('--category <cat>', 'Token category (e.g., "Colors — Canvas", "Spacing")')
99
+ .requiredOption('--mode <mode>', 'Dark mode behavior: auto, static, or override')
100
+ .option('--description <desc>', 'Comment description for the CSS file')
101
+ .option('--dark-value <val>', 'Dark mode value (required if mode is "override")')
102
+ .option('--dry-run', 'Show what would be changed without writing')
103
+ .action(withErrorHandling(async (tokenName, value, options) => {
104
+ await tokenAddCommand(getProjectRoot(), tokenName, value, {
105
+ category: options.category,
106
+ mode: options.mode,
107
+ ...pickDefined({
108
+ description: options.description,
109
+ darkValue: options.darkValue,
110
+ dryRun: options.dryRun,
111
+ }),
112
+ });
113
+ }));
114
+ token
115
+ .command('coverage')
116
+ .description('Measure design token usage across modified CSS files')
117
+ .action(withErrorHandling(async () => {
118
+ await tokenCoverageCommand(getProjectRoot());
119
+ }));
120
+ }
121
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ /**
4
+ * Runs the watch command for auto-rebuilding.
5
+ * @param projectRoot - Root directory of the project
6
+ */
7
+ export declare function watchCommand(projectRoot: string): Promise<void>;
8
+ /** Registers the watch command on the CLI program. */
9
+ export declare function registerWatch(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;