@hominis/fireforge 0.39.0 → 0.41.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 (279) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +35 -5
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +27 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +27 -9
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +30 -10
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +73 -51
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint-report.d.ts +21 -0
  45. package/dist/src/commands/lint-report.js +62 -0
  46. package/dist/src/commands/lint.js +26 -22
  47. package/dist/src/commands/manifest.js +3 -1
  48. package/dist/src/commands/package.js +9 -16
  49. package/dist/src/commands/patch/compact.js +5 -5
  50. package/dist/src/commands/patch/delete.js +5 -5
  51. package/dist/src/commands/patch/lint-ignore.js +3 -2
  52. package/dist/src/commands/patch/move-files-create.js +22 -4
  53. package/dist/src/commands/patch/move-files-into.js +14 -10
  54. package/dist/src/commands/patch/move-files.js +6 -5
  55. package/dist/src/commands/patch/rename.d.ts +2 -2
  56. package/dist/src/commands/patch/rename.js +152 -51
  57. package/dist/src/commands/patch/reorder.d.ts +9 -0
  58. package/dist/src/commands/patch/reorder.js +14 -10
  59. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  60. package/dist/src/commands/patch/split-plan.js +22 -12
  61. package/dist/src/commands/patch/split.d.ts +1 -1
  62. package/dist/src/commands/patch/split.js +23 -9
  63. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  64. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  65. package/dist/src/commands/patch/staged-dependency.js +112 -14
  66. package/dist/src/commands/re-export-adjacent.d.ts +34 -0
  67. package/dist/src/commands/re-export-adjacent.js +105 -0
  68. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  69. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  70. package/dist/src/commands/re-export-drift.d.ts +74 -0
  71. package/dist/src/commands/re-export-drift.js +234 -0
  72. package/dist/src/commands/re-export-files.js +5 -3
  73. package/dist/src/commands/re-export-lint.d.ts +97 -0
  74. package/dist/src/commands/re-export-lint.js +210 -0
  75. package/dist/src/commands/re-export-options.js +11 -0
  76. package/dist/src/commands/re-export-register.d.ts +18 -0
  77. package/dist/src/commands/re-export-register.js +61 -0
  78. package/dist/src/commands/re-export-scan.js +13 -7
  79. package/dist/src/commands/re-export-single.d.ts +20 -0
  80. package/dist/src/commands/re-export-single.js +261 -0
  81. package/dist/src/commands/re-export.d.ts +0 -4
  82. package/dist/src/commands/re-export.js +123 -289
  83. package/dist/src/commands/rebase/abort.js +13 -4
  84. package/dist/src/commands/rebase/continue.js +11 -4
  85. package/dist/src/commands/rebase/index.js +19 -29
  86. package/dist/src/commands/rebase/patch-loop.js +12 -0
  87. package/dist/src/commands/reset.js +4 -23
  88. package/dist/src/commands/resolve.js +2 -9
  89. package/dist/src/commands/run.js +29 -23
  90. package/dist/src/commands/source.js +3 -9
  91. package/dist/src/commands/status-check.d.ts +48 -0
  92. package/dist/src/commands/status-check.js +106 -0
  93. package/dist/src/commands/status-json.d.ts +31 -0
  94. package/dist/src/commands/status-json.js +76 -0
  95. package/dist/src/commands/status-output.d.ts +1 -0
  96. package/dist/src/commands/status-output.js +10 -1
  97. package/dist/src/commands/status-ownership.d.ts +52 -0
  98. package/dist/src/commands/status-ownership.js +57 -0
  99. package/dist/src/commands/status.js +190 -170
  100. package/dist/src/commands/test-build-phase.d.ts +19 -0
  101. package/dist/src/commands/test-build-phase.js +168 -0
  102. package/dist/src/commands/test-diagnose.js +30 -2
  103. package/dist/src/commands/test-modes.d.ts +9 -1
  104. package/dist/src/commands/test-modes.js +46 -13
  105. package/dist/src/commands/test-register.js +59 -27
  106. package/dist/src/commands/test-run.d.ts +29 -3
  107. package/dist/src/commands/test-run.js +59 -7
  108. package/dist/src/commands/test-verdict.d.ts +32 -0
  109. package/dist/src/commands/test-verdict.js +48 -0
  110. package/dist/src/commands/test.d.ts +9 -0
  111. package/dist/src/commands/test.js +100 -123
  112. package/dist/src/commands/token-coverage.js +4 -9
  113. package/dist/src/commands/token.js +4 -1
  114. package/dist/src/commands/tree.d.ts +19 -0
  115. package/dist/src/commands/tree.js +320 -0
  116. package/dist/src/commands/verify.js +2 -2
  117. package/dist/src/commands/watch.js +22 -21
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.d.ts +11 -0
  120. package/dist/src/core/ast-utils.js +11 -0
  121. package/dist/src/core/build-audit-platform.js +6 -1
  122. package/dist/src/core/build-audit-registration.js +2 -0
  123. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  124. package/dist/src/core/build-audit-resolve.js +34 -13
  125. package/dist/src/core/build-audit.js +4 -20
  126. package/dist/src/core/build-baseline-types.d.ts +29 -8
  127. package/dist/src/core/build-baseline-types.js +2 -1
  128. package/dist/src/core/build-baseline.d.ts +10 -2
  129. package/dist/src/core/build-baseline.js +39 -8
  130. package/dist/src/core/build-prepare.d.ts +4 -0
  131. package/dist/src/core/build-prepare.js +7 -1
  132. package/dist/src/core/config-paths.d.ts +1 -1
  133. package/dist/src/core/config-paths.js +2 -0
  134. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  135. package/dist/src/core/config-validate.js +45 -30
  136. package/dist/src/core/config.js +16 -3
  137. package/dist/src/core/coverage-extend.d.ts +89 -0
  138. package/dist/src/core/coverage-extend.js +188 -0
  139. package/dist/src/core/destructive.js +3 -1
  140. package/dist/src/core/discard-baseline.js +18 -4
  141. package/dist/src/core/engine-precondition.d.ts +35 -0
  142. package/dist/src/core/engine-precondition.js +50 -0
  143. package/dist/src/core/engine-session-lock.d.ts +38 -1
  144. package/dist/src/core/engine-session-lock.js +78 -4
  145. package/dist/src/core/file-lock.d.ts +7 -0
  146. package/dist/src/core/file-lock.js +20 -30
  147. package/dist/src/core/firefox.js +5 -2
  148. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  149. package/dist/src/core/furnace-apply-helpers.js +12 -3
  150. package/dist/src/core/furnace-apply-output.js +9 -0
  151. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  152. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  153. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  154. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  155. package/dist/src/core/furnace-apply.d.ts +16 -4
  156. package/dist/src/core/furnace-apply.js +39 -43
  157. package/dist/src/core/furnace-config.js +2 -0
  158. package/dist/src/core/furnace-css-fragments.js +2 -0
  159. package/dist/src/core/furnace-jsconfig.js +14 -7
  160. package/dist/src/core/furnace-registration-ast.js +8 -8
  161. package/dist/src/core/furnace-registration-remove.js +6 -3
  162. package/dist/src/core/furnace-registration.js +9 -12
  163. package/dist/src/core/furnace-rollback.js +1 -1
  164. package/dist/src/core/furnace-scanner.js +2 -1
  165. package/dist/src/core/furnace-stale-export.js +2 -1
  166. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  167. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  168. package/dist/src/core/furnace-validate-helpers.js +33 -1
  169. package/dist/src/core/furnace-validate-registration.js +16 -0
  170. package/dist/src/core/furnace-validate.js +4 -2
  171. package/dist/src/core/git-diff.js +37 -2
  172. package/dist/src/core/lint-cache.d.ts +15 -5
  173. package/dist/src/core/lint-cache.js +49 -14
  174. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  175. package/dist/src/core/mach-build-artifacts.js +108 -2
  176. package/dist/src/core/mach-resource-shim.js +1 -0
  177. package/dist/src/core/marionette-port.d.ts +16 -0
  178. package/dist/src/core/marionette-port.js +93 -8
  179. package/dist/src/core/marionette-preflight.js +6 -5
  180. package/dist/src/core/patch-apply.d.ts +20 -13
  181. package/dist/src/core/patch-apply.js +32 -33
  182. package/dist/src/core/patch-export.d.ts +2 -2
  183. package/dist/src/core/patch-export.js +4 -6
  184. package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
  185. package/dist/src/core/patch-lint-checkjs.js +132 -8
  186. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  187. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  188. package/dist/src/core/patch-lint-creators.js +45 -0
  189. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  190. package/dist/src/core/patch-lint-cross.js +16 -32
  191. package/dist/src/core/patch-lint-css.js +64 -16
  192. package/dist/src/core/patch-lint-diff.d.ts +8 -0
  193. package/dist/src/core/patch-lint-diff.js +31 -0
  194. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  195. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  196. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  197. package/dist/src/core/patch-lint-module-registration.js +88 -0
  198. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  199. package/dist/src/core/patch-lint-ownership.d.ts +21 -0
  200. package/dist/src/core/patch-lint-ownership.js +30 -0
  201. package/dist/src/core/patch-lint-reexports.d.ts +2 -1
  202. package/dist/src/core/patch-lint-reexports.js +1 -0
  203. package/dist/src/core/patch-lint.d.ts +31 -0
  204. package/dist/src/core/patch-lint.js +114 -8
  205. package/dist/src/core/patch-lock.d.ts +12 -1
  206. package/dist/src/core/patch-lock.js +40 -4
  207. package/dist/src/core/patch-manifest-consistency.js +15 -3
  208. package/dist/src/core/patch-manifest.d.ts +1 -1
  209. package/dist/src/core/patch-parse.d.ts +8 -0
  210. package/dist/src/core/patch-parse.js +10 -0
  211. package/dist/src/core/patch-policy.d.ts +7 -0
  212. package/dist/src/core/patch-policy.js +4 -1
  213. package/dist/src/core/patch-transform.d.ts +10 -0
  214. package/dist/src/core/patch-transform.js +13 -2
  215. package/dist/src/core/rebase-session.d.ts +44 -2
  216. package/dist/src/core/rebase-session.js +114 -17
  217. package/dist/src/core/register-browser-content.js +2 -1
  218. package/dist/src/core/register-shared-css.js +2 -1
  219. package/dist/src/core/smoke-patterns.d.ts +24 -16
  220. package/dist/src/core/smoke-patterns.js +26 -14
  221. package/dist/src/core/status-binary.d.ts +55 -0
  222. package/dist/src/core/status-binary.js +124 -0
  223. package/dist/src/core/status-classify.d.ts +22 -1
  224. package/dist/src/core/status-classify.js +112 -70
  225. package/dist/src/core/test-harness-crash.d.ts +32 -1
  226. package/dist/src/core/test-harness-crash.js +59 -4
  227. package/dist/src/core/test-path-scope.js +2 -0
  228. package/dist/src/core/test-stale-check.js +1 -20
  229. package/dist/src/core/token-category.d.ts +40 -0
  230. package/dist/src/core/token-category.js +264 -0
  231. package/dist/src/core/token-manager.d.ts +7 -0
  232. package/dist/src/core/token-manager.js +48 -196
  233. package/dist/src/core/toolchain-preflight.js +24 -26
  234. package/dist/src/core/tree-cow.d.ts +18 -0
  235. package/dist/src/core/tree-cow.js +71 -0
  236. package/dist/src/core/tree-guard.d.ts +35 -0
  237. package/dist/src/core/tree-guard.js +179 -0
  238. package/dist/src/core/tree-store.d.ts +129 -0
  239. package/dist/src/core/tree-store.js +452 -0
  240. package/dist/src/core/typecheck-shim.d.ts +14 -0
  241. package/dist/src/core/typecheck-shim.js +41 -2
  242. package/dist/src/core/typecheck.js +2 -1
  243. package/dist/src/core/wire-destroy.js +14 -3
  244. package/dist/src/core/wire-init.js +37 -40
  245. package/dist/src/core/wire-subscript.js +11 -3
  246. package/dist/src/core/wire-utils.js +4 -4
  247. package/dist/src/core/xpcshell-appdir.js +3 -0
  248. package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
  249. package/dist/src/core/xpcshell-profile-dir.js +45 -0
  250. package/dist/src/errors/base.d.ts +13 -0
  251. package/dist/src/errors/base.js +13 -0
  252. package/dist/src/errors/git.d.ts +8 -0
  253. package/dist/src/errors/git.js +17 -0
  254. package/dist/src/errors/rebase.d.ts +13 -0
  255. package/dist/src/errors/rebase.js +17 -0
  256. package/dist/src/index.d.ts +2 -0
  257. package/dist/src/index.js +1 -0
  258. package/dist/src/types/commands/options.d.ts +119 -3
  259. package/dist/src/types/commands/patches.d.ts +8 -0
  260. package/dist/src/types/config.d.ts +19 -0
  261. package/dist/src/types/furnace.d.ts +7 -0
  262. package/dist/src/utils/build-info.d.ts +31 -0
  263. package/dist/src/utils/build-info.js +98 -0
  264. package/dist/src/utils/concurrency.d.ts +8 -0
  265. package/dist/src/utils/concurrency.js +21 -0
  266. package/dist/src/utils/errors.d.ts +39 -0
  267. package/dist/src/utils/errors.js +55 -0
  268. package/dist/src/utils/fs.js +5 -12
  269. package/dist/src/utils/logger.d.ts +14 -2
  270. package/dist/src/utils/logger.js +43 -13
  271. package/dist/src/utils/options.d.ts +21 -4
  272. package/dist/src/utils/options.js +26 -6
  273. package/dist/src/utils/package-root.js +7 -4
  274. package/dist/src/utils/process-group.js +13 -2
  275. package/dist/src/utils/stdio-drain.d.ts +28 -0
  276. package/dist/src/utils/stdio-drain.js +54 -0
  277. package/dist/src/utils/validation.d.ts +33 -2
  278. package/dist/src/utils/validation.js +57 -2
  279. package/package.json +13 -13
@@ -23,13 +23,11 @@
23
23
  * plugin, etc.) can legitimately have a fresh `dist/` with no
24
24
  * FireForge-recorded baseline update.
25
25
  */
26
- import { createHash } from 'node:crypto';
27
- import { readFile } from 'node:fs/promises';
28
- import { join } from 'node:path';
29
26
  import { toError } from '../utils/errors.js';
30
27
  import { verbose, warn } from '../utils/logger.js';
31
28
  import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
32
29
  import { readBuildBaseline } from './build-baseline.js';
30
+ import { hashEngineFile } from './coverage-extend.js';
33
31
  import { collectChangedEnginePaths } from './engine-changes.js';
34
32
  export { isXpcomManifestPath };
35
33
  /** Cap on the number of changed paths rendered inline. */
@@ -83,23 +81,6 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
83
81
  const truncated = Math.max(0, packageable.length - head.length);
84
82
  return { stale: true, changedPaths: head, truncated, baseline };
85
83
  }
86
- /**
87
- * Reads a file under the engine directory and returns a hex-encoded
88
- * SHA-256 of its contents, matching the hash the baseline writer
89
- * produces. Returns `undefined` on any IO error (missing file,
90
- * permission denied, etc.) so the caller can treat the path as still
91
- * stale rather than crashing the preflight.
92
- */
93
- async function hashEngineFile(engineDir, relPath) {
94
- try {
95
- const buffer = await readFile(join(engineDir, relPath));
96
- return createHash('sha256').update(buffer).digest('hex');
97
- }
98
- catch (error) {
99
- verbose(`Stale-build preflight: could not hash ${relPath} for baseline comparison — ${toError(error).message}`);
100
- return undefined;
101
- }
102
- }
103
84
  /**
104
85
  * Formats a human-readable warning body from a {@link StaleBuildResult}.
105
86
  * Kept separate from the probe so test code can assert on the structured
@@ -0,0 +1,40 @@
1
+ /**
2
+ * True when `lines` contain a category header (single-line or multi-line
3
+ * banner shape) whose name EQUALS `category`. Shared by the pre-add
4
+ * assertion, the banner creation path, and the section finder so all
5
+ * agree on what "exists" means.
6
+ */
7
+ export declare function categoryHeaderExists(lines: string[], category: string): boolean;
8
+ /**
9
+ * Asserts the category banner exists (or that creation was requested).
10
+ * Throws a GeneralError naming the available categories and the
11
+ * `--create-category` remedy otherwise.
12
+ */
13
+ export declare function assertTokenCategoryExists(engineDir: string, tokensCssPath: string, category: string, createCategory?: boolean): Promise<void>;
14
+ /**
15
+ * Splices a new single-line category banner ("= Name =" comment shape, the
16
+ * same format `discoverCategoryHeaders` recognises) just before the closing
17
+ * brace of the `:root` block, making the new category the last section.
18
+ * Mutates `lines` in place.
19
+ */
20
+ export declare function declareCategoryBanner(lines: string[], category: string, tokensCssPath: string): void;
21
+ /** Locates and bounds the named category section. Throws when absent. */
22
+ export declare function findCategorySection(lines: string[], category: string, tokensCssPath: string): {
23
+ categoryLine: number;
24
+ sectionEnd: number;
25
+ };
26
+ /** Location of an existing base-`:root` token declaration. */
27
+ export interface TokenDeclarationLocation {
28
+ /** 1-based line number in the tokens CSS file. */
29
+ line: number;
30
+ /** Nearest enclosing category banner name, when one precedes the declaration. */
31
+ category?: string;
32
+ }
33
+ /**
34
+ * Finds an existing declaration of `tokenName` inside the base `:root`
35
+ * block. Dark `@media` and `:root[variant]` companion blocks are
36
+ * deliberately excluded — they mirror the base declaration, they do not
37
+ * own the token. Comment content never matches, and the name match is
38
+ * exact (`--foo-bar` does not match `--x-foo-bar`).
39
+ */
40
+ export declare function findTokenDeclarationInRoot(lines: string[], tokenName: string): TokenDeclarationLocation | undefined;
@@ -0,0 +1,264 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Category banner and token-declaration helpers for the tokens CSS file
4
+ * (FORGE G3). Split out of `token-manager.ts` to stay inside the per-file
5
+ * line budget.
6
+ *
7
+ * Banner matching is EXACT: the text between the `=` runs (single-line
8
+ * shape) or on the block line (multi-line shape) must equal the category
9
+ * after trimming. The pre-0.40.0 substring match let a TOC comment or a
10
+ * longer banner (`Colors — Canvas` for `Colors`) satisfy the lookup, so
11
+ * `token add` wrote into the wrong section — or no-oped for 19
12
+ * consecutive calls — with exit 0.
13
+ */
14
+ import { join } from 'node:path';
15
+ import { GeneralError } from '../errors/base.js';
16
+ import { pathExists, readText } from '../utils/fs.js';
17
+ import { escapeRegex } from '../utils/regex.js';
18
+ function singleLineBannerPattern(category) {
19
+ return new RegExp(`\\/\\*\\s*=+\\s*${escapeRegex(category)}\\s*=+\\s*\\*\\/`);
20
+ }
21
+ function multiLineBlockNameMatches(blockLine, category) {
22
+ return blockLine.replace(/^\s*\*\s*/, '').trim() === category;
23
+ }
24
+ /**
25
+ * True when `lines` contain a category header (single-line or multi-line
26
+ * banner shape) whose name EQUALS `category`. Shared by the pre-add
27
+ * assertion, the banner creation path, and the section finder so all
28
+ * agree on what "exists" means.
29
+ */
30
+ export function categoryHeaderExists(lines, category) {
31
+ const singleLinePattern = singleLineBannerPattern(category);
32
+ for (let i = 0; i < lines.length; i++) {
33
+ const line = lines[i] ?? '';
34
+ if (singleLinePattern.test(line)) {
35
+ return true;
36
+ }
37
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
38
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
39
+ const blockLine = lines[j] ?? '';
40
+ if (multiLineBlockNameMatches(blockLine, category)) {
41
+ return true;
42
+ }
43
+ if (/\*\//.test(blockLine))
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Scans a tokens CSS file for category header comments and returns the
52
+ * category names in document order. Used to enrich the "category not
53
+ * found" error body with concrete alternatives the operator can copy.
54
+ *
55
+ * Mirrors the shapes `findCategorySection` recognises:
56
+ * - Single-line: `/* = Foo = *\/`
57
+ * - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
58
+ * closing `*\/`.
59
+ *
60
+ * This helper exists as a pure inspector; it never throws on malformed
61
+ * headers and silently skips shapes it cannot parse.
62
+ */
63
+ function discoverCategoryHeaders(lines) {
64
+ const categories = new Set();
65
+ const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i] ?? '';
68
+ const singleMatch = singleLinePattern.exec(line);
69
+ if (singleMatch?.[1]) {
70
+ const extracted = singleMatch[1].trim();
71
+ if (extracted.length > 0)
72
+ categories.add(extracted);
73
+ continue;
74
+ }
75
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
76
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
77
+ const blockLine = lines[j] ?? '';
78
+ if (/\*\//.test(blockLine))
79
+ break;
80
+ const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
81
+ if (trimmed.length === 0)
82
+ continue;
83
+ if (/^=+$/.test(trimmed))
84
+ continue;
85
+ categories.add(trimmed);
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ return [...categories];
91
+ }
92
+ /**
93
+ * Asserts the category banner exists (or that creation was requested).
94
+ * Throws a GeneralError naming the available categories and the
95
+ * `--create-category` remedy otherwise.
96
+ */
97
+ export async function assertTokenCategoryExists(engineDir, tokensCssPath, category, createCategory = false) {
98
+ const filePath = join(engineDir, tokensCssPath);
99
+ if (!(await pathExists(filePath))) {
100
+ throw new GeneralError(`Token CSS file not found: ${tokensCssPath}`);
101
+ }
102
+ const content = await readText(filePath);
103
+ const lines = content.split('\n');
104
+ if (categoryHeaderExists(lines, category))
105
+ return;
106
+ // The write path declares the banner in the same edit as the token
107
+ // insertion, so a missing category is fine when creation was requested.
108
+ if (createCategory)
109
+ return;
110
+ const discoveredCategories = discoverCategoryHeaders(lines);
111
+ const available = discoveredCategories.length > 0
112
+ ? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
113
+ : 'The file currently has no category headers.';
114
+ throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
115
+ `${available}\n\n` +
116
+ 'Categories are declared by comment headers. Single-line shape: /* = My Category = */. ' +
117
+ 'Multi-line shape: /* =============\\n * My Category\\n * ============= */.\n\n' +
118
+ 'Re-run with --create-category to declare the banner and insert the token in one step.');
119
+ }
120
+ /**
121
+ * Splices a new single-line category banner ("= Name =" comment shape, the
122
+ * same format `discoverCategoryHeaders` recognises) just before the closing
123
+ * brace of the `:root` block, making the new category the last section.
124
+ * Mutates `lines` in place.
125
+ */
126
+ export function declareCategoryBanner(lines, category, tokensCssPath) {
127
+ const bounds = findBaseRootBounds(lines);
128
+ if (bounds === null) {
129
+ throw new GeneralError(`Cannot create category "${category}": no :root block found in ${tokensCssPath}. ` +
130
+ 'Run "fireforge furnace init --force" to re-scaffold the tokens CSS file.');
131
+ }
132
+ if (bounds.close === -1) {
133
+ throw new GeneralError(`Cannot create category "${category}": the :root block in ${tokensCssPath} never closes.`);
134
+ }
135
+ lines.splice(bounds.close, 0, '', ` /* = ${category} = */`);
136
+ }
137
+ /** Locates and bounds the named category section. Throws when absent. */
138
+ export function findCategorySection(lines, category, tokensCssPath) {
139
+ const singleLinePattern = singleLineBannerPattern(category);
140
+ let categoryLine = -1;
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const line = lines[i] ?? '';
143
+ // Check single-line format: /* = Category = */
144
+ if (singleLinePattern.test(line)) {
145
+ categoryLine = i;
146
+ break;
147
+ }
148
+ // Check multi-line format: line opens a block comment with === but does NOT close it
149
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
150
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
151
+ const blockLine = lines[j] ?? '';
152
+ if (multiLineBlockNameMatches(blockLine, category)) {
153
+ categoryLine = i;
154
+ break;
155
+ }
156
+ if (/\*\//.test(blockLine))
157
+ break;
158
+ }
159
+ if (categoryLine !== -1)
160
+ break;
161
+ }
162
+ }
163
+ if (categoryLine === -1) {
164
+ const discoveredCategories = discoverCategoryHeaders(lines);
165
+ const available = discoveredCategories.length > 0
166
+ ? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
167
+ : 'The file currently has no category headers.';
168
+ throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
169
+ `${available}\n\n` +
170
+ 'Add a header by hand inside the :root block (format: "/* = My Category = */") or re-run "fireforge furnace init --force" to re-seed the default categories.');
171
+ }
172
+ // Find the end of this category section (next section header or closing })
173
+ // Skip past the current header block first
174
+ let scanStart = categoryLine + 1;
175
+ for (let i = categoryLine + 1; i < lines.length; i++) {
176
+ const line = lines[i] ?? '';
177
+ if (/^\s*\/\*\s*=/.test(line) || /^\s*\*\s*=/.test(line) || /^\s*\*\//.test(line)) {
178
+ scanStart = i + 1;
179
+ continue;
180
+ }
181
+ break;
182
+ }
183
+ let sectionEnd = lines.length;
184
+ for (let i = scanStart; i < lines.length; i++) {
185
+ const line = lines[i] ?? '';
186
+ if (/\/\*\s*=.*=\s*\*\//.test(line) ||
187
+ (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) ||
188
+ /^\s*\}/.test(line)) {
189
+ sectionEnd = i;
190
+ break;
191
+ }
192
+ }
193
+ return { categoryLine, sectionEnd };
194
+ }
195
+ /** 0-based open/close line indices of the base `:root {` block. */
196
+ function findBaseRootBounds(lines) {
197
+ let open = -1;
198
+ for (let i = 0; i < lines.length; i++) {
199
+ if (/:root\s*\{/.test(lines[i] ?? '')) {
200
+ open = i;
201
+ break;
202
+ }
203
+ }
204
+ if (open === -1)
205
+ return null;
206
+ for (let i = open + 1; i < lines.length; i++) {
207
+ if (/^\s*\}/.test(lines[i] ?? '')) {
208
+ return { open, close: i };
209
+ }
210
+ }
211
+ return { open, close: -1 };
212
+ }
213
+ /** Masks block-comment content per line so declarations inside comments never match. */
214
+ function maskCommentLines(lines) {
215
+ const masked = lines.join('\n').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
216
+ return masked.split('\n');
217
+ }
218
+ /**
219
+ * Finds an existing declaration of `tokenName` inside the base `:root`
220
+ * block. Dark `@media` and `:root[variant]` companion blocks are
221
+ * deliberately excluded — they mirror the base declaration, they do not
222
+ * own the token. Comment content never matches, and the name match is
223
+ * exact (`--foo-bar` does not match `--x-foo-bar`).
224
+ */
225
+ export function findTokenDeclarationInRoot(lines, tokenName) {
226
+ const bounds = findBaseRootBounds(lines);
227
+ if (bounds === null || bounds.close === -1)
228
+ return undefined;
229
+ const masked = maskCommentLines(lines);
230
+ const declPattern = new RegExp(`^\\s*${escapeRegex(tokenName)}\\s*:`);
231
+ for (let i = bounds.open + 1; i < bounds.close; i++) {
232
+ if (!declPattern.test(masked[i] ?? ''))
233
+ continue;
234
+ const category = findSectionNameAbove(lines, i);
235
+ return category === undefined ? { line: i + 1 } : { line: i + 1, category };
236
+ }
237
+ return undefined;
238
+ }
239
+ /** Nearest category banner name above `lineIndex`, if any. */
240
+ function findSectionNameAbove(lines, lineIndex) {
241
+ const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
242
+ for (let i = lineIndex - 1; i >= 0; i--) {
243
+ const line = lines[i] ?? '';
244
+ const singleMatch = singleLinePattern.exec(line);
245
+ if (singleMatch?.[1]) {
246
+ const extracted = singleMatch[1].trim();
247
+ if (extracted.length > 0 && !/^=+$/.test(extracted))
248
+ return extracted;
249
+ continue;
250
+ }
251
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
252
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
253
+ const blockLine = lines[j] ?? '';
254
+ if (/\*\//.test(blockLine))
255
+ break;
256
+ const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
257
+ if (trimmed.length === 0 || /^=+$/.test(trimmed))
258
+ continue;
259
+ return trimmed;
260
+ }
261
+ }
262
+ }
263
+ return undefined;
264
+ }
@@ -1,3 +1,4 @@
1
+ import { type TokenDeclarationLocation } from './token-category.js';
1
2
  /**
2
3
  * Dark mode behavior for a token.
3
4
  */
@@ -47,6 +48,12 @@ export interface AddTokenResult {
47
48
  skipped: boolean;
48
49
  /** Whether a new category banner was declared by this add. */
49
50
  categoryCreated?: boolean;
51
+ /**
52
+ * When the add skipped because the token already lives in the TARGET
53
+ * category, names where the existing base-`:root` declaration sits so
54
+ * the skip message can point at it (FORGE G3).
55
+ */
56
+ skippedExisting?: TokenDeclarationLocation;
50
57
  }
51
58
  /** Returns the token CSS path relative to engine root for a given binary name. */
52
59
  export declare function getTokensCssPath(binaryName: string): string;