@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Deferred `fail-on-error` exits for introspected runs.
3
+ *
4
+ * `--fail-on-error` promises an operator that a failing dependency extractor
5
+ * never produces a plausible-but-incomplete BOM, so the extractors exit the
6
+ * process the moment a build tool fails. Under `--introspect` that exit
7
+ * destroys the very evidence the run exists to produce: the ledger events
8
+ * recorded a statement earlier die with the process, `postProcess` never
9
+ * runs, and neither the BOM nor the fidelity report is written.
10
+ *
11
+ * On an introspected run the extractor instead records the failure here and
12
+ * throws {@link DeferredFailOnError}, which the extractor catches at its
13
+ * completion point so it still returns everything it collected before the
14
+ * failure — the incomplete-result fallbacks stay skipped, exactly as
15
+ * `fail-on-error` demands. The CLI then writes the BOM and both reports and
16
+ * exits with the dedicated {@link INTROSPECTION_FAILURE_EXIT_CODE} status, so
17
+ * the exit code still claims "an extractor failed", distinct from the
18
+ * fidelity gate's 4.
19
+ *
20
+ * Without introspection this module is a pass-through to `process.exit(1)`;
21
+ * the default contract of the flag is unchanged.
22
+ */
23
+
24
+ import {
25
+ isIntrospectionEnabled,
26
+ LEDGER_EVENT_KINDS,
27
+ recordLedgerEvent,
28
+ } from "./buildLedger.js";
29
+ import { thoughtLog } from "./logger.js";
30
+
31
+ /**
32
+ * Exit status used when an introspected run completes its outputs despite a
33
+ * `--fail-on-error` extractor failure. Distinct from 1 (generic failure),
34
+ * 3 (TEA publish failure) and 4 (the introspection fidelity gate).
35
+ *
36
+ * @type {number}
37
+ */
38
+ export const INTROSPECTION_FAILURE_EXIT_CODE = 5;
39
+
40
+ /**
41
+ * Control-flow signal thrown by {@link deferFailOnError} on an introspected
42
+ * run. The extractor that called it must let it unwind to its completion
43
+ * point, keep everything it collected until then, and rethrow anything else.
44
+ */
45
+ export class DeferredFailOnError extends Error {
46
+ /**
47
+ * @param {string} message Human-readable failure summary.
48
+ */
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "DeferredFailOnError";
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The extractor failures deferred so far. Populated only on introspected
57
+ * runs; the CLI reads it after the BOM and the reports are written to decide
58
+ * the process exit status.
59
+ *
60
+ * @type {Array<{ecosystem: string, tool: string, detail: string, exitCode: number|undefined, command: string|undefined, outputExcerpt: string|undefined}>}
61
+ */
62
+ const deferredFailures = [];
63
+
64
+ /**
65
+ * Test whether an error is the fail-on-error deferral signal.
66
+ *
67
+ * @param {unknown} err The caught error.
68
+ * @returns {boolean} True when the error unwinds a deferred fail-on-error abort.
69
+ */
70
+ export function isDeferredFailOnError(err) {
71
+ return err instanceof DeferredFailOnError;
72
+ }
73
+
74
+ /**
75
+ * Snapshot of the deferred failures recorded by this run.
76
+ *
77
+ * @returns {Array<Object>} Frozen copies of the recorded failures.
78
+ */
79
+ export function getDeferredFailures() {
80
+ return deferredFailures.map((failure) => Object.freeze({ ...failure }));
81
+ }
82
+
83
+ /**
84
+ * Handle a failing extractor on behalf of a `options.failOnError &&
85
+ * process.exit(1)` site. Without `fail-on-error` the run continues exactly
86
+ * as it would have. With the flag and no introspection the process exits 1,
87
+ * unchanged. On an introspected run the failure is recorded — including one
88
+ * ledger observation on the ecosystem row the failure cost evidence — and
89
+ * the deferral signal is thrown so the extractor stops where it is without
90
+ * taking any incomplete-result fallback.
91
+ *
92
+ * @param {Object} options CLI options.
93
+ * @param {Object} failure What failed, for the operator message and the report.
94
+ * @param {string} failure.tool The build tool that failed.
95
+ * @param {string} [failure.ecosystem] The ecosystem the tool serves, so the
96
+ * observation lands on the row whose evidence the failure cost.
97
+ * @param {string} failure.detail One sentence, present tense.
98
+ * @param {number} [failure.exitCode] The tool's exit status, when known.
99
+ * @param {string} [failure.command] The redacted command that was attempted.
100
+ * @param {string} [failure.outputExcerpt] The failed command's combined output,
101
+ * when the caller already holds it; the recorder bounds and redacts it.
102
+ * @returns {void} Never returns on an introspected run; it throws.
103
+ */
104
+ export function deferFailOnError(options, failure) {
105
+ if (!options?.failOnError) {
106
+ return;
107
+ }
108
+ const entry = {
109
+ ecosystem: failure?.ecosystem || "cdxgen",
110
+ tool: failure?.tool || "unknown",
111
+ detail: failure?.detail || "the extractor reported a failure",
112
+ exitCode:
113
+ typeof failure?.exitCode === "number" ? failure.exitCode : undefined,
114
+ command: failure?.command,
115
+ outputExcerpt: failure?.outputExcerpt,
116
+ };
117
+ if (!isIntrospectionEnabled(options)) {
118
+ process.exit(1);
119
+ // Unreachable in a real run; the explicit return keeps the deferral from
120
+ // continuing wherever process.exit is instrumented rather than fatal.
121
+ return;
122
+ }
123
+ deferredFailures.push(entry);
124
+ thoughtLog(
125
+ `Deferring the fail-on-error exit for ${entry.tool} so the introspection verdict can be produced: ${entry.detail}`,
126
+ );
127
+ // A `command.failed` observation on the row the failure cost evidence: it
128
+ // carries no remediationId, so it reports the deferral without competing
129
+ // with the degradation the extractor recorded for the same failure, and it
130
+ // never trips the truncation marker, which keys on `evidence.degraded`
131
+ // under the reserved recorder ecosystem.
132
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
133
+ ecosystem: entry.ecosystem,
134
+ tool: entry.tool,
135
+ exitCode: entry.exitCode,
136
+ command: entry.command,
137
+ outputExcerpt: entry.outputExcerpt,
138
+ detail: `fail-on-error was deferred so the BOM and the report could be written: ${entry.detail}`,
139
+ });
140
+ throw new DeferredFailOnError(`${entry.tool}: ${entry.detail}`);
141
+ }
package/lib/core/env.js CHANGED
@@ -173,9 +173,33 @@ export function getJavaCommand() {
173
173
  /** Resolved Python executable command (PYTHON_CMD env var > CONDA_PYTHON_EXE > "python"). */
174
174
  export const PYTHON_CMD = getPythonCommand();
175
175
 
176
+ /**
177
+ * Whether an executable of the given name is reachable through PATH.
178
+ *
179
+ * @param {string} name Executable name, without a platform extension.
180
+ * @returns {boolean} True when a PATH entry holds the executable.
181
+ */
182
+ function isOnPath(name) {
183
+ const searchPath = readEnvironmentVariable("PATH");
184
+ if (!searchPath) {
185
+ return false;
186
+ }
187
+ const names =
188
+ process.platform === "win32" ? [`${name}.exe`, `${name}.bat`] : [name];
189
+ return searchPath
190
+ .split(process.platform === "win32" ? ";" : ":")
191
+ .some((binDir) =>
192
+ names.some((binName) => binDir && safeExistsSync(join(binDir, binName))),
193
+ );
194
+ }
195
+
176
196
  /**
177
197
  * Returns the Python executable command to use, resolved in priority order:
178
- * PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python".
198
+ * PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python" > "python3".
199
+ *
200
+ * Distributions that ship only a versioned interpreter leave a bare "python"
201
+ * unresolvable, which would fail every virtualenv this tool creates, so the
202
+ * versioned name is used when it is the only one PATH offers.
179
203
  *
180
204
  * @returns {string} Path or name of the Python executable
181
205
  */
@@ -185,6 +209,8 @@ export function getPythonCommand() {
185
209
  pythonCmd = readEnvironmentVariable("PYTHON_CMD");
186
210
  } else if (readEnvironmentVariable("CONDA_PYTHON_EXE")) {
187
211
  pythonCmd = readEnvironmentVariable("CONDA_PYTHON_EXE");
212
+ } else if (!isOnPath("python") && isOnPath("python3")) {
213
+ pythonCmd = "python3";
188
214
  }
189
215
  return pythonCmd;
190
216
  }
@@ -230,6 +256,19 @@ if (readEnvironmentVariable("CARGO_CMD")) {
230
256
  CARGO_CMD = readEnvironmentVariable("CARGO_CMD");
231
257
  }
232
258
 
259
+ /**
260
+ * Target triple the cargo resolve graph is filtered to, so that a
261
+ * cross-compiled project can be described for the triple it ships on rather
262
+ * than for the union of every triple its lock file covers.
263
+ *
264
+ * `CDXGEN_CARGO_TARGET` is cdxgen's own name for it; `CARGO_BUILD_TARGET` is
265
+ * cargo's standard variable, which a cross-compiling build already sets.
266
+ */
267
+ export const CARGO_TARGET =
268
+ readEnvironmentVariable("CDXGEN_CARGO_TARGET") ||
269
+ readEnvironmentVariable("CARGO_BUILD_TARGET") ||
270
+ undefined;
271
+
233
272
  /** Resolved Clojure CLI executable command (CLJ_CMD env var, or "clj"). */
234
273
  export let CLJ_CMD = "clj";
235
274
  if (readEnvironmentVariable("CLJ_CMD")) {
@@ -281,6 +320,7 @@ export const PROJECT_TYPE_ALIASES = {
281
320
  "java24",
282
321
  "java25",
283
322
  "java26",
323
+ "java27",
284
324
  "groovy",
285
325
  "kotlin",
286
326
  "kt",
@@ -313,6 +353,9 @@ export const PROJECT_TYPE_ALIASES = {
313
353
  "nodejs20",
314
354
  "nodejs22",
315
355
  "nodejs23",
356
+ "nodejs24",
357
+ "nodejs25",
358
+ "nodejs26",
316
359
  "node",
317
360
  "node8",
318
361
  "node10",
@@ -323,11 +366,16 @@ export const PROJECT_TYPE_ALIASES = {
323
366
  "node20",
324
367
  "node22",
325
368
  "node23",
369
+ "node24",
370
+ "node25",
371
+ "node26",
326
372
  "js",
327
373
  "javascript",
328
374
  "typescript",
329
375
  "ts",
330
376
  "tsx",
377
+ "svelte",
378
+ "sveltekit",
331
379
  "yarn",
332
380
  "rush",
333
381
  "bun",
@@ -335,6 +383,7 @@ export const PROJECT_TYPE_ALIASES = {
335
383
  ],
336
384
  mcp: ["mcp"],
337
385
  "ai-skill": ["ai-skill", "skill", "skills"],
386
+ agentic: ["agentic", "agentic-cli", "ai-tools", "ai-cli"],
338
387
  py: [
339
388
  "py",
340
389
  "python",
@@ -346,6 +395,8 @@ export const PROJECT_TYPE_ALIASES = {
346
395
  "python311",
347
396
  "python312",
348
397
  "python313",
398
+ "python314",
399
+ "python315",
349
400
  "pixi",
350
401
  "pip",
351
402
  "poetry",
@@ -372,6 +423,8 @@ export const PROJECT_TYPE_ALIASES = {
372
423
  "dotnet7",
373
424
  "dotnet8",
374
425
  "dotnet9",
426
+ "dotnet10",
427
+ "dotnet11",
375
428
  "dotnet-framework",
376
429
  "dotnet-framework47",
377
430
  "dotnet-framework48",
@@ -391,8 +444,19 @@ export const PROJECT_TYPE_ALIASES = {
391
444
  "hmiproj",
392
445
  ],
393
446
  dart: ["dart", "flutter", "pub"],
394
- haskell: ["haskell", "hackage", "cabal"],
447
+ haskell: ["haskell", "hackage", "cabal", "stack", "stackage"],
395
448
  elixir: ["elixir", "hex", "mix"],
449
+ erlang: ["erlang", "rebar", "rebar3"],
450
+ julia: ["julia"],
451
+ r: ["r", "cran", "renv"],
452
+ ocaml: ["ocaml", "opam", "dune"],
453
+ elm: ["elm"],
454
+ crystal: ["crystal", "shards"],
455
+ nim: ["nim", "nimble"],
456
+ lua: ["lua", "luarocks"],
457
+ perl: ["perl", "cpan", "carton", "cpanfile"],
458
+ spack: ["spack"],
459
+ terraform: ["terraform", "opentofu", "tf"],
396
460
  c: ["c", "cpp", "c++", "conan", "collider"],
397
461
  clojure: ["clojure", "edn", "clj", "leiningen"],
398
462
  github: ["github", "actions"],
@@ -459,6 +523,39 @@ export const PACKAGE_MANAGER_ALIASES = {
459
523
  scala: ["sbt"],
460
524
  };
461
525
 
526
+ /**
527
+ * Project-type prefixes that accept a version suffix pinning a JVM build
528
+ * tool, e.g. `maven3.9.9`, `mvn3.9.9`, `gradle8.14`, `sbt1.10`, or
529
+ * `scala3.6.4`. Kept alongside the alias map because both describe how CLI
530
+ * project types map to base types.
531
+ *
532
+ * @type {string[]}
533
+ */
534
+ export const JVM_BUILD_TOOL_TYPE_PREFIXES = [
535
+ "maven",
536
+ "mvn",
537
+ "gradle",
538
+ "sbt",
539
+ "scala",
540
+ ];
541
+
542
+ /**
543
+ * Check whether a project type pins a JVM build tool version, i.e. it starts
544
+ * with one of the JVM build tool prefixes followed by a digit.
545
+ *
546
+ * @param {string} projectType Project type from the CLI
547
+ * @returns {boolean} True for versioned JVM build tool types.
548
+ */
549
+ export function isVersionedJvmToolProjectType(projectType) {
550
+ const pt = `${projectType || ""}`.toLowerCase();
551
+ for (const prefix of JVM_BUILD_TOOL_TYPE_PREFIXES) {
552
+ if (pt.startsWith(prefix) && /^\d/.test(pt.slice(prefix.length))) {
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+
462
559
  /**
463
560
  * Method to check if a given feature flag is enabled.
464
561
  *
@@ -553,6 +650,15 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
553
650
  ) {
554
651
  baseProjectTypes.push("ruby");
555
652
  }
653
+ // Versioned JVM build tool types such as maven3.9.9 or gradle8.14 are java
654
+ // types with a pinned build tool, so they must dispatch java parsing too.
655
+ if (
656
+ options.projectType?.length &&
657
+ projectTypes.includes("java") &&
658
+ options.projectType.some((pt) => isVersionedJvmToolProjectType(pt))
659
+ ) {
660
+ baseProjectTypes.push("java");
661
+ }
556
662
  const baseExcludeTypes = [];
557
663
  for (const abt of Object.keys(PROJECT_TYPE_ALIASES)) {
558
664
  if (
package/lib/core/fs.js CHANGED
@@ -2,12 +2,15 @@ import { Buffer } from "node:buffer";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import {
5
+ accessSync,
5
6
  copyFileSync,
6
7
  createReadStream,
7
8
  existsSync,
9
+ constants as fsConstants,
8
10
  lstatSync,
9
11
  mkdirSync,
10
12
  mkdtempSync,
13
+ readFileSync,
11
14
  rmSync,
12
15
  unlinkSync,
13
16
  writeFileSync,
@@ -111,6 +114,26 @@ export function safeExistsSync(filePath) {
111
114
  return exists;
112
115
  }
113
116
 
117
+ /**
118
+ * Permission-aware check for whether a path exists and carries the executable
119
+ * bit for the current user. Answers false instead of throwing when the path
120
+ * is absent, unreadable, or blocked by secure mode.
121
+ *
122
+ * @param {string} filePath File path to check.
123
+ * @returns {boolean} True when the path exists and is executable.
124
+ */
125
+ export function safeIsExecutableSync(filePath) {
126
+ if (!safeExistsSync(filePath)) {
127
+ return false;
128
+ }
129
+ try {
130
+ accessSync(filePath, fsConstants.X_OK);
131
+ return true;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
114
137
  /**
115
138
  * Permission- and dry-run-aware wrapper around writeFileSync. Records the
116
139
  * activity and returns undefined when blocked.
@@ -146,6 +169,43 @@ export function safeWriteSync(filePath, data, options) {
146
169
  recordFilesystemActivity("write", filePath, "completed");
147
170
  }
148
171
 
172
+ /**
173
+ * Permission-aware wrapper around readFileSync that returns undefined instead
174
+ * of throwing. Reads are allowed in dry-run mode, so unlike the write wrappers
175
+ * this only guards secure-mode read permission; the read itself is not recorded
176
+ * in the activity ledger because inspection of classified paths is already
177
+ * recorded by safeExistsSync and a read has no side effect to audit.
178
+ *
179
+ * @param {string} filePath File path to read.
180
+ * @param {Object|string} [options] readFileSync options; defaults to UTF-8 text.
181
+ * @returns {string|Buffer|undefined} File content, or undefined when the path cannot be read.
182
+ */
183
+ export function safeReadFileSync(filePath, options = "utf-8") {
184
+ if (!hasReadPermission(filePath)) {
185
+ if (DEBUG_MODE) {
186
+ console.log("cdxgen lacks read permission for a requested path.");
187
+ }
188
+ const pathMetadata = classifyActivityPath(filePath);
189
+ if (pathMetadata) {
190
+ recordPolicyActivity(filePath, {
191
+ metadata: {
192
+ classification: pathMetadata.classification,
193
+ ecosystem: pathMetadata.ecosystem,
194
+ policyType: "fs.read",
195
+ },
196
+ reason: `Denied read of ${pathMetadata.label} ${filePath} due to missing fs.read permission.`,
197
+ status: "blocked",
198
+ });
199
+ }
200
+ return undefined;
201
+ }
202
+ try {
203
+ return readFileSync(filePath, options);
204
+ } catch {
205
+ return undefined;
206
+ }
207
+ }
208
+
149
209
  /**
150
210
  * Safely create a directory without crashing due to a lack of permissions
151
211
  *
@@ -1,31 +1,18 @@
1
1
  import { Console } from "node:console";
2
- import fs from "node:fs";
3
2
  import process from "node:process";
4
- import { Writable } from "node:stream";
5
- import { isMainThread } from "node:worker_threads";
3
+
4
+ import {
5
+ closeAllSyncFileWriters,
6
+ diagnosticStream,
7
+ openSyncFileWriter,
8
+ } from "./syncFileWriter.js";
9
+
10
+ export { diagnosticStream };
6
11
 
7
12
  // Environment variables here are read with raw `process.env` rather than
8
13
  // `readEnvironmentVariable`: that helper lives in activity.js, which imports
9
14
  // this module, so routing these through it would form an import cycle.
10
15
 
11
- // Stream contract: human-readable diagnostics default to stderr so stdout
12
- // carries only the BOM payload. CDXGEN_LOG_STREAM=stdout restores the
13
- // pre-v13 behaviour for callers that scraped cdxgen's stdout.
14
- const logStream =
15
- process.env.CDXGEN_LOG_STREAM === "stdout" ? process.stdout : process.stderr;
16
-
17
- /**
18
- * The stream every human-readable diagnostic is written to.
19
- *
20
- * Exported so that the live region and the table renderer resolve the same
21
- * destination as the loggers do. A caller that sets `CDXGEN_LOG_STREAM=stdout`
22
- * gets the pre-v13 behaviour for all of it, not just for the thought and trace
23
- * logs.
24
- *
25
- * @type {NodeJS.WritableStream}
26
- */
27
- export const diagnosticStream = logStream;
28
-
29
16
  // Custom lightweight colors formatting helper replacing yoctocolors
30
17
  const colors = {
31
18
  cyanBright: (text) => `\x1b[96m${text}\x1b[39m`,
@@ -38,81 +25,9 @@ export const THINK_MODE =
38
25
  ["true", "1"].includes(process.env.CDXGEN_THINK_MODE) ||
39
26
  process.env.CDXGEN_DEBUG_MODE === "verbose";
40
27
 
41
- // Log files are written synchronously rather than through
42
- // `fs.createWriteStream`. A write stream buffers, so anything still queued when
43
- // the process exits is lost, leaving readers that tail these files a truncated
44
- // log with no closing `</think>` marker. Writing through one shared descriptor
45
- // per path also keeps the two Console channels from interleaving at
46
- // independent file offsets.
47
- const syncFileWriters = new Map();
48
-
49
- /**
50
- * Open a synchronous writer for a log file path.
51
- *
52
- * `Console` requires a real stream, so this is a `Writable` whose `_write`
53
- * completes synchronously: bytes reach the descriptor as each record is
54
- * logged, and nothing is left queued for an event loop turn that a process
55
- * exit may never take.
56
- *
57
- * @param {string} filePath Destination path
58
- * @returns {Writable} Writer, or the diagnostic stream when the path is unwritable
59
- */
60
- function openSyncFileWriter(filePath) {
61
- const existing = syncFileWriters.get(filePath);
62
- if (existing) {
63
- return existing;
64
- }
65
- let fd;
66
- try {
67
- // Worker threads log to the same path through their own instance of this
68
- // module, so the descriptor is opened in append mode: O_APPEND lands every
69
- // write at the end of the file no matter how many writers share it, where
70
- // a truncating open gives each writer its own offset and they punch holes
71
- // through each other's records. The main thread truncates once so a run
72
- // still starts from an empty log.
73
- if (isMainThread) {
74
- try {
75
- fs.truncateSync(filePath, 0);
76
- } catch {
77
- // No file yet: the append open below creates it.
78
- }
79
- }
80
- fd = fs.openSync(filePath, "a");
81
- } catch {
82
- // An unwritable path must not take the whole run down: fall back to the
83
- // diagnostic stream so the records stay visible somewhere.
84
- return logStream;
85
- }
86
- const writer = new Writable({
87
- write(chunk, _encoding, callback) {
88
- if (fd !== undefined) {
89
- try {
90
- fs.writeSync(fd, chunk);
91
- } catch {
92
- // A full disk or a revoked descriptor must not abort BOM generation.
93
- }
94
- }
95
- callback();
96
- },
97
- });
98
- writer.closeSync = () => {
99
- if (fd === undefined) {
100
- return;
101
- }
102
- try {
103
- fs.closeSync(fd);
104
- } finally {
105
- fd = undefined;
106
- syncFileWriters.delete(filePath);
107
- }
108
- };
109
- syncFileWriters.set(filePath, writer);
110
- return writer;
111
- }
112
-
113
28
  const thinkOutput = process.env.CDXGEN_THOUGHT_LOG
114
29
  ? openSyncFileWriter(process.env.CDXGEN_THOUGHT_LOG)
115
- : logStream;
30
+ : diagnosticStream;
116
31
  const thinkErrorOutput = process.env.CDXGEN_THOUGHT_LOG
117
32
  ? thinkOutput
118
33
  : process.stderr;
@@ -131,7 +46,7 @@ export const TRACE_MODE =
131
46
 
132
47
  const traceOutput = process.env.CDXGEN_TRACE_LOG
133
48
  ? openSyncFileWriter(process.env.CDXGEN_TRACE_LOG)
134
- : logStream;
49
+ : diagnosticStream;
135
50
  const traceErrorOutput = process.env.CDXGEN_TRACE_LOG
136
51
  ? traceOutput
137
52
  : process.stderr;
@@ -186,7 +101,7 @@ export function thoughtEnd() {
186
101
  function colorizeText(s) {
187
102
  // Only colorize for an interactive terminal stream; piped/CI/file output
188
103
  // stays free of escape bytes so the diagnostic stream is grep-safe.
189
- if (process.env.CDXGEN_THOUGHT_LOG || !logStream.isTTY) {
104
+ if (process.env.CDXGEN_THOUGHT_LOG || !diagnosticStream.isTTY) {
190
105
  return s;
191
106
  }
192
107
  s = s.replace(/(\d+)/g, colors.cyanBright("$1"));
@@ -270,9 +185,7 @@ export function closeLogStreams() {
270
185
  }
271
186
  logStreamsClosed = true;
272
187
  thoughtEnd();
273
- for (const writer of [...syncFileWriters.values()]) {
274
- writer.closeSync();
275
- }
188
+ closeAllSyncFileWriters();
276
189
  }
277
190
 
278
191
  // The writers are synchronous, so this handler only has to emit the terminator
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared severity ordering for finding and confidence ladders.
3
+ *
4
+ * Lives in the core layer so modules below the audit layer can order labels
5
+ * such as `low`/`medium`/`high` without importing the audit package; the audit
6
+ * scoring module re-exports this constant under its historical name.
7
+ *
8
+ * @type {Readonly<Record<string, number>>}
9
+ */
10
+ export const SEVERITY_ORDER = Object.freeze({
11
+ none: -1,
12
+ low: 0,
13
+ medium: 1,
14
+ high: 2,
15
+ critical: 3,
16
+ });
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs";
2
+ import process from "node:process";
3
+ import { Writable } from "node:stream";
4
+ import { isMainThread } from "node:worker_threads";
5
+
6
+ // Stream contract: human-readable diagnostics default to stderr so stdout
7
+ // carries only the BOM payload. CDXGEN_LOG_STREAM=stdout restores the
8
+ // pre-v13 behaviour for callers that scraped cdxgen's stdout.
9
+
10
+ /**
11
+ * The stream every human-readable diagnostic is written to.
12
+ *
13
+ * Also the fallback destination of {@link openSyncFileWriter} when a log path
14
+ * cannot be opened, so an unwritable path keeps the records visible somewhere
15
+ * instead of taking the run down.
16
+ *
17
+ * @type {NodeJS.WritableStream}
18
+ */
19
+ export const diagnosticStream =
20
+ process.env.CDXGEN_LOG_STREAM === "stdout" ? process.stdout : process.stderr;
21
+
22
+ // Log files are written synchronously rather than through
23
+ // `fs.createWriteStream`. A write stream buffers, so anything still queued when
24
+ // the process exits is lost, leaving readers that tail these files a truncated
25
+ // log. Writing through one shared descriptor per path also keeps concurrent
26
+ // writers from interleaving at independent file offsets.
27
+ const syncFileWriters = new Map();
28
+
29
+ /**
30
+ * Open a synchronous writer for a log file path, or return the writer already
31
+ * open for that path.
32
+ *
33
+ * `Console` requires a real stream, so this is a `Writable` whose `_write`
34
+ * completes synchronously: bytes reach the descriptor as each record is
35
+ * written, and nothing is left queued for an event loop turn that a process
36
+ * exit may never take.
37
+ *
38
+ * @param {string} filePath Destination path
39
+ * @param {NodeJS.WritableStream|null} [fallbackStream] Stream returned when the path cannot be opened; defaults to the diagnostic stream. Pass `null` to get `undefined` instead, for callers that must never let data records spill into a diagnostic stream.
40
+ * @returns {Writable|NodeJS.WritableStream|undefined} Writer for the path, the fallback stream, or `undefined` when the path cannot be opened and no fallback was requested
41
+ */
42
+ export function openSyncFileWriter(
43
+ filePath,
44
+ fallbackStream = diagnosticStream,
45
+ ) {
46
+ const existing = syncFileWriters.get(filePath);
47
+ if (existing) {
48
+ return existing;
49
+ }
50
+ let fd;
51
+ try {
52
+ // Worker threads open the same path through their own instance of this
53
+ // module, so the descriptor is opened in append mode: O_APPEND lands every
54
+ // write at the end of the file no matter how many writers share it, where
55
+ // a truncating open gives each writer its own offset and they punch holes
56
+ // through each other's records. The main thread truncates once so a run
57
+ // still starts from an empty log.
58
+ if (isMainThread) {
59
+ try {
60
+ fs.truncateSync(filePath, 0);
61
+ } catch {
62
+ // No file yet: the append open below creates it.
63
+ }
64
+ }
65
+ fd = fs.openSync(filePath, "a");
66
+ } catch {
67
+ return fallbackStream ?? undefined;
68
+ }
69
+ const writer = new Writable({
70
+ write(chunk, _encoding, callback) {
71
+ if (fd !== undefined) {
72
+ try {
73
+ fs.writeSync(fd, chunk);
74
+ } catch {
75
+ // A full disk or a revoked descriptor must not abort BOM generation.
76
+ }
77
+ }
78
+ callback();
79
+ },
80
+ });
81
+ writer.closeSync = () => {
82
+ if (fd === undefined) {
83
+ return;
84
+ }
85
+ try {
86
+ fs.closeSync(fd);
87
+ } finally {
88
+ fd = undefined;
89
+ syncFileWriters.delete(filePath);
90
+ }
91
+ };
92
+ syncFileWriters.set(filePath, writer);
93
+ return writer;
94
+ }
95
+
96
+ /**
97
+ * Close every synchronous file writer opened by {@link openSyncFileWriter}.
98
+ * Safe to call more than once and after individual writers were closed.
99
+ *
100
+ * @returns {void}
101
+ */
102
+ export function closeAllSyncFileWriters() {
103
+ for (const writer of [...syncFileWriters.values()]) {
104
+ writer.closeSync();
105
+ }
106
+ }