@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
@@ -0,0 +1,179 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Read-only guard for verification trees (FORGE G15).
4
+ *
5
+ * A tree is a snapshot: its `patches/` and `components/` are copies with
6
+ * no merge-back model, so any command that mutates project or engine
7
+ * state must run in the primary tree. Enforcement is DEFAULT-DENY via an
8
+ * explicit verdict table over every top-level command — a newly added
9
+ * command is refused inside trees until its author classifies it (the
10
+ * drift test in `tree-guard.test.ts` fails otherwise) — and lives in ONE
11
+ * commander `preAction` hook installed by `createProgram()`, so the
12
+ * guard fires no matter how the cwd ended up inside a tree (`tree exec`
13
+ * or a plain `cd`).
14
+ */
15
+ import { existsSync } from 'node:fs';
16
+ import { dirname, join, resolve } from 'node:path';
17
+ import { GeneralError } from '../errors/base.js';
18
+ import { getTreeMarkerPath, readTreeMarkerState, } from './tree-store.js';
19
+ /**
20
+ * Per-command verdicts. `allowed` runs unconditionally; `refused` never
21
+ * runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
22
+ * the invoked subcommand chain, parsed options, and the tree marker.
23
+ */
24
+ export const TREE_COMMAND_VERDICTS = {
25
+ setup: 'refused',
26
+ source: 'refused',
27
+ download: 'refused',
28
+ bootstrap: 'refused',
29
+ import: 'refused',
30
+ resolve: 'refused',
31
+ build: 'refused',
32
+ run: 'refused',
33
+ status: 'allowed',
34
+ reset: 'refused',
35
+ discard: 'refused',
36
+ export: 'conditional',
37
+ 'export-all': 'conditional',
38
+ 're-export': 'conditional',
39
+ patch: 'refused',
40
+ rebase: 'refused',
41
+ package: 'refused',
42
+ watch: 'refused',
43
+ // Build-less runs only, and only in trees whose marker records a cloned
44
+ // (mozinfo-rewritten) objdir — see the conditional check below.
45
+ test: 'conditional',
46
+ config: 'conditional',
47
+ doctor: 'conditional',
48
+ register: 'refused',
49
+ wire: 'refused',
50
+ token: 'refused',
51
+ lint: 'allowed',
52
+ typecheck: 'allowed',
53
+ verify: 'allowed',
54
+ furnace: 'refused',
55
+ // No nesting; `tree list` inside a tree is pointless but harmless — the
56
+ // subcommand check below allows only `list`.
57
+ tree: 'conditional',
58
+ };
59
+ /** The command set named in refusal messages. */
60
+ const ALLOWED_SUMMARY = 'Verification trees support: status, lint, typecheck, verify, doctor (read-only), ' +
61
+ 'config (read), export --dry-run, export-all --dry-run, re-export --dry-run, and — ' +
62
+ 'in trees created with --with-objdir — build-less test.';
63
+ const TREE_CONDITIONAL_CHECKS = {
64
+ // Exports write patches/ + patches.json; only the dry-run preview is a read.
65
+ export: ({ options }) => options['dryRun'] === true,
66
+ 'export-all': ({ options }) => options['dryRun'] === true,
67
+ // Same rule for refreshing existing patches: the dry-run projection is a
68
+ // read (proven side-effect free and runtime-enforced by
69
+ // withDryRunPurityGuard — FORGE H1/H3); a real re-export writes patches/.
70
+ 're-export': ({ options }) => options['dryRun'] === true,
71
+ // `config` with no positional value prints; with a value it writes.
72
+ config: ({ args }) => args.filter((a) => typeof a === 'string').length <= 1,
73
+ // Doctor reads unless a repair flag is set.
74
+ doctor: ({ options }) => options['repairFurnace'] !== true && options['fix'] !== true,
75
+ // Build-less test needs the objdir the marker records as cloned,
76
+ // mozinfo-rewritten AND reconfigured in-tree; `test --build` rebuilds
77
+ // the engine and stays primary-only (a tree is a snapshot with no
78
+ // merge-back model, so build outputs mutated in-tree go nowhere).
79
+ test: ({ options }, marker) => marker.clonedObjdir !== undefined && options['build'] !== true,
80
+ // No tree lifecycle inside a tree; `tree list` is the only read.
81
+ tree: ({ subcommands }) => subcommands[0] === 'list',
82
+ };
83
+ /**
84
+ * Command-specific refusal hints appended after the generic sentence —
85
+ * for verdicts where "mutates project or engine state" alone would
86
+ * misdiagnose the actual reason.
87
+ */
88
+ const TREE_REFUSAL_HINTS = {
89
+ test: ({ options }, marker) => {
90
+ if (options['build'] === true) {
91
+ return '"test --build" rebuilds the engine and must run in the primary tree; run build-less "fireforge test" here instead.';
92
+ }
93
+ if (marker.clonedObjdir === undefined) {
94
+ return 'In-tree test requires a tree created with "fireforge tree create <name> --with-objdir".';
95
+ }
96
+ return undefined;
97
+ },
98
+ };
99
+ /**
100
+ * Throws when `commandName` (with its subcommand chain and options) must
101
+ * not run inside the tree described by `marker`. Exported for the
102
+ * preAction hook in cli.ts and for the guard tests.
103
+ */
104
+ export function enforceTreeGuard(marker, commandName, input) {
105
+ const verdict = TREE_COMMAND_VERDICTS[commandName] ?? 'refused';
106
+ if (verdict === 'allowed')
107
+ return;
108
+ const conditional = TREE_CONDITIONAL_CHECKS[commandName];
109
+ if (verdict === 'conditional' && conditional && conditional(input, marker))
110
+ return;
111
+ const hint = TREE_REFUSAL_HINTS[commandName]?.(input, marker);
112
+ throw new GeneralError(`This is a FireForge verification tree ("${marker.name}", created from ${marker.primaryRoot}). ` +
113
+ `"${[commandName, ...input.subcommands].join(' ')}" mutates project or engine state and must run in the primary tree. ` +
114
+ (hint === undefined ? '' : `${hint} `) +
115
+ ALLOWED_SUMMARY);
116
+ }
117
+ /**
118
+ * Commander preAction hook body: locates the enclosing tree (if any) and
119
+ * enforces the verdict table for the invoked command chain.
120
+ */
121
+ export async function runTreeGuardHook(rootCommandName, actionCommand) {
122
+ // Resolve the top-level command and subcommand chain from the action
123
+ // command's parent links (commander nests subcommands).
124
+ const chain = [];
125
+ let node = actionCommand;
126
+ while (node && node.parent) {
127
+ chain.unshift(node.name());
128
+ node = node.parent;
129
+ }
130
+ const [commandName, ...subcommands] = chain;
131
+ if (commandName === undefined || commandName === rootCommandName)
132
+ return;
133
+ const options = actionCommand.optsWithGlobals();
134
+ const located = await (async () => {
135
+ let current = resolve(process.cwd());
136
+ for (let depth = 0; depth < 50; depth++) {
137
+ if (existsSync(join(current, 'fireforge.json'))) {
138
+ return { root: current, state: await readTreeMarkerState(current) };
139
+ }
140
+ const parent = dirname(current);
141
+ if (parent === current)
142
+ break;
143
+ current = parent;
144
+ }
145
+ return undefined;
146
+ })();
147
+ if (located === undefined)
148
+ return;
149
+ const state = located.state;
150
+ if (state.kind === 'absent')
151
+ return;
152
+ // A marker we cannot read is not evidence that this is not a tree — it is
153
+ // evidence that we cannot tell. Treating it as "not a tree" would grant the
154
+ // full mutating command set to a snapshot on the strength of a file that
155
+ // failed to parse, which is exactly backwards for a default-deny guard.
156
+ if (state.kind === 'corrupt') {
157
+ if (options['ignoreCorruptTreeMarker'] === true)
158
+ return;
159
+ // Unconditionally-allowed commands never consult marker fields — their
160
+ // verdict is the same in every tree — so an unreadable marker cannot
161
+ // change their answer. Letting them through keeps read-only diagnostics
162
+ // (status, lint) usable on the very tree the operator needs to inspect.
163
+ // 'conditional' verdicts stay refused: their predicates can write.
164
+ if (TREE_COMMAND_VERDICTS[commandName] === 'allowed')
165
+ return;
166
+ const markerPath = getTreeMarkerPath(located.root);
167
+ throw new GeneralError(`${markerPath} identifies this directory as a FireForge verification tree, but ${state.reason}. ` +
168
+ `Refusing "${[commandName, ...subcommands].join(' ')}" because it cannot be established ` +
169
+ 'whether this is a snapshot or the primary tree.\n\n' +
170
+ 'Recreate the tree (fireforge tree remove <name> && fireforge tree create <name>), or — if ' +
171
+ 'this really is the primary tree — delete the stray marker. ' +
172
+ 'Pass --ignore-corrupt-tree-marker to proceed anyway.');
173
+ }
174
+ enforceTreeGuard(state.marker, commandName, {
175
+ subcommands,
176
+ options,
177
+ args: actionCommand.args,
178
+ });
179
+ }
@@ -0,0 +1,129 @@
1
+ import { type CowCapability } from './tree-cow.js';
2
+ /** `.fireforge/tree.json` — identifies a directory as a verification tree. */
3
+ export interface TreeMarker {
4
+ schemaVersion: 1;
5
+ name: string;
6
+ primaryRoot: string;
7
+ createdAt: string;
8
+ engineHead: string | null;
9
+ /** Hash of the primary engine's tracked + untracked working-tree diff. */
10
+ engineFingerprint?: string | null;
11
+ patchesFingerprint: string | null;
12
+ /**
13
+ * Name of the obj-* directory cloned into this tree, written only after
14
+ * its mozinfo.json was successfully rewritten to the tree's paths AND the
15
+ * caller's in-tree reconfigure regenerated the remaining configure output.
16
+ * Its presence is what lets the guard admit build-less `test` in-tree
17
+ * (`tree-guard.ts`); absent on trees created without `--with-objdir`.
18
+ */
19
+ clonedObjdir?: string;
20
+ }
21
+ /** One row of `tree list`. */
22
+ export interface TreeListEntry {
23
+ name: string;
24
+ path: string;
25
+ createdAt: string;
26
+ staleness: 'fresh' | 'stale (engine advanced)' | 'stale (engine worktree changed)' | 'stale (patches changed)' | 'unknown';
27
+ }
28
+ /** Absolute trees directory for a primary root. */
29
+ export declare function getTreesDir(primaryRoot: string): string;
30
+ /** Serialises tree create/remove against each other (primary-side sidecar lock). */
31
+ export declare function withTreeLifecycleLock<T>(primaryRoot: string, operation: () => Promise<T>): Promise<T>;
32
+ /**
33
+ * Outcome of reading a tree marker.
34
+ *
35
+ * The three states must stay distinct because they carry opposite safety
36
+ * meanings. `absent` means "this directory is not a tree", which is what lets
37
+ * every mutating command run. `corrupt` means "this directory claims to be a
38
+ * tree but we cannot read the claim" — collapsing it into `absent` (as a bare
39
+ * `undefined` return does) hands a snapshot the full mutating command set on
40
+ * the strength of a file we failed to parse, inverting `tree-guard.ts`'s
41
+ * default-deny design. Deleting one field from a marker was enough to run
42
+ * `reset` inside a clone.
43
+ */
44
+ export type TreeMarkerRead = {
45
+ kind: 'absent';
46
+ } | {
47
+ kind: 'valid';
48
+ marker: TreeMarker;
49
+ } | {
50
+ kind: 'corrupt';
51
+ reason: string;
52
+ };
53
+ /** Absolute path of a root's tree marker. */
54
+ export declare function getTreeMarkerPath(root: string): string;
55
+ /**
56
+ * Reads the tree marker for a project root, distinguishing "not a tree" from
57
+ * "a tree whose marker we could not read". Callers that make a safety decision
58
+ * (the guard hook, the no-nesting refusal) must use this rather than
59
+ * {@link readTreeMarker}.
60
+ *
61
+ * Only ENOENT/ENOTDIR mean `absent`: any other probe failure (EACCES on the
62
+ * `.fireforge` directory, EIO, …) reports `corrupt`, because "we cannot read
63
+ * the marker" is not evidence that there is no marker — a `pathExists`
64
+ * pre-check here classified a tree with an unreadable `.fireforge` as
65
+ * `absent` and handed it the full mutating command set.
66
+ */
67
+ export declare function readTreeMarkerState(root: string): Promise<TreeMarkerRead>;
68
+ /**
69
+ * Reads the tree marker for a project root, or undefined when it is absent
70
+ * OR unreadable.
71
+ *
72
+ * For display and best-effort paths only (`tree list` renders an unreadable
73
+ * marker as staleness 'unknown'). Anything that decides whether a mutation may
74
+ * proceed must call {@link readTreeMarkerState} and handle `corrupt`.
75
+ */
76
+ export declare function readTreeMarker(root: string): Promise<TreeMarker | undefined>;
77
+ /**
78
+ * Refuses a build-less in-tree `test` when the objdir preflight found is not
79
+ * the one this tree's marker vouched for as rewritten-and-reconfigured. A
80
+ * mismatch means an objdir appeared in the tree through some path other than
81
+ * `tree create --with-objdir` (manual copy, partial restore), so nothing
82
+ * proves it was relocated — running against it could consult primary paths.
83
+ * No-op outside a tree; a corrupt marker is already refused by the guard.
84
+ */
85
+ export declare function assertObjdirMatchesTreeMarker(projectRoot: string, objDir: string | undefined): Promise<void>;
86
+ /** Current primary-side snapshot identity, including dirty source and patch bodies. */
87
+ export declare function computePrimaryFingerprint(primaryRoot: string): Promise<{
88
+ engineHead: string | null;
89
+ engineFingerprint?: string | null;
90
+ patchesFingerprint: string | null;
91
+ }>;
92
+ /** Validates a tree name: one path segment, no traversal. */
93
+ export declare function assertValidTreeName(name: string): void;
94
+ /**
95
+ * Materialises the tree at `treeRoot` from `primaryRoot` using
96
+ * `capability` ('none' = plain copy, already gated on --force-copy by
97
+ * the caller). The engine is cloned one TOP-LEVEL entry at a time so unwanted
98
+ * `obj-*` directories are never traversed or materialised. That distinction
99
+ * is substantial on Firefox trees: even CoW-cloning and deleting an objdir
100
+ * touches metadata for millions of build outputs. With `withObjdir` exactly
101
+ * that objdir is included — after {@link assertCloneSafeObjdir}
102
+ * refuses symlinked or engine-escaping objdirs before any copying —
103
+ * rewritten to the tree (see below), and
104
+ * reconfigured in-tree via the caller-supplied `reconfigure` hook. A
105
+ * fresh `.fireforge/` is created with only `state.json` (when present),
106
+ * the last-build baseline (withObjdir only), and the tree marker.
107
+ */
108
+ export declare function cloneTree(args: {
109
+ primaryRoot: string;
110
+ treeRoot: string;
111
+ name: string;
112
+ capability: CowCapability;
113
+ createdAt: string;
114
+ withObjdir?: {
115
+ objDir: string;
116
+ reconfigure: (treeEngineDir: string) => Promise<void>;
117
+ };
118
+ }): Promise<TreeMarker>;
119
+ /** Lists trees with staleness against the current primary state. */
120
+ export declare function listTrees(primaryRoot: string): Promise<TreeListEntry[]>;
121
+ /**
122
+ * Removes one tree. Refuses when a live process holds the tree's build
123
+ * or engine-session lock — or when a lock directory exists whose ownership
124
+ * cannot be established, which `--force` overrides — and never deletes a path
125
+ * outside the trees directory (containment check before `rm -rf`).
126
+ */
127
+ export declare function removeTree(primaryRoot: string, name: string, options?: {
128
+ force?: boolean;
129
+ }): Promise<void>;