@hominis/fireforge 0.44.4 → 0.46.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 (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -0,0 +1,259 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Owner record of a `withFileLock` directory: the file that says WHICH
4
+ * process holds the lock, so stale recovery can tell a dead holder from a
5
+ * slow one and release can refuse to remove a lock it no longer owns.
6
+ *
7
+ * Split out of `file-lock.ts` so the record's format, its write-and-verify
8
+ * contract, and the PID-reuse guard live in one place with their own tests.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { readFile, rm, writeFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { GeneralError } from '../errors/base.js';
14
+ import { isProcessAlive, toError } from '../utils/errors.js';
15
+ import { verbose } from '../utils/logger.js';
16
+ import { sleep } from '../utils/sleep.js';
17
+ /**
18
+ * Filename of a lock directory's owner record, relative to the lock dir.
19
+ *
20
+ * Re-exported by `file-lock.ts` so external readers (`tree-store.ts`) share
21
+ * the constant instead of re-spelling `'pid'`; the file *format* is
22
+ * documented on {@link readLockOwner}.
23
+ */
24
+ export const LOCK_PID_FILE = 'pid';
25
+ /**
26
+ * Prefix of the mechanical owner-record line that carries the acquisition
27
+ * wall time. It lives among the metadata lines (line 3 onward) so older
28
+ * readers, which treat everything past line 2 as free-form diagnostics,
29
+ * still parse the record; the current reader lifts it out of `metadata`.
30
+ * Diagnostic only — see {@link START_TICK_PREFIX} for the liveness field.
31
+ */
32
+ const ACQUIRED_AT_PREFIX = 'acquired-at-ms=';
33
+ /**
34
+ * Prefix of the owner-record line carrying the holder's own process start
35
+ * tick (`/proc/self/stat` field 22, in USER_HZ since boot). Written only
36
+ * where procfs exposes it (Linux). Boot-relative ticks are what the
37
+ * PID-reuse guard compares: unlike a wall-clock time they are immune to
38
+ * the clock being stepped by NTP/chrony/`date -s` between acquisition and
39
+ * probe, and a process's start tick never changes for its lifetime, so the
40
+ * comparison is exact rather than a tolerance window.
41
+ */
42
+ const START_TICK_PREFIX = 'start-tick=';
43
+ /**
44
+ * How many times the post-write read-back is retried when the READ itself
45
+ * errors (not when the record is absent or differs). An on-access scanner
46
+ * on Windows briefly holding the just-written file (`EBUSY`), or a FUSE
47
+ * mount's momentary `EIO`, must not be indistinguishable from "the record
48
+ * never landed" — that verdict releases a lock this process has correctly
49
+ * acquired and aborts the run before it starts.
50
+ */
51
+ const READ_BACK_ATTEMPTS = 5;
52
+ /** Pause between read-back attempts. */
53
+ const READ_BACK_RETRY_MS = 20;
54
+ /**
55
+ * Parses the on-disk record text. Returns `undefined` when the PID line
56
+ * does not parse as a finite integer.
57
+ */
58
+ function parseLockOwnerRecord(pidContent) {
59
+ const [pidLine, tokenLine, ...trailingLines] = pidContent.split('\n');
60
+ const pid = parseInt((pidLine ?? '').trim(), 10);
61
+ if (!Number.isFinite(pid))
62
+ return undefined;
63
+ const metadata = [];
64
+ let acquiredAtMs;
65
+ let startTick;
66
+ for (const rawLine of trailingLines) {
67
+ const line = rawLine.trim();
68
+ if (line.length === 0)
69
+ continue;
70
+ if (line.startsWith(ACQUIRED_AT_PREFIX)) {
71
+ const parsed = Number.parseInt(line.slice(ACQUIRED_AT_PREFIX.length), 10);
72
+ if (Number.isFinite(parsed))
73
+ acquiredAtMs ??= parsed;
74
+ continue;
75
+ }
76
+ if (line.startsWith(START_TICK_PREFIX)) {
77
+ const parsed = Number.parseInt(line.slice(START_TICK_PREFIX.length), 10);
78
+ if (Number.isFinite(parsed))
79
+ startTick ??= parsed;
80
+ continue;
81
+ }
82
+ metadata.push(line);
83
+ }
84
+ const token = tokenLine?.trim();
85
+ const owner = { present: true, pid, metadata };
86
+ if (token !== undefined && token.length > 0)
87
+ owner.token = token;
88
+ if (acquiredAtMs !== undefined)
89
+ owner.acquiredAtMs = acquiredAtMs;
90
+ if (startTick !== undefined)
91
+ owner.startTick = startTick;
92
+ return owner;
93
+ }
94
+ /**
95
+ * Reads the owner PID (line 1), acquisition token (line 2), the mechanical
96
+ * `acquired-at-ms=` / `start-tick=` lines (when present), and diagnostic
97
+ * metadata (the remaining lines 3+) from a lock directory's PID file.
98
+ * Returns `{ present: false }` when the PID file is missing, unreadable, or
99
+ * the PID does not parse as a finite integer (caller falls back to the
100
+ * age-only staleness heuristic).
101
+ *
102
+ * File format note: external readers do `parseInt(content.trim(), 10)`,
103
+ * which parses the leading digits of a multi-line file — so the token and
104
+ * mechanical lines stay compatible.
105
+ *
106
+ * @param lockPath - Lock directory whose owner record to read
107
+ * @returns The parsed owner record, or `{ present: false }`
108
+ */
109
+ export async function readLockOwner(lockPath) {
110
+ try {
111
+ const owner = parseLockOwnerRecord(await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8'));
112
+ if (owner !== undefined)
113
+ return owner;
114
+ }
115
+ catch {
116
+ // PID file missing or unreadable — treat as absent.
117
+ }
118
+ return { present: false };
119
+ }
120
+ /**
121
+ * Reads the just-written record back, retrying a READ ERROR a few times
122
+ * before giving up. A missing or malformed record is returned as-is (the
123
+ * caller treats it as a failed write); only errno-level failures retry.
124
+ */
125
+ async function readBackLockOwner(lockPath) {
126
+ let lastError;
127
+ for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
128
+ try {
129
+ return parseLockOwnerRecord(await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8'));
130
+ }
131
+ catch (error) {
132
+ lastError = error;
133
+ if (attempt < READ_BACK_ATTEMPTS - 1)
134
+ await sleep(READ_BACK_RETRY_MS);
135
+ }
136
+ }
137
+ throw new Error(`owner record could not be read back: ${toError(lastError).message}`);
138
+ }
139
+ /**
140
+ * Stamps this process's ownership (PID, per-acquisition token, acquisition
141
+ * time, own start tick, diagnostic metadata) into a lock directory it has
142
+ * just created, and reads the record back to prove it landed.
143
+ *
144
+ * The write is FATAL. It used to be best-effort, on the theory that stale
145
+ * recovery would fall back to the age heuristic — but that heuristic reaps
146
+ * any owner-less lock after five minutes, and the build lock legitimately
147
+ * holds for hours. A holder whose record failed to write was therefore
148
+ * reaped mid-build and a second process entered the critical section. On
149
+ * failure the just-created lock directory is removed (nothing else can own
150
+ * it yet) and a {@link GeneralError} names the cause.
151
+ *
152
+ * @param lockPath - Lock directory this process has just `mkdir`'d
153
+ * @param token - Per-acquisition UUID, later checked by release
154
+ * @param ownerMetadata - Extra human-diagnostic lines (see `FileLockOptions.ownerMetadata`)
155
+ * @throws {GeneralError} When the record cannot be written or does not read back as written
156
+ */
157
+ export async function writeLockOwner(lockPath, token, ownerMetadata) {
158
+ const metadata = ownerMetadata
159
+ ?.map((line) => line.replace(/\r?\n/g, ' ').trim())
160
+ .filter((line) => line.length > 0 &&
161
+ !line.startsWith(ACQUIRED_AT_PREFIX) &&
162
+ !line.startsWith(START_TICK_PREFIX)) ?? [];
163
+ const ownStartTick = readProcessStartTick(process.pid);
164
+ const mechanicalLines = [
165
+ `${ACQUIRED_AT_PREFIX}${Date.now()}`,
166
+ ...(ownStartTick === undefined ? [] : [`${START_TICK_PREFIX}${ownStartTick}`]),
167
+ ];
168
+ const record = `${process.pid}\n${token}\n${[...mechanicalLines, ...metadata].map((line) => `${line}\n`).join('')}`;
169
+ let failure;
170
+ try {
171
+ await writeFile(join(lockPath, LOCK_PID_FILE), record, 'utf-8');
172
+ const readBack = await readBackLockOwner(lockPath);
173
+ if (readBack !== undefined && readBack.pid === process.pid && readBack.token === token) {
174
+ return;
175
+ }
176
+ failure = new Error('owner record did not read back as written');
177
+ }
178
+ catch (error) {
179
+ failure = error;
180
+ }
181
+ try {
182
+ await rm(lockPath, { recursive: true, force: true });
183
+ }
184
+ catch (cleanupError) {
185
+ verbose(`Could not remove unowned lock ${lockPath}: ${toError(cleanupError).message}`);
186
+ }
187
+ throw new GeneralError(`Could not record ownership of lock ${lockPath}: ${toError(failure).message}. ` +
188
+ 'The lock was released without running the operation — a lock with no readable owner ' +
189
+ 'would be reaped as stale after five minutes and a second process could enter the ' +
190
+ 'critical section. Check permissions and free space on the lock directory, then retry.', failure);
191
+ }
192
+ /**
193
+ * Boot-relative start tick of a process, where the platform exposes it
194
+ * cheaply.
195
+ *
196
+ * Linux only: field 22 of `/proc/<pid>/stat`, in USER_HZ since boot.
197
+ * Elsewhere (macOS, Windows) obtaining a start time means spawning
198
+ * `ps`/`wmic` per probe, which is not worth paying on every poll of a
199
+ * contended lock — those platforms return `undefined` and liveness stays
200
+ * PID-only.
201
+ *
202
+ * Deliberately NOT converted to wall-clock time via `/proc/stat` `btime`:
203
+ * on current kernels `btime` is derived from the realtime clock and moves
204
+ * with every clock step, so a wall-clock comparison against a recorded
205
+ * acquisition time misjudges a live holder as a reused PID after NTP steps
206
+ * the clock forward — and reaps a lock that is in use.
207
+ *
208
+ * @param pid - Process to look up
209
+ * @param procRoot - procfs mount, overridable for tests
210
+ * @returns Start tick, or `undefined` when unobtainable
211
+ */
212
+ export function readProcessStartTick(pid, procRoot = '/proc') {
213
+ try {
214
+ const stat = readFileSync(join(procRoot, String(pid), 'stat'), 'utf-8');
215
+ // `comm` (field 2) is parenthesised and may itself contain spaces or
216
+ // parentheses, so split only what follows the LAST closing paren:
217
+ // field 3 (state) is then index 0 and field 22 (starttime) index 19.
218
+ const startTick = Number.parseInt(stat
219
+ .slice(stat.lastIndexOf(')') + 1)
220
+ .trim()
221
+ .split(/\s+/)[19] ?? '', 10);
222
+ return Number.isFinite(startTick) ? startTick : undefined;
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ }
228
+ /**
229
+ * Whether the process named by a lock's owner record is the SAME process
230
+ * that acquired the lock, not merely a live process wearing its PID.
231
+ *
232
+ * `isProcessAlive` answers "does this PID exist"; after a holder crashes,
233
+ * the kernel may hand its PID to a long-lived daemon, and a PID-only check
234
+ * then honours the dead lock until the waiter's timeout. Where the record
235
+ * carries the writer's start tick and the candidate's own tick is cheaply
236
+ * obtainable (see {@link readProcessStartTick}), the two must be equal: a
237
+ * different tick is a different process. Records without a start tick, and
238
+ * platforms without one, keep the PID-only answer.
239
+ *
240
+ * @param owner - Owner record fields that identify the holder
241
+ * @param procRoot - procfs mount, overridable for tests
242
+ * @returns True when the original holder is still running (or cannot be proven gone)
243
+ */
244
+ export function isLockOwnerAlive(owner, procRoot) {
245
+ if (!isProcessAlive(owner.pid))
246
+ return false;
247
+ if (owner.startTick === undefined)
248
+ return true;
249
+ const currentStartTick = readProcessStartTick(owner.pid, procRoot);
250
+ if (currentStartTick === undefined)
251
+ return true;
252
+ if (currentStartTick !== owner.startTick) {
253
+ verbose(`Lock owner PID ${owner.pid} started at tick ${currentStartTick}, but the ` +
254
+ `record was written by a process started at tick ${owner.startTick} — the PID was ` +
255
+ 'reused; treating the holder as dead');
256
+ return false;
257
+ }
258
+ return true;
259
+ }
@@ -1,3 +1,5 @@
1
+ import { LOCK_PID_FILE } from './file-lock-owner.js';
2
+ export { LOCK_PID_FILE };
1
3
  /**
2
4
  * Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
3
5
  * `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
@@ -95,13 +97,6 @@ export interface FileLockOptions {
95
97
  }
96
98
  /** Derives the sibling lock-directory path used to guard a file-based resource. */
97
99
  export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
98
- /**
99
- * Filename of a lock directory's owner record, relative to the lock dir.
100
- *
101
- * Exported so external readers (`tree-store.ts`) share the constant instead of
102
- * re-spelling `'pid'`; the file *format* is documented on {@link readLockOwner}.
103
- */
104
- export declare const LOCK_PID_FILE = "pid";
105
100
  /**
106
101
  * Reads the live waiter queue around `lockPath`, dropping entries whose
107
102
  * process is gone (a killed waiter must not inflate the depth forever).
@@ -1,11 +1,14 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { randomUUID } from 'node:crypto';
3
- import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { LockContentionError } from '../errors/base.js';
6
6
  import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
7
7
  import { ensureDir } from '../utils/fs.js';
8
8
  import { verbose, warn } from '../utils/logger.js';
9
+ import { sleep } from '../utils/sleep.js';
10
+ import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
11
+ export { LOCK_PID_FILE };
9
12
  const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
10
13
  const DEFAULT_LOCK_POLL_MS = 50;
11
14
  const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
@@ -23,52 +26,10 @@ const STALE_REPROBE_INTERVAL_MS = 5_000;
23
26
  * deadline extension but no progress line of its own.
24
27
  */
25
28
  const WAIT_PROBE_INTERVAL_MS = 5_000;
26
- function sleep(ms) {
27
- return new Promise((resolve) => {
28
- setTimeout(resolve, ms);
29
- });
30
- }
31
29
  /** Derives the sibling lock-directory path used to guard a file-based resource. */
32
30
  export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
33
31
  return `${filePath}${suffix}`;
34
32
  }
35
- /**
36
- * Filename of a lock directory's owner record, relative to the lock dir.
37
- *
38
- * Exported so external readers (`tree-store.ts`) share the constant instead of
39
- * re-spelling `'pid'`; the file *format* is documented on {@link readLockOwner}.
40
- */
41
- export const LOCK_PID_FILE = 'pid';
42
- /**
43
- * Reads the owner PID (line 1), acquisition token (line 2), and diagnostic
44
- * metadata (lines 3+, see {@link FileLockOptions.ownerMetadata}) from a lock
45
- * directory's PID file. Returns `{ present: false }` when the PID file is
46
- * missing or the PID does not parse as a finite integer (caller falls back
47
- * to the age-only staleness heuristic).
48
- *
49
- * File format note: external readers do `parseInt(content.trim(), 10)`,
50
- * which parses the leading digits of a multi-line file — so the token line
51
- * stays compatible.
52
- */
53
- async function readLockOwner(lockPath) {
54
- try {
55
- const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
56
- const [pidLine, tokenLine, ...metadataLines] = pidContent.split('\n');
57
- const pid = parseInt((pidLine ?? '').trim(), 10);
58
- if (Number.isFinite(pid)) {
59
- const metadata = metadataLines.map((line) => line.trim()).filter((line) => line.length > 0);
60
- const token = tokenLine?.trim();
61
- if (token !== undefined && token.length > 0) {
62
- return { present: true, pid, token, metadata };
63
- }
64
- return { present: true, pid, metadata };
65
- }
66
- }
67
- catch {
68
- // PID file missing or unreadable — treat as absent.
69
- }
70
- return { present: false };
71
- }
72
33
  /** Sibling directory in which waiters advertise themselves. */
73
34
  function lockWaitersDir(lockPath) {
74
35
  return `${lockPath}.waiters`;
@@ -127,7 +88,7 @@ export async function readLockStatus(lockPath) {
127
88
  }
128
89
  const owner = await readLockOwner(lockPath);
129
90
  const holder = owner.present
130
- ? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
91
+ ? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
131
92
  : undefined;
132
93
  return { held: true, holder, heldForMs, queueDepth: queue.depth };
133
94
  }
@@ -138,10 +99,10 @@ export async function readLockStatus(lockPath) {
138
99
  */
139
100
  async function registerWaiter(lockPath, startedAt) {
140
101
  const dir = lockWaitersDir(lockPath);
141
- const file = join(dir, `${String(startedAt)}-${String(process.pid)}-${randomUUID()}`);
102
+ const file = join(dir, `${startedAt}-${process.pid}-${randomUUID()}`);
142
103
  try {
143
104
  await ensureDir(dir);
144
- await writeFile(file, `${String(process.pid)}\n`, 'utf-8');
105
+ await writeFile(file, `${process.pid}\n`, 'utf-8');
145
106
  }
146
107
  catch (error) {
147
108
  // Advisory only — never fail a lock acquisition over the queue display.
@@ -164,7 +125,7 @@ async function registerWaiter(lockPath, startedAt) {
164
125
  async function probeWaitState(lockPath, startedAt) {
165
126
  const owner = await readLockOwner(lockPath);
166
127
  const holder = owner.present
167
- ? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
128
+ ? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
168
129
  : undefined;
169
130
  return { holder, queue: await readLockQueue(lockPath, startedAt) };
170
131
  }
@@ -189,7 +150,7 @@ async function probeWaitState(lockPath, startedAt) {
189
150
  * legitimately held and the caller should keep waiting.
190
151
  */
191
152
  async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
192
- const reapPath = `${lockPath}.reaping-${String(process.pid)}-${randomUUID()}`;
153
+ const reapPath = `${lockPath}.reaping-${process.pid}-${randomUUID()}`;
193
154
  try {
194
155
  await rename(lockPath, reapPath);
195
156
  }
@@ -205,16 +166,16 @@ async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
205
166
  // We exclusively own reapPath now. Re-verify: the owner may have changed
206
167
  // between the staleness probe and the rename (release + fresh acquire).
207
168
  const owner = await readLockOwner(reapPath);
208
- if (owner.present && isProcessAlive(owner.pid)) {
169
+ if (owner.present && isLockOwnerAlive(owner)) {
209
170
  try {
210
171
  await rename(reapPath, lockPath);
211
- verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${String(owner.pid)} is alive (lock was re-acquired mid-probe)`);
172
+ verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${owner.pid} is alive (lock was re-acquired mid-probe)`);
212
173
  }
213
174
  catch (restoreError) {
214
175
  // Rename-back can only fail if a third process mkdir'd the lock path
215
176
  // in this instant. The displaced live owner keeps running without its
216
177
  // lock — surface it loudly instead of hiding the lost exclusion.
217
- warn(`Could not restore live lock for PID ${String(owner.pid)} at ${lockPath} ` +
178
+ warn(`Could not restore live lock for PID ${owner.pid} at ${lockPath} ` +
218
179
  `(${toError(restoreError).message}). Displaced lock kept at ${reapPath} for inspection.`);
219
180
  }
220
181
  return false;
@@ -246,13 +207,14 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
246
207
  // PID-first check an explicitly-dead owner unblocks immediately.
247
208
  const owner = await readLockOwner(lockPath);
248
209
  if (owner.present) {
249
- if (!isProcessAlive(owner.pid)) {
210
+ if (!isLockOwnerAlive(owner)) {
250
211
  return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
251
212
  }
252
- // PID is alive — respect it regardless of age. A slow `mach build`
253
- // legitimately holds the lock past the stale threshold and we don't
254
- // want to race-remove it.
255
- verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${String(owner.pid)} is still running — not removing`);
213
+ // The owner is alive (PID exists AND, where `/proc` can tell, it is
214
+ // the process that acquired the lock, not a recycled PID) — respect
215
+ // it regardless of age. A slow `mach build` legitimately holds the
216
+ // lock past the stale threshold and we don't want to race-remove it.
217
+ verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${owner.pid} is still running — not removing`);
256
218
  return false;
257
219
  }
258
220
  // No readable PID file. Fall back to the age-only heuristic so locks
@@ -281,19 +243,16 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
281
243
  * double-acquisition. When the PID file no longer names us, warn and leave
282
244
  * the directory alone.
283
245
  *
284
- * `ownerFileWritten` is false when our own owner-file write failed at
285
- * acquisition (non-fatal there); ownership then cannot be verified and the
286
- * removal is unconditional, which is strictly better than leaking the lock.
246
+ * The owner record is always present here: `writeLockOwner` is fatal, so an
247
+ * acquisition whose record failed to write never reached the operation.
287
248
  */
288
- async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
289
- if (ownerFileWritten) {
290
- const owner = await readLockOwner(lockPath);
291
- if (owner.present &&
292
- (owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
293
- warn(`Not removing lock ${lockPath}: it is now owned by PID ${String(owner.pid)} — ` +
294
- 'this process no longer holds it.');
295
- return;
296
- }
249
+ async function releaseLock(lockPath, ownerToken) {
250
+ const owner = await readLockOwner(lockPath);
251
+ if (owner.present &&
252
+ (owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
253
+ warn(`Not removing lock ${lockPath}: it is now owned by PID ${owner.pid} — ` +
254
+ 'this process no longer holds it.');
255
+ return;
297
256
  }
298
257
  await rm(lockPath, { recursive: true, force: true });
299
258
  }
@@ -339,19 +298,17 @@ export async function releaseLockIfOwned(lockPath) {
339
298
  */
340
299
  async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
341
300
  const owner = await readLockOwner(lockPath);
342
- const liveHolder = owner.present && isProcessAlive(owner.pid) ? owner : undefined;
301
+ const liveHolder = owner.present && isLockOwnerAlive(owner) ? owner : undefined;
343
302
  const details = liveHolder !== undefined && liveHolder.metadata.length > 0
344
303
  ? ` (${liveHolder.metadata.join(', ')})`
345
304
  : '';
346
305
  const position = formatQueuePositionReached(queue);
347
306
  if (onTimeoutMessage !== undefined) {
348
- const identifiedHolder = liveHolder !== undefined
349
- ? ` The lock is held by PID ${String(liveHolder.pid)}${details}.`
350
- : '';
307
+ const identifiedHolder = liveHolder !== undefined ? ` The lock is held by PID ${liveHolder.pid}${details}.` : '';
351
308
  return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}${position}`, cause);
352
309
  }
353
310
  const holderHint = liveHolder !== undefined
354
- ? ` It is held by running process ${String(liveHolder.pid)}${details} — wait for it to finish, or stop it and retry.`
311
+ ? ` It is held by running process ${liveHolder.pid}${details} — wait for it to finish, or stop it and retry.`
355
312
  : ' If no other FireForge process is running, remove the lock directory and retry.';
356
313
  return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}${position}`, cause);
357
314
  }
@@ -369,9 +326,9 @@ function formatQueuePositionReached(queue) {
369
326
  if (queue === undefined || queue.depth === 0)
370
327
  return '';
371
328
  if (queue.ahead === 0) {
372
- return ` You were next in a queue of ${String(queue.depth)} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
329
+ return ` You were next in a queue of ${queue.depth} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
373
330
  }
374
- return ` You were still ${String(queue.ahead)} from the head of a queue of ${String(queue.depth)} when the wait expired; re-run with a larger --wait-lock.`;
331
+ return ` You were still ${queue.ahead} from the head of a queue of ${queue.depth} when the wait expired; re-run with a larger --wait-lock.`;
375
332
  }
376
333
  /**
377
334
  * Runs one wait probe: reads holder and queue, and reports progress when the
@@ -515,28 +472,20 @@ export async function withFileLock(lockPath, operation, options = {}) {
515
472
  // acquired the lock or timed out.
516
473
  await deregisterWaiter?.();
517
474
  }
518
- // Stamp ownership (PID + per-acquisition token) into the lock directory so
519
- // stale-lock recovery can check liveness and release can verify the lock
520
- // is still ours before removing it.
475
+ // Stamp ownership (PID + per-acquisition token + acquisition time) into
476
+ // the lock directory so stale-lock recovery can check liveness and release
477
+ // can verify the lock is still ours before removing it. Fatal on failure:
478
+ // an owner-less lock is reaped by the age heuristic after five minutes,
479
+ // which for a multi-hour build meant a second holder mid-critical-section.
480
+ // `writeLockOwner` removes the just-created directory before throwing.
521
481
  const ownerToken = randomUUID();
522
- let ownerFileWritten = false;
523
- try {
524
- const metadata = options.ownerMetadata
525
- ?.map((line) => line.replace(/\r?\n/g, ' ').trim())
526
- .filter((line) => line.length > 0) ?? [];
527
- await writeFile(join(lockPath, LOCK_PID_FILE), `${String(process.pid)}\n${ownerToken}\n${metadata.length > 0 ? `${metadata.join('\n')}\n` : ''}`, 'utf-8');
528
- ownerFileWritten = true;
529
- }
530
- catch {
531
- // Non-fatal: stale recovery falls back to age-only heuristic and release
532
- // falls back to unconditional removal.
533
- }
482
+ await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
534
483
  heldLocks.add(lockPath);
535
484
  try {
536
485
  return await operation();
537
486
  }
538
487
  finally {
539
488
  heldLocks.delete(lockPath);
540
- await releaseLock(lockPath, ownerToken, ownerFileWritten);
489
+ await releaseLock(lockPath, ownerToken);
541
490
  }
542
491
  }
@@ -3,13 +3,25 @@
3
3
  */
4
4
  import type { ResolvedArchive } from './firefox-archive.js';
5
5
  import type { ProgressCallback } from './firefox-download.js';
6
+ /** Options governing the default (unpinned) integrity check. */
7
+ export interface ArchiveIntegrityOptions {
8
+ /**
9
+ * Accept the download with a warning when the published SHA256SUMS cannot
10
+ * be obtained (`firefox.allowUnverifiedDownload`). Default: fail closed.
11
+ */
12
+ allowUnverifiedDownload?: boolean;
13
+ }
6
14
  /**
7
15
  * Ensures a valid cached archive exists, downloading it if needed.
8
16
  * @param archive - Resolved archive descriptor
9
17
  * @param cacheDir - Cache directory
10
18
  * @param onProgress - Optional progress callback
19
+ * @param expectedSha256 - Operator-pinned digest; when set it replaces the
20
+ * published-SHA256SUMS check
21
+ * @param onCacheProgress - Phase progress messages
22
+ * @param integrity - Default integrity-check options (unpinned downloads)
11
23
  */
12
- export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void): Promise<void>;
24
+ export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void, integrity?: ArchiveIntegrityOptions): Promise<void>;
13
25
  /**
14
26
  * Removes cached tarball, metadata, and partial download files for an archive.
15
27
  * @param archive - Resolved archive descriptor