@hominis/fireforge 0.40.0 → 0.42.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 (256) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -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 +13 -3
  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 +16 -7
  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 +12 -59
  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.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/branding.d.ts +16 -0
  118. package/dist/src/core/branding.js +18 -2
  119. package/dist/src/core/build-audit-platform.js +6 -1
  120. package/dist/src/core/build-audit-registration.js +2 -0
  121. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  122. package/dist/src/core/build-audit-resolve.js +34 -13
  123. package/dist/src/core/build-audit.js +4 -20
  124. package/dist/src/core/build-baseline-types.d.ts +29 -8
  125. package/dist/src/core/build-baseline-types.js +2 -1
  126. package/dist/src/core/build-baseline.d.ts +10 -2
  127. package/dist/src/core/build-baseline.js +39 -8
  128. package/dist/src/core/build-prepare.d.ts +4 -0
  129. package/dist/src/core/build-prepare.js +7 -1
  130. package/dist/src/core/config-paths.d.ts +1 -1
  131. package/dist/src/core/config-paths.js +2 -0
  132. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  133. package/dist/src/core/config-validate.js +34 -36
  134. package/dist/src/core/config.js +16 -3
  135. package/dist/src/core/coverage-extend.d.ts +89 -0
  136. package/dist/src/core/coverage-extend.js +188 -0
  137. package/dist/src/core/destructive.js +3 -1
  138. package/dist/src/core/discard-baseline.js +18 -4
  139. package/dist/src/core/engine-precondition.d.ts +35 -0
  140. package/dist/src/core/engine-precondition.js +50 -0
  141. package/dist/src/core/engine-session-lock.d.ts +38 -1
  142. package/dist/src/core/engine-session-lock.js +78 -4
  143. package/dist/src/core/file-lock.d.ts +7 -0
  144. package/dist/src/core/file-lock.js +20 -30
  145. package/dist/src/core/firefox.js +5 -2
  146. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  147. package/dist/src/core/furnace-apply-helpers.js +12 -3
  148. package/dist/src/core/furnace-apply-output.js +9 -0
  149. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  150. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  151. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  152. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  153. package/dist/src/core/furnace-apply.d.ts +16 -4
  154. package/dist/src/core/furnace-apply.js +39 -43
  155. package/dist/src/core/furnace-config.js +2 -0
  156. package/dist/src/core/furnace-css-fragments.js +2 -0
  157. package/dist/src/core/furnace-jsconfig.js +14 -7
  158. package/dist/src/core/furnace-registration-ast.js +8 -8
  159. package/dist/src/core/furnace-registration-remove.js +6 -3
  160. package/dist/src/core/furnace-registration.js +9 -12
  161. package/dist/src/core/furnace-rollback.js +1 -1
  162. package/dist/src/core/furnace-scanner.js +2 -1
  163. package/dist/src/core/furnace-stale-export.js +2 -1
  164. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  165. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  166. package/dist/src/core/furnace-validate-helpers.js +33 -1
  167. package/dist/src/core/furnace-validate-registration.js +16 -0
  168. package/dist/src/core/furnace-validate.js +4 -2
  169. package/dist/src/core/git-diff.js +37 -2
  170. package/dist/src/core/lint-cache.d.ts +1 -1
  171. package/dist/src/core/lint-cache.js +33 -10
  172. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  173. package/dist/src/core/mach-build-artifacts.js +108 -2
  174. package/dist/src/core/mach-mozconfig.js +9 -1
  175. package/dist/src/core/mach-resource-shim.js +1 -0
  176. package/dist/src/core/marionette-port.d.ts +16 -0
  177. package/dist/src/core/marionette-port.js +93 -8
  178. package/dist/src/core/marionette-preflight.js +6 -5
  179. package/dist/src/core/patch-apply.d.ts +20 -13
  180. package/dist/src/core/patch-apply.js +32 -33
  181. package/dist/src/core/patch-export.js +3 -5
  182. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  183. package/dist/src/core/patch-lint-checkjs.js +78 -7
  184. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  185. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  186. package/dist/src/core/patch-lint-creators.js +45 -0
  187. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  188. package/dist/src/core/patch-lint-cross.js +16 -32
  189. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  190. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  191. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  192. package/dist/src/core/patch-lint-module-registration.js +88 -0
  193. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  194. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  195. package/dist/src/core/patch-lint-reexports.js +1 -0
  196. package/dist/src/core/patch-lint.d.ts +8 -0
  197. package/dist/src/core/patch-lint.js +84 -6
  198. package/dist/src/core/patch-lock.d.ts +12 -1
  199. package/dist/src/core/patch-lock.js +40 -4
  200. package/dist/src/core/patch-manifest-consistency.js +15 -3
  201. package/dist/src/core/patch-parse.d.ts +8 -0
  202. package/dist/src/core/patch-parse.js +10 -0
  203. package/dist/src/core/patch-policy.d.ts +7 -0
  204. package/dist/src/core/patch-policy.js +4 -1
  205. package/dist/src/core/patch-transform.d.ts +10 -0
  206. package/dist/src/core/patch-transform.js +13 -2
  207. package/dist/src/core/rebase-session.d.ts +44 -2
  208. package/dist/src/core/rebase-session.js +114 -17
  209. package/dist/src/core/register-browser-content.js +2 -1
  210. package/dist/src/core/register-shared-css.js +2 -1
  211. package/dist/src/core/smoke-patterns.d.ts +0 -8
  212. package/dist/src/core/smoke-patterns.js +3 -2
  213. package/dist/src/core/status-binary.d.ts +55 -0
  214. package/dist/src/core/status-binary.js +124 -0
  215. package/dist/src/core/status-classify.d.ts +6 -1
  216. package/dist/src/core/status-classify.js +101 -73
  217. package/dist/src/core/test-harness-crash.d.ts +32 -1
  218. package/dist/src/core/test-harness-crash.js +59 -4
  219. package/dist/src/core/test-path-scope.js +2 -0
  220. package/dist/src/core/test-stale-check.js +1 -20
  221. package/dist/src/core/toolchain-preflight.js +24 -26
  222. package/dist/src/core/tree-guard.d.ts +1 -1
  223. package/dist/src/core/tree-guard.js +67 -12
  224. package/dist/src/core/tree-store.d.ts +86 -10
  225. package/dist/src/core/tree-store.js +310 -72
  226. package/dist/src/core/typecheck.js +2 -1
  227. package/dist/src/core/wire-destroy.js +14 -3
  228. package/dist/src/core/wire-init.js +37 -40
  229. package/dist/src/core/wire-subscript.js +11 -3
  230. package/dist/src/core/wire-utils.js +4 -4
  231. package/dist/src/core/xpcshell-appdir.js +3 -0
  232. package/dist/src/errors/base.d.ts +13 -0
  233. package/dist/src/errors/base.js +13 -0
  234. package/dist/src/errors/rebase.d.ts +13 -0
  235. package/dist/src/errors/rebase.js +17 -0
  236. package/dist/src/types/commands/options.d.ts +86 -1
  237. package/dist/src/types/commands/patches.d.ts +8 -0
  238. package/dist/src/types/furnace.d.ts +7 -0
  239. package/dist/src/utils/build-info.d.ts +31 -0
  240. package/dist/src/utils/build-info.js +98 -0
  241. package/dist/src/utils/concurrency.d.ts +8 -0
  242. package/dist/src/utils/concurrency.js +21 -0
  243. package/dist/src/utils/errors.d.ts +39 -0
  244. package/dist/src/utils/errors.js +55 -0
  245. package/dist/src/utils/fs.js +5 -12
  246. package/dist/src/utils/logger.d.ts +14 -2
  247. package/dist/src/utils/logger.js +43 -13
  248. package/dist/src/utils/options.d.ts +21 -4
  249. package/dist/src/utils/options.js +26 -6
  250. package/dist/src/utils/package-root.js +7 -4
  251. package/dist/src/utils/process-group.js +13 -2
  252. package/dist/src/utils/stdio-drain.d.ts +28 -0
  253. package/dist/src/utils/stdio-drain.js +54 -0
  254. package/dist/src/utils/validation.d.ts +23 -2
  255. package/dist/src/utils/validation.js +29 -3
  256. package/package.json +9 -9
@@ -7,7 +7,7 @@ import MagicString from 'magic-string';
7
7
  import { GeneralError } from '../errors/base.js';
8
8
  import { BuildError } from '../errors/build.js';
9
9
  import { pathExists, readText, writeText } from '../utils/fs.js';
10
- import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
10
+ import { asEstree, detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
11
11
  import { withParserFallback } from './parser-fallback.js';
12
12
  import { assertBraceBalancePreserved, findNearestTryLine, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
13
13
  const BROWSER_MAIN_JS = 'browser/base/content/browser-main.js';
@@ -28,7 +28,7 @@ export function addSubscriptAST(content, name, marker = DEFAULT_MARKER) {
28
28
  walkAST(ast, {
29
29
  enter(node) {
30
30
  if (node.type === 'TryStatement') {
31
- const n = node;
31
+ const n = asEstree(node);
32
32
  const src = getNodeSource(content, n);
33
33
  if (src.includes('loadSubScript')) {
34
34
  tryNodes.push(n);
@@ -137,7 +137,15 @@ export async function addSubscriptToBrowserMain(engineDir, name, marker = DEFAUL
137
137
  if (content.includes(`content/${name}.js"`)) {
138
138
  return false;
139
139
  }
140
- const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS);
140
+ const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS,
141
+ // Rethrow only the internal-invariant GeneralErrors ("Unexpected empty
142
+ // …array"): those are programming bugs, and retrying the legacy scanner
143
+ // cannot fix a broken invariant — it just buries the stack. Everything
144
+ // else still falls back, which is load-bearing: the AST path raises a raw
145
+ // acorn SyntaxError on chrome sources acorn cannot parse (preprocessor
146
+ // directives), and BuildError when the file's shape is unexpected. Both
147
+ // are exactly what the legacy scanner is here to handle.
148
+ (error) => error instanceof GeneralError);
141
149
  if (usedFallback) {
142
150
  assertBraceBalancePreserved(content, value, BROWSER_MAIN_JS);
143
151
  }
@@ -1,5 +1,5 @@
1
1
  import { GeneralError, ParserFallbackError } from '../errors/base.js';
2
- import { walkAST } from './ast-utils.js';
2
+ import { asEstree, walkAST } from './ast-utils.js';
3
3
  /**
4
4
  * Validates a name for safe interpolation into generated JavaScript string literals.
5
5
  * Rejects strings containing characters that could break out of JS strings or inject code.
@@ -336,13 +336,13 @@ export function findMethodBody(ast, methodName) {
336
336
  if (found)
337
337
  return;
338
338
  if (node.type === 'Property') {
339
- const prop = node;
339
+ const prop = asEstree(node);
340
340
  if (prop.key.type === 'Identifier' &&
341
341
  names.includes(prop.key.name) &&
342
342
  (prop.value.type === 'FunctionExpression' ||
343
343
  prop.value.type === 'ArrowFunctionExpression')) {
344
- const fn = prop.value;
345
- found = fn.body;
344
+ const fn = asEstree(prop.value);
345
+ found = asEstree(fn.body);
346
346
  }
347
347
  }
348
348
  },
@@ -203,6 +203,7 @@ export async function resolveAbsoluteAppPath(objDirAbs, relativeAppdir) {
203
203
  entries = await readdir(distDir);
204
204
  }
205
205
  catch {
206
+ // No readable dist directory means no appdir candidate can be resolved.
206
207
  return null;
207
208
  }
208
209
  for (const entry of entries) {
@@ -254,6 +255,8 @@ export async function resolveXpcshellAppdirArg(engineDir, testPaths, objDirName)
254
255
  body = await readText(manifestPath);
255
256
  }
256
257
  catch {
258
+ // An unreadable manifest cannot declare an appdir; skip it and keep
259
+ // walking the remaining candidates.
257
260
  continue;
258
261
  }
259
262
  // Operator already migrated — harness will read the appname-keyed
@@ -83,6 +83,19 @@ export declare class ExecTimeoutError extends FireForgeError {
83
83
  timeoutMs: number, cause?: unknown);
84
84
  get userMessage(): string;
85
85
  }
86
+ /**
87
+ * Error thrown when a file-lock wait times out because another process
88
+ * holds the lock (FORGE H5).
89
+ *
90
+ * Dedicated subclass so the CLI boundary renders lock contention as the
91
+ * one-line reason-first/remedy-second refusal it is — before this, the
92
+ * timeout surfaced as a plain `Error`, which `withErrorHandling` treats
93
+ * as an internal failure and prints with a five-frame stack. The refusal
94
+ * was always correct; the presentation made it look like a crash.
95
+ */
96
+ export declare class LockContentionError extends FireForgeError {
97
+ readonly code: 1;
98
+ }
86
99
  /**
87
100
  * Error thrown when the user cancels an interactive prompt.
88
101
  */
@@ -113,6 +113,19 @@ export class ExecTimeoutError extends FireForgeError {
113
113
  'If the host is just slow or loaded, re-running may succeed.');
114
114
  }
115
115
  }
116
+ /**
117
+ * Error thrown when a file-lock wait times out because another process
118
+ * holds the lock (FORGE H5).
119
+ *
120
+ * Dedicated subclass so the CLI boundary renders lock contention as the
121
+ * one-line reason-first/remedy-second refusal it is — before this, the
122
+ * timeout surfaced as a plain `Error`, which `withErrorHandling` treats
123
+ * as an internal failure and prints with a five-frame stack. The refusal
124
+ * was always correct; the presentation made it look like a crash.
125
+ */
126
+ export class LockContentionError extends FireForgeError {
127
+ code = ExitCode.GENERAL_ERROR;
128
+ }
116
129
  /**
117
130
  * Error thrown when the user cancels an interactive prompt.
118
131
  */
@@ -18,3 +18,16 @@ export declare class RebaseSessionExistsError extends RebaseError {
18
18
  export declare class NoRebaseSessionError extends RebaseError {
19
19
  constructor();
20
20
  }
21
+ /**
22
+ * Thrown when the session file exists but cannot be used.
23
+ *
24
+ * This case had no error of its own before 0.41.0 and was reported as
25
+ * {@link NoRebaseSessionError} by `--continue`/`--abort` while `rebase`
26
+ * reported {@link RebaseSessionExistsError} — a closed cycle in which each
27
+ * command pointed at the other two. The message names the file so an operator
28
+ * always has a way out, and `--abort` now clears a corrupt session rather than
29
+ * refusing to run against one.
30
+ */
31
+ export declare class CorruptRebaseSessionError extends RebaseError {
32
+ constructor(sessionPath: string, reason: string);
33
+ }
@@ -32,3 +32,20 @@ export class NoRebaseSessionError extends RebaseError {
32
32
  super('No rebase session in progress. Start one with "fireforge rebase".');
33
33
  }
34
34
  }
35
+ /**
36
+ * Thrown when the session file exists but cannot be used.
37
+ *
38
+ * This case had no error of its own before 0.41.0 and was reported as
39
+ * {@link NoRebaseSessionError} by `--continue`/`--abort` while `rebase`
40
+ * reported {@link RebaseSessionExistsError} — a closed cycle in which each
41
+ * command pointed at the other two. The message names the file so an operator
42
+ * always has a way out, and `--abort` now clears a corrupt session rather than
43
+ * refusing to run against one.
44
+ */
45
+ export class CorruptRebaseSessionError extends RebaseError {
46
+ constructor(sessionPath, reason) {
47
+ super(`The rebase session at ${sessionPath} cannot be read (${reason}).\n` +
48
+ 'Run "fireforge rebase --abort" to discard it and restore the engine, ' +
49
+ 'or delete the file to start over.');
50
+ }
51
+ }
@@ -289,6 +289,31 @@ export interface ReExportOptions {
289
289
  * explicitly.
290
290
  */
291
291
  refuseAdjacentUnmanaged?: boolean;
292
+ /**
293
+ * Bypass per-patch lint result cache reads AND writes for this
294
+ * invocation (`--no-cache`). The cache is what makes a repeat
295
+ * re-export of an unchanged patch skip the checkJs program build
296
+ * entirely (FORGE J1).
297
+ */
298
+ noCache?: boolean;
299
+ /**
300
+ * Refuse (non-zero exit, patch not written) a re-export whose refreshed
301
+ * body would absorb engine lines not present in the old patch body —
302
+ * the multi-session guard against silently capturing another session's
303
+ * uncommitted edits in files the patch already owns. The drift preview
304
+ * always prints; this flag turns it into a hard stop. Mutually
305
+ * exclusive with `--scan` and `--files`.
306
+ */
307
+ refuseForeignDrift?: boolean;
308
+ /**
309
+ * Engine-relative files whose drift is EXPECTED (`--expect`, repeatable;
310
+ * requires `--refuse-foreign-drift`). The content-based drift detector
311
+ * cannot tell the exporting session's own slice edits from another
312
+ * session's (FORGE L6), so a real slice export under the refusal flag
313
+ * names its intended files here: drift confined to expected files
314
+ * proceeds, drift anywhere else still refuses.
315
+ */
316
+ expect?: string[];
292
317
  }
293
318
  /**
294
319
  * Options for the `fireforge patch tier` subcommand. Sets or clears the
@@ -386,6 +411,8 @@ export interface PatchSplitOptions {
386
411
  forceUnsafe?: boolean;
387
412
  /** Skip per-patch lint of the projected bodies. */
388
413
  skipLint?: boolean;
414
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
415
+ waitLock?: number | boolean;
389
416
  }
390
417
  /**
391
418
  * Options for the `fireforge patch move-files` subcommand. Without
@@ -413,6 +440,8 @@ export interface PatchMoveFilesOptions {
413
440
  forceUnsafe?: boolean;
414
441
  /** Skip per-patch lint of the projected bodies. */
415
442
  skipLint?: boolean;
443
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
444
+ waitLock?: number | boolean;
416
445
  }
417
446
  /**
418
447
  * Options for the rebase command.
@@ -484,6 +513,24 @@ export interface TestOptions {
484
513
  headless?: boolean;
485
514
  /** Run incremental UI build before testing */
486
515
  build?: boolean;
516
+ /**
517
+ * Run the scoped pre-test build (packaging exactly the requested paths,
518
+ * MIXED harnesses allowed) and exit without dispatching tests. One
519
+ * ~10-minute build then covers both an xpcshell and a browser-chrome
520
+ * half of a slice; run each harness afterwards without `--build`
521
+ * (FORGE J9). Emits `FIREFORGE-VERDICT: PASS` on success.
522
+ */
523
+ buildOnly?: boolean;
524
+ /**
525
+ * Union the paths this build packages onto the recorded coverage claim
526
+ * instead of REPLACING it (FORGE L1), so successive scoped builds
527
+ * accumulate and an earlier slice's build-less runs stay covered. Valid
528
+ * only with `--build`/`--build-only` and explicit paths. Refused
529
+ * fail-closed unless the build anchor is unchanged: same engine HEAD,
530
+ * same `engine/mozconfig`, and every previously fingerprinted packageable
531
+ * file byte-identical. See `src/core/coverage-extend.ts`.
532
+ */
533
+ extendCoverage?: boolean;
487
534
  /** Forward mach's pathless auto-selection mode. Valid only with no explicit paths. */
488
535
  auto?: boolean;
489
536
  /**
@@ -745,6 +792,8 @@ export interface PatchDeleteOptions {
745
792
  dryRun?: boolean;
746
793
  /** Bypass the hard refusal when later patches depend on the target. */
747
794
  forceUnsafe?: boolean;
795
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
796
+ waitLock?: number | boolean;
748
797
  }
749
798
  /**
750
799
  * Options for the `fireforge patch rename` subcommand. Updates the
@@ -760,9 +809,22 @@ export interface PatchRenameOptions {
760
809
  * New human-readable name. Sanitised the same way `export --name`
761
810
  * sanitises into the filename slug (lowercase, non-alphanumerics
762
811
  * collapsed to `-`, length-capped). The patch's `name` field stores
763
- * the raw value; the filename uses the sanitised slug.
812
+ * the raw value; the filename uses the sanitised slug. Optional when
813
+ * `--category`/`--order`/`--description` carry the change.
764
814
  */
765
815
  to?: string;
816
+ /**
817
+ * New category (validated against configured categories). Rewrites
818
+ * the filename prefix and the manifest row in one transaction —
819
+ * recategorising no longer requires `patch delete` + fresh export.
820
+ */
821
+ category?: string;
822
+ /**
823
+ * Move the patch to this exact UNUSED sparse order. Collisions refuse
824
+ * with a pointer to `patch reorder` (which renumbers siblings); an
825
+ * order change is projected through cross-patch lint first.
826
+ */
827
+ order?: number;
766
828
  /**
767
829
  * Replacement description. Omit to leave the description unchanged
768
830
  * (intentional — operators frequently want to relabel the slug
@@ -775,6 +837,8 @@ export interface PatchRenameOptions {
775
837
  yes?: boolean;
776
838
  /** Bypass force-mode patchPolicy refusals. */
777
839
  forceUnsafe?: boolean;
840
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
841
+ waitLock?: number | boolean;
778
842
  }
779
843
  /**
780
844
  * Options for the patch reorder command.
@@ -786,6 +850,8 @@ export interface PatchReorderOptions {
786
850
  yes?: boolean;
787
851
  dryRun?: boolean;
788
852
  forceUnsafe?: boolean;
853
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
854
+ waitLock?: number | boolean;
789
855
  }
790
856
  /**
791
857
  * Options for the patch compact command.
@@ -797,6 +863,8 @@ export interface PatchCompactOptions {
797
863
  dryRun?: boolean;
798
864
  /** Bypass force-mode patchPolicy refusals. */
799
865
  forceUnsafe?: boolean;
866
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
867
+ waitLock?: number | boolean;
800
868
  }
801
869
  /**
802
870
  * Options for the status command.
@@ -819,6 +887,23 @@ export interface StatusOptions {
819
887
  testCoverage?: boolean;
820
888
  /** Output machine-readable JSON instead of human-readable text. */
821
889
  json?: boolean;
890
+ /**
891
+ * With `--json`: emit only the summary counts (plus the offending
892
+ * files per fail-set classification when the `--check`/`--fail-on`
893
+ * policy is active), omitting the per-file `files[]` payload that
894
+ * grows with the queue (FORGE K8). Refused without `--json`.
895
+ */
896
+ summary?: boolean;
897
+ /**
898
+ * With `--json` (and composing with `--summary`), append an additive
899
+ * `ownership` block — the flat path→owning-patch rows plus
900
+ * managed/unmanaged/conflict counts — so a gate reads ownership,
901
+ * classification, and the check verdict from ONE scan instead of three
902
+ * back-to-back `status` invocations (FORGE L3). A modifier, not a mode:
903
+ * exit semantics are unchanged, so ownership conflicts still fail only
904
+ * the human `--ownership` mode. Refused without `--json`.
905
+ */
906
+ includeOwnership?: boolean;
822
907
  /**
823
908
  * Exit non-zero when any classification in the fail policy is
824
909
  * non-empty (default policy: unmanaged, patch-owned-drift, conflict).
@@ -212,6 +212,14 @@ export interface PatchLintIssue {
212
212
  fingerprint?: string;
213
213
  /** Human-readable description of the issue */
214
214
  message: string;
215
+ /**
216
+ * Queue entry filenames this issue implicates (the declaring/importing
217
+ * patch, or every creator for a duplicate-creation clash). Used by the
218
+ * export placement gate to attribute projected errors to the exported
219
+ * patch vs pre-existing patches without parsing messages or the
220
+ * rename-sensitive fingerprint (FORGE K9).
221
+ */
222
+ patches?: string[];
215
223
  /** Severity: errors block export, warnings are advisory, notices are informational (not counted) */
216
224
  severity: 'error' | 'warning' | 'notice';
217
225
  /**
@@ -260,6 +260,13 @@ export interface ApplyResult {
260
260
  * persisted — the engine has been restored to its pre-apply state.
261
261
  */
262
262
  rolledBack?: boolean;
263
+ /**
264
+ * Operator-facing warnings, printed by `logApplyResult`. Populated when
265
+ * apply overwrote a deployed, patch-owned file whose engine bytes
266
+ * differed from the component source (FORGE J6) — the case where a
267
+ * shipped engine-only fix is silently reverted by the copy.
268
+ */
269
+ warnings?: string[];
263
270
  }
264
271
  /**
265
272
  * An action that would be performed during a dry-run deploy.
@@ -0,0 +1,31 @@
1
+ /** Source identity of the running build. */
2
+ export interface BuildIdentity {
3
+ shortCommit: string;
4
+ dirty: boolean;
5
+ /**
6
+ * Content hash of the uncommitted diff, present only for a DIRTY
7
+ * installed build (FORGE L8). Two packs from the same HEAD with
8
+ * different uncommitted content share `shortCommit` and the `.dirty`
9
+ * marker, so without this they report byte-identical `--version`
10
+ * strings — which is what made a field report against
11
+ * `0.41.0+g<sha>.dirty` impossible to trace back to a specific pack.
12
+ * Absent in a git checkout: identity there comes from live git, and
13
+ * hashing the full diff on every `--version` call is not worth it.
14
+ */
15
+ dirtyHash?: string;
16
+ }
17
+ /** @internal Reads the stamped dist/build-info.json; null when absent/invalid. */
18
+ export declare function readBuildInfoFile(packageRoot: string): BuildIdentity | null;
19
+ type GitRunner = (args: string[]) => string;
20
+ /** @internal Live git identity of the checkout; null when git is unusable. */
21
+ export declare function readGitBuildIdentity(runGit?: GitRunner): BuildIdentity | null;
22
+ /**
23
+ * @internal Formats `<version>+g<sha>[.dirty[.<hash>]]`; plain version on
24
+ * null identity. The trailing content hash appears only for a dirty
25
+ * INSTALLED build, where `dist/build-info.json` already carries it — every
26
+ * segment stays valid semver build metadata (dot-separated alphanumerics).
27
+ */
28
+ export declare function formatVersionWithIdentity(version: string, identity: BuildIdentity | null): string;
29
+ /** The `--version` string: semver plus build identity when known. */
30
+ export declare function getCliVersion(): string;
31
+ export {};
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Build identity for `--version` (FORGE K2/K3/L8). Three distinct builds
4
+ * shipped as plain "0.41.0" — consumers had to fingerprint tarballs by
5
+ * sha256 to know which build a claim was verified against. `--version`
6
+ * now reports `<semver>+g<short-sha>[.dirty[.<content-hash>]]`:
7
+ *
8
+ * - In a git checkout (dev runs via tsx), identity comes from live git —
9
+ * a stale `dist/build-info.json` from an old build must never win.
10
+ * - In an installed package, it comes from `dist/build-info.json`,
11
+ * stamped by scripts/generate-build-info.mjs at build/pack time.
12
+ * - When neither yields an identity, `--version` degrades to the plain
13
+ * semver. Reading identity must never throw.
14
+ *
15
+ * Deliberately NOT wired into the per-patch lint cache key: identity
16
+ * would churn the cache on every commit at the same semver for no
17
+ * correctness gain (`getPackageVersion` stays the cache input).
18
+ */
19
+ import { execFileSync } from 'node:child_process';
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { getPackageRoot, getPackageVersion } from './package-root.js';
23
+ import { isObject } from './validation.js';
24
+ /** @internal Reads the stamped dist/build-info.json; null when absent/invalid. */
25
+ export function readBuildInfoFile(packageRoot) {
26
+ try {
27
+ const raw = readFileSync(join(packageRoot, 'dist', 'build-info.json'), 'utf-8');
28
+ const data = JSON.parse(raw);
29
+ if (!isObject(data) || data['schemaVersion'] !== 1)
30
+ return null;
31
+ const shortCommit = data['shortCommit'];
32
+ const dirty = data['dirty'];
33
+ if (typeof shortCommit !== 'string' || shortCommit.length === 0)
34
+ return null;
35
+ const dirtyHash = data['dirtyHash'];
36
+ return {
37
+ shortCommit,
38
+ dirty: dirty === true,
39
+ ...(typeof dirtyHash === 'string' && dirtyHash.length > 0 ? { dirtyHash } : {}),
40
+ };
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ const defaultGitRunner = (args) => execFileSync('git', args, {
47
+ cwd: getPackageRoot(),
48
+ encoding: 'utf-8',
49
+ stdio: ['ignore', 'pipe', 'ignore'],
50
+ });
51
+ /** @internal Live git identity of the checkout; null when git is unusable. */
52
+ export function readGitBuildIdentity(runGit = defaultGitRunner) {
53
+ try {
54
+ const commit = runGit(['rev-parse', 'HEAD']).trim();
55
+ if (!/^[0-9a-f]{7,40}$/.test(commit))
56
+ return null;
57
+ const status = runGit(['status', '--porcelain']);
58
+ return { shortCommit: commit.slice(0, 12), dirty: status.length > 0 };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Resolves the running build's identity: live git in a checkout (covers
66
+ * worktrees, whose `.git` is a file), else the stamped build-info file,
67
+ * else null. Never throws.
68
+ */
69
+ function getBuildIdentity() {
70
+ try {
71
+ const packageRoot = getPackageRoot();
72
+ if (existsSync(join(packageRoot, '.git'))) {
73
+ return readGitBuildIdentity();
74
+ }
75
+ return readBuildInfoFile(packageRoot);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * @internal Formats `<version>+g<sha>[.dirty[.<hash>]]`; plain version on
83
+ * null identity. The trailing content hash appears only for a dirty
84
+ * INSTALLED build, where `dist/build-info.json` already carries it — every
85
+ * segment stays valid semver build metadata (dot-separated alphanumerics).
86
+ */
87
+ export function formatVersionWithIdentity(version, identity) {
88
+ if (identity === null)
89
+ return version;
90
+ if (!identity.dirty)
91
+ return `${version}+g${identity.shortCommit}`;
92
+ const contentHash = identity.dirtyHash === undefined ? '' : `.${identity.dirtyHash}`;
93
+ return `${version}+g${identity.shortCommit}.dirty${contentHash}`;
94
+ }
95
+ /** The `--version` string: semver plus build identity when known. */
96
+ export function getCliVersion() {
97
+ return formatVersionWithIdentity(getPackageVersion(), getBuildIdentity());
98
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bounded-concurrency helpers shared by commands that fan out per-file IO
3
+ * (status classification, import's unmanaged-dirty guard, re-export scans,
4
+ * the dry-run purity guard). Kept deliberately tiny: one order-preserving
5
+ * mapper, no queues, no cancellation.
6
+ */
7
+ /** Maps items with at most `limit` in-flight promises. Preserves order. */
8
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bounded-concurrency helpers shared by commands that fan out per-file IO
3
+ * (status classification, import's unmanaged-dirty guard, re-export scans,
4
+ * the dry-run purity guard). Kept deliberately tiny: one order-preserving
5
+ * mapper, no queues, no cancellation.
6
+ */
7
+ /** Maps items with at most `limit` in-flight promises. Preserves order. */
8
+ export async function mapWithConcurrency(items, limit, fn) {
9
+ const results = new Array(items.length);
10
+ let next = 0;
11
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
12
+ for (;;) {
13
+ const index = next++;
14
+ if (index >= items.length)
15
+ return;
16
+ results[index] = await fn(items[index], index);
17
+ }
18
+ });
19
+ await Promise.all(workers);
20
+ return results;
21
+ }
@@ -1,2 +1,41 @@
1
+ /**
2
+ * Throwable normalisation primitives.
3
+ *
4
+ * **This module must import nothing.** It sits at the very bottom of the
5
+ * dependency graph — 81 modules across `core/`, `commands/`, `utils/` and
6
+ * `bin/` reach it — so it is the one place a shared helper can live and be
7
+ * provably cycle-immune. Adding an import here makes that whole subtree
8
+ * reachable from every util, and any future `src/errors/*` module wanting a
9
+ * util back (`errors/git.ts` already carries a mirrored index-lock heuristic
10
+ * that would like `getNodeErrorCode`) then closes the loop and fails the
11
+ * `dpdm --exit-code circular:1` release gate. A helper that needs an import
12
+ * belongs in a sibling leaf module, not here.
13
+ */
1
14
  /** Normalizes unknown throwables into an Error instance. */
2
15
  export declare function toError(error: unknown): Error;
16
+ /**
17
+ * Extracts a Node errno string (`ENOENT`, `EACCES`, `EPERM`, …) from an
18
+ * unknown throwable, or `undefined` when the value carries no string `code`.
19
+ *
20
+ * The check is deliberately structural rather than `error instanceof Error`:
21
+ * a plain object carrying `.code` — exactly the shape {@link toError} exists
22
+ * to normalise — reaches errno consumers through rejected promises and
23
+ * cross-realm throws. Nine hand-rolled copies of this predicate existed
24
+ * before 0.41.0, four of them (in `utils/fs.ts`) gated on `instanceof Error`
25
+ * and so misclassified that shape as "no code".
26
+ */
27
+ export declare function getNodeErrorCode(error: unknown): string | undefined;
28
+ /**
29
+ * Checks whether a process with the given PID is still running, using
30
+ * `kill(pid, 0)` — no signal is sent, only the existence check is performed.
31
+ *
32
+ * Only `ESRCH` ("no such process") means dead. **`EPERM` means ALIVE**: the
33
+ * process exists but is owned by another uid, which happens routinely with
34
+ * root-owned builds, `sudo`, shared CI runners and container UID mismatches.
35
+ * Two copies of this predicate (`tree-store.ts`, `doctor-furnace.ts`) read
36
+ * EPERM as dead before 0.41.0, and both gated a recursive delete — a live
37
+ * build's tree clone and a live furnace lock were removed out from under
38
+ * their owner. Any other errno is treated as "unknown, assume alive", which
39
+ * is the safe direction for every caller: it refuses rather than destroys.
40
+ */
41
+ export declare function isProcessAlive(pid: number): boolean;
@@ -1,4 +1,17 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Throwable normalisation primitives.
4
+ *
5
+ * **This module must import nothing.** It sits at the very bottom of the
6
+ * dependency graph — 81 modules across `core/`, `commands/`, `utils/` and
7
+ * `bin/` reach it — so it is the one place a shared helper can live and be
8
+ * provably cycle-immune. Adding an import here makes that whole subtree
9
+ * reachable from every util, and any future `src/errors/*` module wanting a
10
+ * util back (`errors/git.ts` already carries a mirrored index-lock heuristic
11
+ * that would like `getNodeErrorCode`) then closes the loop and fails the
12
+ * `dpdm --exit-code circular:1` release gate. A helper that needs an import
13
+ * belongs in a sibling leaf module, not here.
14
+ */
2
15
  /** Normalizes unknown throwables into an Error instance. */
3
16
  export function toError(error) {
4
17
  if (error instanceof Error) {
@@ -12,3 +25,45 @@ export function toError(error) {
12
25
  }
13
26
  return new Error(typeof error === 'string' ? error : String(error), { cause: error });
14
27
  }
28
+ /**
29
+ * Extracts a Node errno string (`ENOENT`, `EACCES`, `EPERM`, …) from an
30
+ * unknown throwable, or `undefined` when the value carries no string `code`.
31
+ *
32
+ * The check is deliberately structural rather than `error instanceof Error`:
33
+ * a plain object carrying `.code` — exactly the shape {@link toError} exists
34
+ * to normalise — reaches errno consumers through rejected promises and
35
+ * cross-realm throws. Nine hand-rolled copies of this predicate existed
36
+ * before 0.41.0, four of them (in `utils/fs.ts`) gated on `instanceof Error`
37
+ * and so misclassified that shape as "no code".
38
+ */
39
+ export function getNodeErrorCode(error) {
40
+ if (typeof error === 'object' &&
41
+ error !== null &&
42
+ 'code' in error &&
43
+ typeof error.code === 'string') {
44
+ return error.code;
45
+ }
46
+ return undefined;
47
+ }
48
+ /**
49
+ * Checks whether a process with the given PID is still running, using
50
+ * `kill(pid, 0)` — no signal is sent, only the existence check is performed.
51
+ *
52
+ * Only `ESRCH` ("no such process") means dead. **`EPERM` means ALIVE**: the
53
+ * process exists but is owned by another uid, which happens routinely with
54
+ * root-owned builds, `sudo`, shared CI runners and container UID mismatches.
55
+ * Two copies of this predicate (`tree-store.ts`, `doctor-furnace.ts`) read
56
+ * EPERM as dead before 0.41.0, and both gated a recursive delete — a live
57
+ * build's tree clone and a live furnace lock were removed out from under
58
+ * their owner. Any other errno is treated as "unknown, assume alive", which
59
+ * is the safe direction for every caller: it refuses rather than destroys.
60
+ */
61
+ export function isProcessAlive(pid) {
62
+ try {
63
+ process.kill(pid, 0);
64
+ return true;
65
+ }
66
+ catch (error) {
67
+ return getNodeErrorCode(error) !== 'ESRCH';
68
+ }
69
+ }
@@ -2,6 +2,7 @@
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { access, chmod, copyFile as fsCopyFile, lstat, mkdir, open, readdir, readFile, rename, rm, stat, statfs, } from 'node:fs/promises';
4
4
  import { basename, dirname, join } from 'node:path';
5
+ import { getNodeErrorCode } from './errors.js';
5
6
  const RETRIABLE_REMOVE_ERRORS = new Set(['ENOTEMPTY', 'EBUSY', 'EPERM']);
6
7
  const RETRIABLE_RENAME_ERRORS = new Set(['EPERM', 'EACCES', 'EBUSY']);
7
8
  function sleep(ms) {
@@ -33,9 +34,7 @@ export async function pathExistsStrict(path) {
33
34
  return true;
34
35
  }
35
36
  catch (error) {
36
- const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
37
- ? error.code
38
- : undefined;
37
+ const code = getNodeErrorCode(error);
39
38
  if (code === 'ENOENT') {
40
39
  return false;
41
40
  }
@@ -68,9 +67,7 @@ export async function removeDir(path) {
68
67
  return;
69
68
  }
70
69
  catch (error) {
71
- const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
72
- ? error.code
73
- : undefined;
70
+ const code = getNodeErrorCode(error);
74
71
  if (!code || !RETRIABLE_REMOVE_ERRORS.has(code) || attempt === 4) {
75
72
  throw error;
76
73
  }
@@ -177,9 +174,7 @@ export async function writeFileAtomic(path, content) {
177
174
  existingMode = (await stat(path)).mode;
178
175
  }
179
176
  catch (error) {
180
- const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
181
- ? error.code
182
- : undefined;
177
+ const code = getNodeErrorCode(error);
183
178
  if (code !== 'ENOENT') {
184
179
  throw error;
185
180
  }
@@ -223,9 +218,7 @@ async function renameWithRetries(from, to) {
223
218
  return;
224
219
  }
225
220
  catch (error) {
226
- const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
227
- ? error.code
228
- : undefined;
221
+ const code = getNodeErrorCode(error);
229
222
  if (!code || !RETRIABLE_RENAME_ERRORS.has(code) || attempt === 4) {
230
223
  throw error;
231
224
  }