@socketsecurity/cli-with-sentry 1.1.152 → 1.1.153

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 (61) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/cli.js +2348 -629
  3. package/dist/cli.js.map +1 -1
  4. package/dist/constants.js +4 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
  7. package/dist/manifest-scripts/socket-facts.init.gradle +14 -45
  8. package/dist/manifest-scripts/socket-facts.plugin.scala +15 -46
  9. package/dist/manifest-scripts/socket-workspaces.init.gradle +99 -0
  10. package/dist/manifest-scripts/socket-workspaces.plugin.scala +112 -0
  11. package/dist/tsconfig.dts.tsbuildinfo +1 -1
  12. package/dist/types/commands/manifest/cmd-manifest-dynamic-sbom-inference.d.mts +9 -0
  13. package/dist/types/commands/manifest/cmd-manifest-dynamic-sbom-inference.d.mts.map +1 -0
  14. package/dist/types/commands/manifest/cmd-manifest-gradle.d.mts.map +1 -1
  15. package/dist/types/commands/manifest/cmd-manifest-kotlin.d.mts.map +1 -1
  16. package/dist/types/commands/manifest/cmd-manifest-maven.d.mts.map +1 -1
  17. package/dist/types/commands/manifest/cmd-manifest-scala.d.mts.map +1 -1
  18. package/dist/types/commands/manifest/cmd-manifest-setup.d.mts.map +1 -1
  19. package/dist/types/commands/manifest/cmd-manifest.d.mts.map +1 -1
  20. package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +2 -1
  21. package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -1
  22. package/dist/types/commands/manifest/convert-maven-to-facts.d.mts +2 -1
  23. package/dist/types/commands/manifest/convert-maven-to-facts.d.mts.map +1 -1
  24. package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts +2 -1
  25. package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -1
  26. package/dist/types/commands/manifest/discover-manifest-roots.d.mts +18 -0
  27. package/dist/types/commands/manifest/discover-manifest-roots.d.mts.map +1 -0
  28. package/dist/types/commands/manifest/enumerate-workspaces.d.mts +19 -0
  29. package/dist/types/commands/manifest/enumerate-workspaces.d.mts.map +1 -0
  30. package/dist/types/commands/manifest/expand-env-var-refs.d.mts +10 -0
  31. package/dist/types/commands/manifest/expand-env-var-refs.d.mts.map +1 -0
  32. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts +37 -0
  33. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -0
  34. package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
  35. package/dist/types/commands/manifest/handle-manifest-dynamic-sbom-inference.d.mts +8 -0
  36. package/dist/types/commands/manifest/handle-manifest-dynamic-sbom-inference.d.mts.map +1 -0
  37. package/dist/types/commands/manifest/handle-manifest-setup.d.mts +1 -1
  38. package/dist/types/commands/manifest/handle-manifest-setup.d.mts.map +1 -1
  39. package/dist/types/commands/manifest/manifest-flags.d.mts +6 -0
  40. package/dist/types/commands/manifest/manifest-flags.d.mts.map +1 -0
  41. package/dist/types/commands/manifest/output-manifest-dynamic-sbom-inference.d.mts +4 -0
  42. package/dist/types/commands/manifest/output-manifest-dynamic-sbom-inference.d.mts.map +1 -0
  43. package/dist/types/commands/manifest/run-manifest-facts.d.mts +13 -2
  44. package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
  45. package/dist/types/commands/manifest/scripts/exclude-paths-glob.d.mts +8 -0
  46. package/dist/types/commands/manifest/scripts/exclude-paths-glob.d.mts.map +1 -0
  47. package/dist/types/commands/manifest/scripts/run.d.mts +10 -1
  48. package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -1
  49. package/dist/types/commands/manifest/setup-manifest-config.d.mts +19 -0
  50. package/dist/types/commands/manifest/setup-manifest-config.d.mts.map +1 -1
  51. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts +120 -0
  52. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts.map +1 -0
  53. package/dist/types/commands/scan/create-scan-from-github.d.mts +17 -0
  54. package/dist/types/commands/scan/create-scan-from-github.d.mts.map +1 -1
  55. package/dist/types/utils/github-errors.d.mts +51 -0
  56. package/dist/types/utils/github-errors.d.mts.map +1 -0
  57. package/dist/types/utils/socket-json.d.mts +38 -14
  58. package/dist/types/utils/socket-json.d.mts.map +1 -1
  59. package/dist/utils.js +307 -3
  60. package/dist/utils.js.map +1 -1
  61. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -333,9 +333,9 @@ const hidden$x = false;
333
333
  const cmdAnalytics = {
334
334
  description: description$F,
335
335
  hidden: hidden$x,
336
- run: run$U
336
+ run: run$V
337
337
  };
338
- async function run$U(argv, importMeta, {
338
+ async function run$V(argv, importMeta, {
339
339
  parentName
340
340
  }) {
341
341
  const config = {
@@ -756,9 +756,9 @@ const hidden$w = false;
756
756
  const cmdAuditLog = {
757
757
  description: description$E,
758
758
  hidden: hidden$w,
759
- run: run$T
759
+ run: run$U
760
760
  };
761
- async function run$T(argv, importMeta, {
761
+ async function run$U(argv, importMeta, {
762
762
  parentName
763
763
  }) {
764
764
  const config = {
@@ -4342,6 +4342,41 @@ async function extractBazelToMaven(opts) {
4342
4342
  }
4343
4343
  }
4344
4344
 
4345
+ const ENV_VAR_REF = /\$\$|\$\{(\w+)\}|\$(\w+)/g;
4346
+
4347
+ // Expands `$VAR`/`${VAR}` references (e.g. a team-shared `javaHome:
4348
+ // "$JAVA11_HOME"`) against the CLI process's own environment, so a socket.json
4349
+ // value works across machines instead of hardcoding one developer's path.
4350
+ // `$$` is a literal `$`, for a value that must contain a literal `$WORD`.
4351
+ function expandEnvVarRefs(value) {
4352
+ const missing = [];
4353
+ const expanded = value.replace(ENV_VAR_REF, (match, braced, bare) => {
4354
+ if (match === '$$') {
4355
+ return '$';
4356
+ }
4357
+ const name = braced ?? bare;
4358
+ const resolved = process.env[name];
4359
+ if (resolved === undefined) {
4360
+ if (!missing.includes(name)) {
4361
+ missing.push(name);
4362
+ }
4363
+ return '';
4364
+ }
4365
+ return resolved;
4366
+ });
4367
+ return missing.length ? {
4368
+ missing,
4369
+ value: expanded
4370
+ } : {
4371
+ value: expanded
4372
+ };
4373
+ }
4374
+ function formatMissingEnvVarRefs(missing) {
4375
+ const names = missing.map(name => `\`${name}\``).join(', ');
4376
+ const verb = missing.length > 1 ? 'are' : 'is';
4377
+ return `references ${names}, which ${verb} not set in this environment`;
4378
+ }
4379
+
4345
4380
  // Gradle's variant-aware resolver: distinct exceptions give mutually-exclusive
4346
4381
  // phrasing, so most-specific-first substring checks classify reliably.
4347
4382
  function classifyGradleFailure(detail) {
@@ -5097,6 +5132,141 @@ function resolveBuildToolBin(tool, projectDir, bin) {
5097
5132
  return DEFAULT_BUILD_TOOL_BIN[tool];
5098
5133
  }
5099
5134
 
5135
+ // Single source of truth for --exclude-paths glob semantics against a JVM
5136
+ // build root's subproject dirs: anchored, relative, case-sensitive; `*`
5137
+ // within one path segment, `**` across zero or more segments, `?` within one
5138
+ // segment, and `[...]` character classes with `[!..]`/`[^..]` negation.
5139
+ // Entries are assumed pre-validated by assertValidExcludePaths (relative, no
5140
+ // negation, no `..`, balanced brackets, comma-free). A malformed entry falls
5141
+ // back to a literal match, never throws.
5142
+ //
5143
+ // Compiled HERE, once, and handed to every producer (the gradle/sbt/maven
5144
+ // facts and workspaces scripts) pre-compiled, so there is exactly one
5145
+ // implementation and one test suite instead of the same algorithm
5146
+ // re-implemented per language.
5147
+ //
5148
+ // Portability contract: gradle/sbt/maven all run on the JVM and share the
5149
+ // exact same regex engine (java.util.regex), so the same emitted subset
5150
+ // (`.*`, `[^/]*`, `[^/]`, `[...]`, `[^...]`, backslash-escaped
5151
+ // metacharacters) is authoritative for every producer - this compiler and JS
5152
+ // RegExp agree on the identical subset. Class bodies escape `&` because Java
5153
+ // classes support `&&` intersection (JS treats it literally). Each compiled
5154
+ // pattern already means "this path OR its whole subtree", matching the
5155
+ // documented --exclude-paths contract, so a producer never needs its own
5156
+ // "self + /**" expansion. Patterns transport comma-joined: an input glob can
5157
+ // never contain a comma, since --exclude-paths is comma-split before glob
5158
+ // parsing reaches this module.
5159
+
5160
+ function literalSource(glob) {
5161
+ const escaped = glob.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5162
+ return `^(?:${escaped})(?:/.*)?$`;
5163
+ }
5164
+
5165
+ // Strips a trailing `/**` (and trailing slashes) so `dir` and `dir/**` both
5166
+ // compile to the same base pattern - the "self OR subtree" wrap below is
5167
+ // what actually encodes matching the subtree, not the user's own suffix.
5168
+ function stripTrailingGlobstar(glob) {
5169
+ let g = glob;
5170
+ while (g.endsWith('/')) {
5171
+ g = g.slice(0, -1);
5172
+ }
5173
+ while (g.endsWith('/**')) {
5174
+ g = g.slice(0, -3);
5175
+ while (g.endsWith('/')) {
5176
+ g = g.slice(0, -1);
5177
+ }
5178
+ }
5179
+ return g;
5180
+ }
5181
+
5182
+ // Translates one path segment's `*`/`?`/`[...]` - never crosses a `/`.
5183
+ function translateSegment(segment) {
5184
+ let sb = '';
5185
+ let i = 0;
5186
+ const n = segment.length;
5187
+ while (i < n) {
5188
+ const ch = segment.charAt(i);
5189
+ if (ch === '*') {
5190
+ sb += '[^/]*';
5191
+ i += 1;
5192
+ } else if (ch === '?') {
5193
+ sb += '[^/]';
5194
+ i += 1;
5195
+ } else if (ch === '[') {
5196
+ const j = segment.indexOf(']', i + 1);
5197
+ // Treat as a class only with a non-empty body; else a literal `[`.
5198
+ if (j <= i + 1) {
5199
+ sb += '\\[';
5200
+ i += 1;
5201
+ } else {
5202
+ let body = segment.slice(i + 1, j);
5203
+ const neg = body.startsWith('!') || body.startsWith('^');
5204
+ if (neg) {
5205
+ body = body.slice(1);
5206
+ }
5207
+ if (!body) {
5208
+ // `[!]`/`[^]` would emit `[^]`, which JS accepts but Java rejects;
5209
+ // signal the caller to fall back to a whole-glob literal match.
5210
+ throw new Error('empty-class');
5211
+ }
5212
+ // Only literal chars and `-` ranges are meaningful; neutralize
5213
+ // regex-class tricks (`&` guards Java's `&&` class intersection).
5214
+ body = body.replace(/\\/g, '\\\\').replace(/\[/g, '\\[').replace(/\]/g, '\\]').replace(/&/g, '\\&');
5215
+ sb += `[${neg ? '^' : ''}${body}]`;
5216
+ i = j + 1;
5217
+ }
5218
+ } else if ('.\\^$|+(){}]'.includes(ch)) {
5219
+ sb += `\\${ch}`;
5220
+ i += 1;
5221
+ } else {
5222
+ sb += ch;
5223
+ i += 1;
5224
+ }
5225
+ }
5226
+ return sb;
5227
+ }
5228
+ function excludePathGlobToRegexSource(glob) {
5229
+ const stripped = stripTrailingGlobstar(glob);
5230
+ if (!stripped) {
5231
+ // Defensive only: assertValidExcludePaths already rejects a pattern that
5232
+ // reduces to "match everything".
5233
+ return literalSource(glob);
5234
+ }
5235
+ try {
5236
+ const segments = stripped.split('/');
5237
+ let base = '';
5238
+ segments.forEach((segment, i) => {
5239
+ const isLast = i === segments.length - 1;
5240
+ if (segment === '**') {
5241
+ // Zero or more whole segments: mid-pattern, each consumed segment
5242
+ // carries its own trailing slash; as the last segment, anything
5243
+ // (including nothing) for the rest of the path.
5244
+ base += isLast ? '.*' : '(?:[^/]+/)*';
5245
+ } else {
5246
+ base += translateSegment(segment);
5247
+ if (!isLast) {
5248
+ base += '/';
5249
+ }
5250
+ }
5251
+ });
5252
+ const source = `^(?:${base})(?:/.*)?$`;
5253
+ // eslint-disable-next-line no-new
5254
+ new RegExp(source);
5255
+ return source;
5256
+ } catch {
5257
+ return literalSource(glob);
5258
+ }
5259
+ }
5260
+ function compileExcludePathPatterns(paths) {
5261
+ return (paths ?? []).map(excludePathGlobToRegexSource);
5262
+ }
5263
+
5264
+ // Transport form handed to the build-tool scripts: comma-joined pattern
5265
+ // sources. Empty string when there are no patterns.
5266
+ function serializeExcludePathPatterns(paths) {
5267
+ return compileExcludePathPatterns(paths).join(',');
5268
+ }
5269
+
5100
5270
  // Line-protocol the build-tool scripts emit to a records file (NOT stdout — sbt
5101
5271
  // prints unsilenceable resolution noise there). One record per line, fields
5102
5272
  // backslash-escaped (\\, \t, \n, \r) so a value can't break framing:
@@ -5283,8 +5453,11 @@ function parseRecords(text) {
5283
5453
  }
5284
5454
 
5285
5455
  const FACTS_TASK = 'socketFacts';
5286
- const SBT_PLUGIN_FILENAME = 'SocketFactsPlugin.scala';
5287
-
5456
+ const WORKSPACES_TASK = 'socketWorkspaces';
5457
+ // Destination filename inside sbt's isolated plugins/ dir; unrelated to the source filename, which
5458
+ // is whatever socket-facts.plugin.scala / socket-workspaces.plugin.scala are called on disk.
5459
+ const SBT_FACTS_PLUGIN_FILENAME = 'SocketFactsPlugin.scala';
5460
+ const SBT_WORKSPACES_PLUGIN_FILENAME = 'SocketWorkspacesPlugin.scala';
5288
5461
  // Bundled emitter assets, copied into dist by the rollup build.
5289
5462
  function manifestScriptsPath(...parts) {
5290
5463
  return path.join(constants.default.distPath, 'manifest-scripts', ...parts);
@@ -5329,13 +5502,13 @@ async function runNeverThrow(bin, args, opts) {
5329
5502
  throw e;
5330
5503
  }
5331
5504
  }
5332
- async function writeSbtPlugin(globalBase) {
5333
- const src = await fs$1.promises.readFile(manifestScriptsPath('socket-facts.plugin.scala'), 'utf8');
5505
+ async function writeSbtPlugin(globalBase, sourceFilename, destFilename) {
5506
+ const src = await fs$1.promises.readFile(manifestScriptsPath(sourceFilename), 'utf8');
5334
5507
  const pluginsDir = path.join(globalBase, 'plugins');
5335
5508
  await fs$1.promises.mkdir(pluginsDir, {
5336
5509
  recursive: true
5337
5510
  });
5338
- await fs$1.promises.writeFile(path.join(pluginsDir, SBT_PLUGIN_FILENAME), src);
5511
+ await fs$1.promises.writeFile(path.join(pluginsDir, destFilename), src);
5339
5512
  }
5340
5513
  async function assembleFromRecords(out, recordsFile) {
5341
5514
  const text = fs$1.existsSync(recordsFile) ? await fs$1.promises.readFile(recordsFile, 'utf8') : '';
@@ -5375,6 +5548,18 @@ async function runManifestScript(tool, opts) {
5375
5548
  return await runMaven(opts);
5376
5549
  }
5377
5550
  }
5551
+
5552
+ // Emits only `project` records, never resolving dependencies, for discovery
5553
+ // before committing to a full `runManifestScript` resolution cost.
5554
+ async function enumerateWorkspaces$1(tool, opts) {
5555
+ const result = await (tool === 'gradle' ? enumerateGradleWorkspaces(opts) : tool === 'sbt' ? enumerateSbtWorkspaces(opts) : enumerateMavenWorkspaces(opts));
5556
+ return {
5557
+ code: result.code,
5558
+ projects: result.facts.projects ?? [],
5559
+ stderr: result.stderr,
5560
+ stdout: result.stdout
5561
+ };
5562
+ }
5378
5563
  function commonProps(opts, prefix) {
5379
5564
  const props = [];
5380
5565
  if (opts.withFiles) {
@@ -5389,27 +5574,35 @@ function commonProps(opts, prefix) {
5389
5574
  if (opts.excludeConfigs) {
5390
5575
  props.push(`${prefix}socket.excludeConfigs=${opts.excludeConfigs}`);
5391
5576
  }
5392
- if (opts.excludePaths?.length) {
5393
- // CSV: `--exclude-paths` is comma-split at the CLI, so an entry can never
5394
- // contain a comma.
5395
- props.push(`${prefix}socket.excludePaths=${opts.excludePaths.join(',')}`);
5577
+ // Globs compile to regex pattern sources HERE (exclude-paths-glob.mts is
5578
+ // the single glob implementation); the scripts just Pattern.compile what
5579
+ // they receive.
5580
+ const excludePatterns = serializeExcludePathPatterns(opts.excludePaths);
5581
+ if (excludePatterns) {
5582
+ props.push(`${prefix}socket.excludePaths=${excludePatterns}`);
5396
5583
  }
5397
5584
  return props;
5398
5585
  }
5399
- async function runGradle(opts) {
5400
- const initScript = manifestScriptsPath('socket-facts.init.gradle');
5401
- return await utils.withTmpDir('socket-gradle-facts-', async tmp => {
5586
+ async function invokeGradle(opts, initScriptFilename, task, tmpDirPrefix) {
5587
+ const initScript = manifestScriptsPath(initScriptFilename);
5588
+ return await utils.withTmpDir(tmpDirPrefix, async tmp => {
5402
5589
  const recordsFile = path.join(tmp, 'records.tsv');
5403
5590
  const bin = resolveBuildToolBin('gradle', opts.projectDir, opts.bin);
5404
5591
  // Disable the configuration cache: the init script's legacy
5405
5592
  // resolvedConfiguration API and shared accumulator aren't cache-safe.
5406
- const args = ['--init-script', initScript, '-Dorg.gradle.configuration-cache=false', `-Psocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-P'), ...(opts.toolOpts ?? []), FACTS_TASK, '--no-daemon', '--console=plain'];
5593
+ const args = ['--init-script', initScript, '-Dorg.gradle.configuration-cache=false', `-Psocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-P'), ...(opts.toolOpts ?? []), task, '--no-daemon', '--console=plain'];
5407
5594
  const out = await runNeverThrow(bin, args, opts);
5408
5595
  return await assembleFromRecords(out, recordsFile);
5409
5596
  });
5410
5597
  }
5411
- async function runSbtIn(globalBase, opts) {
5412
- await writeSbtPlugin(globalBase);
5598
+ async function runGradle(opts) {
5599
+ return await invokeGradle(opts, 'socket-facts.init.gradle', FACTS_TASK, 'socket-gradle-facts-');
5600
+ }
5601
+ async function enumerateGradleWorkspaces(opts) {
5602
+ return await invokeGradle(opts, 'socket-workspaces.init.gradle', WORKSPACES_TASK, 'socket-gradle-workspaces-');
5603
+ }
5604
+ async function invokeSbtIn(globalBase, opts, pluginSourceFilename, pluginDestFilename, task) {
5605
+ await writeSbtPlugin(globalBase, pluginSourceFilename, pluginDestFilename);
5413
5606
  const recordsFile = path.join(globalBase, 'records.tsv');
5414
5607
  const bin = resolveBuildToolBin('sbt', opts.projectDir, opts.bin);
5415
5608
  // Fresh per-run global base (not ~/.sbt): sbt executes everything under
@@ -5423,30 +5616,44 @@ async function runSbtIn(globalBase, opts) {
5423
5616
  // caller-supplied --java-home.
5424
5617
  const javaHome = opts.env?.['JAVA_HOME'] ?? process.env['JAVA_HOME'];
5425
5618
  const javaHomeOpt = javaHome && !(opts.toolOpts ?? []).includes('--java-home') ? ['--java-home', javaHome] : [];
5426
- const args = [...javaHomeOpt, ...props, ...(opts.toolOpts ?? []), '--batch', FACTS_TASK];
5619
+ const args = [...javaHomeOpt, ...props, ...(opts.toolOpts ?? []), '--batch', task];
5427
5620
  const out = await runNeverThrow(bin, args, opts);
5428
5621
  return await assembleFromRecords(out, recordsFile);
5429
5622
  }
5430
5623
  async function runSbt(opts) {
5624
+ const run = globalBase => invokeSbtIn(globalBase, opts, 'socket-facts.plugin.scala', SBT_FACTS_PLUGIN_FILENAME, FACTS_TASK);
5431
5625
  if (opts.tmpDir) {
5432
- return await runSbtIn(opts.tmpDir, opts);
5626
+ return await run(opts.tmpDir);
5433
5627
  }
5434
- return await utils.withTmpDir('socket-sbt-facts-', globalBase => runSbtIn(globalBase, opts));
5628
+ return await utils.withTmpDir('socket-sbt-facts-', run);
5435
5629
  }
5436
- async function runMaven(opts) {
5630
+ async function enumerateSbtWorkspaces(opts) {
5631
+ const run = globalBase => invokeSbtIn(globalBase, opts, 'socket-workspaces.plugin.scala', SBT_WORKSPACES_PLUGIN_FILENAME, WORKSPACES_TASK);
5632
+ if (opts.tmpDir) {
5633
+ return await run(opts.tmpDir);
5634
+ }
5635
+ return await utils.withTmpDir('socket-sbt-workspaces-', run);
5636
+ }
5637
+ async function invokeMaven(opts, coanaTask, tmpDirPrefix) {
5437
5638
  const jarPath = manifestScriptsPath('maven-extension', 'coana-maven-extension.jar');
5438
5639
  assertMavenExtensionBuilt(jarPath);
5439
- return await utils.withTmpDir('socket-maven-facts-', async tmp => {
5640
+ return await utils.withTmpDir(tmpDirPrefix, async tmp => {
5440
5641
  const recordsFile = path.join(tmp, 'records.tsv');
5441
5642
  const bin = resolveBuildToolBin('maven', opts.projectDir, opts.bin);
5442
- // `validate` is the cheapest phase that triggers the afterSessionEnd
5443
- // extension; no compile needed (analysis uses configured paths, not classes).
5444
- const props = [`-Dmaven.ext.class.path=${jarPath}`, '-Dcoana.task=socket-facts', `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
5643
+ // `validate` is the cheapest phase that triggers the extension; no compile
5644
+ // needed (analysis uses configured paths, not classes).
5645
+ const props = [`-Dmaven.ext.class.path=${jarPath}`, `-Dcoana.task=${coanaTask}`, `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
5445
5646
  const args = [...props, ...(opts.toolOpts ?? []), '--batch-mode', 'validate'];
5446
5647
  const out = await runNeverThrow(bin, args, opts);
5447
5648
  return await assembleFromRecords(out, recordsFile);
5448
5649
  });
5449
5650
  }
5651
+ async function runMaven(opts) {
5652
+ return await invokeMaven(opts, 'socket-facts', 'socket-maven-facts-');
5653
+ }
5654
+ async function enumerateMavenWorkspaces(opts) {
5655
+ return await invokeMaven(opts, 'socket-workspaces', 'socket-maven-workspaces-');
5656
+ }
5450
5657
 
5451
5658
  // Frozen contract with `coana run --compute-artifacts-sidecar`; change only in
5452
5659
  // sync with the coana consumer. Per coordinate: targets/sources present →
@@ -5520,6 +5727,11 @@ function tailBuildOutput(stdout, stderr) {
5520
5727
  return combined.split('\n').slice(-40).join('\n');
5521
5728
  }
5522
5729
 
5730
+ // `null` = a real failure (crash, missing config, blocking unresolved
5731
+ // dependency); `undefined` = genuinely nothing to resolve, not a failure.
5732
+ // Distinguishing the two here means callers never have to infer it from
5733
+ // `process.exitCode`, which can already be non-zero for an unrelated reason.
5734
+
5523
5735
  // Runs the bundled build-tool resolution script for a JVM project and writes
5524
5736
  // `.socket.facts.json`. `withFiles` (reachability only) additionally folds
5525
5737
  // resolved artifact paths into `sidecarAcc`. A blocking resolution failure sets
@@ -5535,17 +5747,33 @@ async function runManifestFacts({
5535
5747
  excludePaths,
5536
5748
  ignoreUnresolved,
5537
5749
  includeConfigs,
5750
+ javaHome,
5538
5751
  sidecarAcc,
5539
5752
  tmpDir,
5540
5753
  verbose,
5541
5754
  withFiles
5542
5755
  }) {
5543
5756
  const factsPath = path.join(cwd, constants.default.DOT_SOCKET_DOT_FACTS_JSON);
5544
- logger.logger.log(`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`);
5757
+ let resolvedJavaHome;
5758
+ if (javaHome) {
5759
+ const expanded = expandEnvVarRefs(javaHome);
5760
+ if (expanded.missing) {
5761
+ process.exitCode = 1;
5762
+ logger.logger.fail(`javaHome (\`${javaHome}\`) ${formatMissingEnvVarRefs(expanded.missing)}.`);
5763
+ return null;
5764
+ }
5765
+ resolvedJavaHome = expanded.value;
5766
+ }
5767
+ logger.logger.info(`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`);
5545
5768
  const scriptOpts = {
5546
5769
  bin: bin || undefined,
5547
5770
  excludeConfigs: excludeConfigs || undefined,
5548
5771
  excludePaths: excludePaths?.length ? excludePaths : undefined,
5772
+ // `env` replaces the spawned process's whole environment, not just JAVA_HOME.
5773
+ env: resolvedJavaHome ? {
5774
+ ...process.env,
5775
+ JAVA_HOME: resolvedJavaHome
5776
+ } : undefined,
5549
5777
  includeConfigs: includeConfigs || undefined,
5550
5778
  projectDir: cwd,
5551
5779
  // Stream the build tool's output only when asked; otherwise capture it and
@@ -5580,8 +5808,8 @@ async function runManifestFacts({
5580
5808
  spinner.failAndStop(`Failed to run ${ecosystem}.`);
5581
5809
  }
5582
5810
  process.exitCode = 1;
5583
- logger.logger.fail(`Could not run the ${ecosystem} build tool` + (verbose ? `: ${e}` : ' (run with --verbose for details).'));
5584
- return;
5811
+ logger.logger.fail(`Could not run the ${ecosystem} build tool` + (verbose ? `: ${utils.getErrorMessageOr(e, String(e))}` : ' (run with --verbose for details).'));
5812
+ return null;
5585
5813
  }
5586
5814
  const {
5587
5815
  artifactPaths,
@@ -5602,16 +5830,16 @@ async function runManifestFacts({
5602
5830
  process.exitCode = 1;
5603
5831
  logger.logger.fail(rendered.summary);
5604
5832
  if (verbose && rendered.details) {
5605
- logger.logger.log(rendered.details);
5833
+ logger.logger.info(rendered.details);
5606
5834
  }
5607
- return;
5835
+ return null;
5608
5836
  }
5609
5837
  }
5610
5838
  if (rendered.nonBlockingNotice) {
5611
5839
  logger.logger.info(rendered.nonBlockingNotice);
5612
5840
  }
5613
5841
  if (verbose && rendered.details) {
5614
- logger.logger.log(rendered.details);
5842
+ logger.logger.info(rendered.details);
5615
5843
  }
5616
5844
 
5617
5845
  // A non-zero build exit with no usable output (no graph, no first-party
@@ -5635,7 +5863,7 @@ async function runManifestFacts({
5635
5863
  // successful run couldn't resolve.
5636
5864
  process.exitCode = 1;
5637
5865
  logger.logger.fail(`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`);
5638
- return;
5866
+ return null;
5639
5867
  }
5640
5868
 
5641
5869
  // Nothing resolved at all — no dependencies and no first-party modules. A
@@ -5650,6 +5878,10 @@ async function runManifestFacts({
5650
5878
  accumulateSidecar(sidecarAcc, facts, artifactPaths);
5651
5879
  }
5652
5880
  logger.logger.success('Generated Socket facts');
5881
+ return {
5882
+ factsPath,
5883
+ projects: facts.projects ?? []
5884
+ };
5653
5885
  }
5654
5886
 
5655
5887
  // Generates `.socket.facts.json` for a Gradle project via the bundled init script.
@@ -5661,6 +5893,7 @@ async function convertGradleToFacts({
5661
5893
  gradleOpts,
5662
5894
  ignoreUnresolved,
5663
5895
  includeConfigs,
5896
+ javaHome,
5664
5897
  sidecarAcc,
5665
5898
  verbose,
5666
5899
  withFiles
@@ -5674,6 +5907,7 @@ async function convertGradleToFacts({
5674
5907
  excludePaths,
5675
5908
  ignoreUnresolved,
5676
5909
  includeConfigs,
5910
+ javaHome,
5677
5911
  sidecarAcc,
5678
5912
  verbose,
5679
5913
  withFiles
@@ -5688,6 +5922,7 @@ async function convertMavenToFacts({
5688
5922
  excludePaths,
5689
5923
  ignoreUnresolved,
5690
5924
  includeConfigs,
5925
+ javaHome,
5691
5926
  mavenOpts,
5692
5927
  sidecarAcc,
5693
5928
  verbose,
@@ -5702,6 +5937,7 @@ async function convertMavenToFacts({
5702
5937
  excludePaths,
5703
5938
  ignoreUnresolved,
5704
5939
  includeConfigs,
5940
+ javaHome,
5705
5941
  sidecarAcc,
5706
5942
  verbose,
5707
5943
  withFiles
@@ -5718,6 +5954,7 @@ async function convertSbtToFacts({
5718
5954
  excludePaths,
5719
5955
  ignoreUnresolved,
5720
5956
  includeConfigs,
5957
+ javaHome,
5721
5958
  sbtOpts,
5722
5959
  sidecarAcc,
5723
5960
  tmpDir,
@@ -5733,6 +5970,7 @@ async function convertSbtToFacts({
5733
5970
  excludePaths,
5734
5971
  ignoreUnresolved,
5735
5972
  includeConfigs,
5973
+ javaHome,
5736
5974
  sidecarAcc,
5737
5975
  tmpDir,
5738
5976
  verbose,
@@ -6279,7 +6517,7 @@ async function generateAutoManifest({
6279
6517
  // Note: `sbt` is more likely to be resolved against PATH env.
6280
6518
  bin: sockJson.defaults?.manifest?.sbt?.bin ?? 'sbt',
6281
6519
  cwd,
6282
- sbtOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.sbt?.sbtOpts),
6520
+ sbtOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.sbt?.sbtOpts ?? undefined),
6283
6521
  verbose: Boolean(sockJson.defaults?.manifest?.sbt?.verbose)
6284
6522
  };
6285
6523
  // Socket facts is the default; opt into pom generation with
@@ -6293,6 +6531,7 @@ async function generateAutoManifest({
6293
6531
  excludePaths,
6294
6532
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved),
6295
6533
  includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
6534
+ javaHome: sockJson.defaults?.manifest?.sbt?.javaHome ?? undefined,
6296
6535
  sidecarAcc,
6297
6536
  tmpDir,
6298
6537
  withFiles: computeArtifactsSidecar
@@ -6314,7 +6553,7 @@ async function generateAutoManifest({
6314
6553
  bin: sockJson.defaults?.manifest?.gradle?.bin ? path.resolve(cwd, sockJson.defaults.manifest.gradle.bin) : resolveBuildToolBin('gradle', cwd),
6315
6554
  cwd,
6316
6555
  verbose: Boolean(sockJson.defaults?.manifest?.gradle?.verbose),
6317
- gradleOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.gradle?.gradleOpts)
6556
+ gradleOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.gradle?.gradleOpts ?? undefined)
6318
6557
  };
6319
6558
  // Socket facts is the default; opt into pom generation with
6320
6559
  // `defaults.manifest.gradle.facts: false` in socket.json.
@@ -6327,6 +6566,7 @@ async function generateAutoManifest({
6327
6566
  excludePaths,
6328
6567
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved),
6329
6568
  includeConfigs: sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
6569
+ javaHome: sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined,
6330
6570
  sidecarAcc,
6331
6571
  withFiles: computeArtifactsSidecar
6332
6572
  });
@@ -6349,7 +6589,8 @@ async function generateAutoManifest({
6349
6589
  excludePaths,
6350
6590
  ignoreUnresolved: Boolean(sockJson.defaults?.manifest?.maven?.ignoreUnresolved),
6351
6591
  includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '',
6352
- mavenOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.maven?.mavenOpts),
6592
+ javaHome: sockJson.defaults?.manifest?.maven?.javaHome ?? undefined,
6593
+ mavenOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.maven?.mavenOpts ?? undefined),
6353
6594
  sidecarAcc,
6354
6595
  verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
6355
6596
  withFiles: computeArtifactsSidecar
@@ -6780,7 +7021,7 @@ async function handleCi(autoManifest) {
6780
7021
  });
6781
7022
  }
6782
7023
 
6783
- const config$m = {
7024
+ const config$n = {
6784
7025
  commandName: 'ci',
6785
7026
  description: 'Alias for `socket scan create --report` (creates report and exits with error if unhealthy)',
6786
7027
  hidden: false,
@@ -6798,7 +7039,7 @@ const config$m = {
6798
7039
  $ ${command} [options]
6799
7040
 
6800
7041
  Options
6801
- ${utils.getFlagListOutput(config$m.flags)}
7042
+ ${utils.getFlagListOutput(config$n.flags)}
6802
7043
 
6803
7044
  This command is intended to use in CI runs to allow automated systems to
6804
7045
  accept or reject a current build. It will use the default org of the
@@ -6816,16 +7057,16 @@ const config$m = {
6816
7057
  `
6817
7058
  };
6818
7059
  const cmdCI = {
6819
- description: config$m.description,
6820
- hidden: config$m.hidden,
6821
- run: run$S
7060
+ description: config$n.description,
7061
+ hidden: config$n.hidden,
7062
+ run: run$T
6822
7063
  };
6823
- async function run$S(argv, importMeta, {
7064
+ async function run$T(argv, importMeta, {
6824
7065
  parentName
6825
7066
  }) {
6826
7067
  const cli = utils.meowOrExit({
6827
7068
  argv,
6828
- config: config$m,
7069
+ config: config$n,
6829
7070
  parentName,
6830
7071
  importMeta
6831
7072
  });
@@ -7068,9 +7309,9 @@ const hidden$v = false;
7068
7309
  const cmdConfigAuto = {
7069
7310
  description: description$D,
7070
7311
  hidden: hidden$v,
7071
- run: run$R
7312
+ run: run$S
7072
7313
  };
7073
- async function run$R(argv, importMeta, {
7314
+ async function run$S(argv, importMeta, {
7074
7315
  parentName
7075
7316
  }) {
7076
7317
  const config = {
@@ -7186,7 +7427,7 @@ async function handleConfigGet({
7186
7427
  await outputConfigGet(key, result, outputKind);
7187
7428
  }
7188
7429
 
7189
- const config$l = {
7430
+ const config$m = {
7190
7431
  commandName: 'get',
7191
7432
  description: 'Get the value of a local CLI config item',
7192
7433
  hidden: false,
@@ -7216,16 +7457,16 @@ ${utils.getSupportedConfigEntries().map(({
7216
7457
  `
7217
7458
  };
7218
7459
  const cmdConfigGet = {
7219
- description: config$l.description,
7220
- hidden: config$l.hidden,
7221
- run: run$Q
7460
+ description: config$m.description,
7461
+ hidden: config$m.hidden,
7462
+ run: run$R
7222
7463
  };
7223
- async function run$Q(argv, importMeta, {
7464
+ async function run$R(argv, importMeta, {
7224
7465
  parentName
7225
7466
  }) {
7226
7467
  const cli = utils.meowOrExit({
7227
7468
  argv,
7228
- config: config$l,
7469
+ config: config$m,
7229
7470
  importMeta,
7230
7471
  parentName
7231
7472
  });
@@ -7327,7 +7568,7 @@ async function outputConfigList({
7327
7568
  }
7328
7569
  }
7329
7570
 
7330
- const config$k = {
7571
+ const config$l = {
7331
7572
  commandName: 'list',
7332
7573
  description: 'Show all local CLI config items and their values',
7333
7574
  hidden: false,
@@ -7352,16 +7593,16 @@ const config$k = {
7352
7593
  `
7353
7594
  };
7354
7595
  const cmdConfigList = {
7355
- description: config$k.description,
7356
- hidden: config$k.hidden,
7357
- run: run$P
7596
+ description: config$l.description,
7597
+ hidden: config$l.hidden,
7598
+ run: run$Q
7358
7599
  };
7359
- async function run$P(argv, importMeta, {
7600
+ async function run$Q(argv, importMeta, {
7360
7601
  parentName
7361
7602
  }) {
7362
7603
  const cli = utils.meowOrExit({
7363
7604
  argv,
7364
- config: config$k,
7605
+ config: config$l,
7365
7606
  importMeta,
7366
7607
  parentName
7367
7608
  });
@@ -7451,9 +7692,9 @@ const hidden$u = false;
7451
7692
  const cmdConfigSet = {
7452
7693
  description: description$C,
7453
7694
  hidden: hidden$u,
7454
- run: run$O
7695
+ run: run$P
7455
7696
  };
7456
- async function run$O(argv, importMeta, {
7697
+ async function run$P(argv, importMeta, {
7457
7698
  parentName
7458
7699
  }) {
7459
7700
  const config = {
@@ -7578,9 +7819,9 @@ const hidden$t = false;
7578
7819
  const cmdConfigUnset = {
7579
7820
  description: description$B,
7580
7821
  hidden: hidden$t,
7581
- run: run$N
7822
+ run: run$O
7582
7823
  };
7583
- async function run$N(argv, importMeta, {
7824
+ async function run$O(argv, importMeta, {
7584
7825
  parentName
7585
7826
  }) {
7586
7827
  const config = {
@@ -8880,7 +9121,7 @@ const hidden$s = false;
8880
9121
  const cmdFix = {
8881
9122
  description: description$z,
8882
9123
  hidden: hidden$s,
8883
- run: run$M
9124
+ run: run$N
8884
9125
  };
8885
9126
  const generalFlags$2 = {
8886
9127
  autopilot: {
@@ -9055,7 +9296,7 @@ const hiddenFlags = {
9055
9296
  hidden: true
9056
9297
  }
9057
9298
  };
9058
- async function run$M(argv, importMeta, {
9299
+ async function run$N(argv, importMeta, {
9059
9300
  parentName
9060
9301
  }) {
9061
9302
  const config = {
@@ -9384,7 +9625,7 @@ async function handleInstallCompletion(targetName) {
9384
9625
  await outputInstallCompletion(result);
9385
9626
  }
9386
9627
 
9387
- const config$j = {
9628
+ const config$k = {
9388
9629
  commandName: 'completion',
9389
9630
  description: 'Install bash completion for Socket CLI',
9390
9631
  hidden: false,
@@ -9421,16 +9662,16 @@ const config$j = {
9421
9662
  `
9422
9663
  };
9423
9664
  const cmdInstallCompletion = {
9424
- description: config$j.description,
9425
- hidden: config$j.hidden,
9426
- run: run$L
9665
+ description: config$k.description,
9666
+ hidden: config$k.hidden,
9667
+ run: run$M
9427
9668
  };
9428
- async function run$L(argv, importMeta, {
9669
+ async function run$M(argv, importMeta, {
9429
9670
  parentName
9430
9671
  }) {
9431
9672
  const cli = utils.meowOrExit({
9432
9673
  argv,
9433
- config: config$j,
9674
+ config: config$k,
9434
9675
  parentName,
9435
9676
  importMeta
9436
9677
  });
@@ -9487,7 +9728,7 @@ async function handleCmdJson(cwd) {
9487
9728
  await outputCmdJson(cwd);
9488
9729
  }
9489
9730
 
9490
- const config$i = {
9731
+ const config$j = {
9491
9732
  commandName: 'json',
9492
9733
  description: `Display the \`${constants.SOCKET_JSON}\` that would be applied for target folder`,
9493
9734
  hidden: true,
@@ -9506,16 +9747,16 @@ const config$i = {
9506
9747
  `
9507
9748
  };
9508
9749
  const cmdJson = {
9509
- description: config$i.description,
9510
- hidden: config$i.hidden,
9511
- run: run$K
9750
+ description: config$j.description,
9751
+ hidden: config$j.hidden,
9752
+ run: run$L
9512
9753
  };
9513
- async function run$K(argv, importMeta, {
9754
+ async function run$L(argv, importMeta, {
9514
9755
  parentName
9515
9756
  }) {
9516
9757
  const cli = utils.meowOrExit({
9517
9758
  argv,
9518
- config: config$i,
9759
+ config: config$j,
9519
9760
  parentName,
9520
9761
  importMeta
9521
9762
  });
@@ -9670,9 +9911,9 @@ const hidden$r = false;
9670
9911
  const cmdLogin = {
9671
9912
  description: description$x,
9672
9913
  hidden: hidden$r,
9673
- run: run$J
9914
+ run: run$K
9674
9915
  };
9675
- async function run$J(argv, importMeta, {
9916
+ async function run$K(argv, importMeta, {
9676
9917
  parentName
9677
9918
  }) {
9678
9919
  const config = {
@@ -9750,7 +9991,7 @@ function attemptLogout() {
9750
9991
  }
9751
9992
  }
9752
9993
 
9753
- const config$h = {
9994
+ const config$i = {
9754
9995
  commandName: 'logout',
9755
9996
  description: 'Socket API logout',
9756
9997
  hidden: false,
@@ -9768,16 +10009,16 @@ const config$h = {
9768
10009
  `
9769
10010
  };
9770
10011
  const cmdLogout = {
9771
- description: config$h.description,
9772
- hidden: config$h.hidden,
9773
- run: run$I
10012
+ description: config$i.description,
10013
+ hidden: config$i.hidden,
10014
+ run: run$J
9774
10015
  };
9775
- async function run$I(argv, importMeta, {
10016
+ async function run$J(argv, importMeta, {
9776
10017
  parentName
9777
10018
  }) {
9778
10019
  const cli = utils.meowOrExit({
9779
10020
  argv,
9780
- config: config$h,
10021
+ config: config$i,
9781
10022
  importMeta,
9782
10023
  parentName
9783
10024
  });
@@ -10090,7 +10331,7 @@ const yargsConfig = {
10090
10331
  'usages-slices-file' // hidden
10091
10332
  ]
10092
10333
  };
10093
- const config$g = {
10334
+ const config$h = {
10094
10335
  commandName: 'cdxgen',
10095
10336
  description: 'Run cdxgen for SBOM generation',
10096
10337
  hidden: false,
@@ -10100,11 +10341,11 @@ const config$g = {
10100
10341
  help: () => ''
10101
10342
  };
10102
10343
  const cmdManifestCdxgen = {
10103
- description: config$g.description,
10104
- hidden: config$g.hidden,
10105
- run: run$H
10344
+ description: config$h.description,
10345
+ hidden: config$h.hidden,
10346
+ run: run$I
10106
10347
  };
10107
- async function run$H(argv, importMeta, context) {
10348
+ async function run$I(argv, importMeta, context) {
10108
10349
  const {
10109
10350
  parentName
10110
10351
  } = {
@@ -10114,7 +10355,7 @@ async function run$H(argv, importMeta, context) {
10114
10355
  const cli = utils.meowOrExit({
10115
10356
  // Don't let meow take over --help.
10116
10357
  argv: argv.filter(a => !utils.isHelpFlag(a)),
10117
- config: config$g,
10358
+ config: config$h,
10118
10359
  importMeta,
10119
10360
  parentName
10120
10361
  });
@@ -11153,7 +11394,7 @@ async function buildSpokeTagLookup(reached, queryOpts, verbose) {
11153
11394
  return lookup;
11154
11395
  }
11155
11396
 
11156
- const config$f = {
11397
+ const config$g = {
11157
11398
  commandName: 'bazel',
11158
11399
  description: '[beta] Bazel SBOM support — generate manifest files for a Bazel project (Maven, PyPI)',
11159
11400
  hidden: false,
@@ -11237,9 +11478,9 @@ const config$f = {
11237
11478
  // shorter default lives in extract_bazel_to_maven.mts.
11238
11479
  const EXPLICIT_PER_REPO_TIMEOUT_MS = 120_000;
11239
11480
  const cmdManifestBazel = {
11240
- description: config$f.description,
11241
- hidden: config$f.hidden,
11242
- run: run$G
11481
+ description: config$g.description,
11482
+ hidden: config$g.hidden,
11483
+ run: run$H
11243
11484
  };
11244
11485
  // Pure outcome-matrix evaluator. Exported so dispatcher behavior can be
11245
11486
  // unit-tested without spawning the CLI binary. Throws InputError on
@@ -11323,12 +11564,12 @@ function pypiOutcome(result) {
11323
11564
  status: 'hardFailure'
11324
11565
  };
11325
11566
  }
11326
- async function run$G(argv, importMeta, {
11567
+ async function run$H(argv, importMeta, {
11327
11568
  parentName
11328
11569
  }) {
11329
11570
  const cli = utils.meowOrExit({
11330
11571
  argv,
11331
- config: config$f,
11572
+ config: config$g,
11332
11573
  importMeta,
11333
11574
  parentName
11334
11575
  });
@@ -11416,7 +11657,7 @@ async function run$G(argv, importMeta, {
11416
11657
  }
11417
11658
  }
11418
11659
  if (verbose) {
11419
- logger.logger.group('- ', parentName, config$f.commandName, ':');
11660
+ logger.logger.group('- ', parentName, config$g.commandName, ':');
11420
11661
  logger.logger.group('- flags:', cli.flags);
11421
11662
  logger.logger.groupEnd();
11422
11663
  logger.logger.log('- input:', cli.input);
@@ -11493,136 +11734,29 @@ async function run$G(argv, importMeta, {
11493
11734
  evaluateEcosystemOutcomes(outcomes, wasExplicitEcosystemSelection);
11494
11735
  }
11495
11736
 
11496
- const reachabilityFlags = {
11497
- dynamicSbomInference: {
11498
- type: 'boolean',
11499
- default: false,
11500
- hidden: true,
11501
- description: 'Internal: enables dynamic SBOM inference for full application reachability analysis. Passes --maven-use-only-root-socket-facts to Coana and implies --auto-manifest.'
11502
- },
11503
- reachVersion: {
11504
- type: 'string',
11505
- description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.default.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION}.`
11506
- },
11507
- reachAnalysisMemoryLimit: {
11508
- type: 'string',
11509
- default: '8192',
11510
- description: 'The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB.'
11511
- },
11512
- reachAnalysisTimeout: {
11737
+ // Manifest-scoped variant of `../scan/reachability-flags.mts`'s
11738
+ // `excludePathsFlag`: description avoids "scan"/reachability wording since
11739
+ // these commands only ever generate a manifest/facts file.
11740
+ const excludePathsFlag$1 = {
11741
+ excludePaths: {
11513
11742
  type: 'string',
11514
- default: '',
11515
- description: 'Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly.'
11516
- },
11517
- reachConcurrency: {
11518
- type: 'number',
11519
- default: 1,
11520
- description: 'Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available.'
11521
- },
11522
- reachContinueOnAnalysisErrors: {
11523
- type: 'boolean',
11524
- default: false,
11525
- description: 'Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back to precomputed reachability results. By default, the CLI halts on analysis errors.'
11526
- },
11527
- reachContinueOnInstallErrors: {
11528
- type: 'boolean',
11529
- default: false,
11530
- description: 'Continue reachability analysis when package installation fails, falling back to precomputed reachability results. By default, the CLI halts on installation errors.'
11531
- },
11532
- reachContinueOnMissingLockFiles: {
11533
- type: 'boolean',
11534
- default: false,
11535
- description: 'Continue reachability analysis when a Gradle or SBT project is missing its lock file (or version catalog / pre-generated SBOM). By default, the CLI halts.'
11536
- },
11537
- reachContinueOnNoSourceFiles: {
11538
- type: 'boolean',
11539
- default: false,
11540
- description: 'Continue reachability analysis when a workspace contains no source files for its ecosystem. By default, the CLI halts.'
11541
- },
11542
- reachDisableExternalToolChecks: {
11543
- type: 'boolean',
11544
- default: false,
11545
- description: 'Disable external tool checks during reachability analysis.'
11546
- },
11547
- reachDebug: {
11548
- type: 'boolean',
11549
- default: false,
11550
- description: 'Enable debug mode for reachability analysis. Provides verbose logging from the reachability CLI.'
11551
- },
11552
- reachDetailedAnalysisLogFile: {
11553
- type: 'boolean',
11554
- default: false,
11555
- description: 'A log file with detailed analysis logs is written to root of each analyzed workspace.'
11556
- },
11557
- reachDisableAnalytics: {
11558
- type: 'boolean',
11559
- default: false,
11560
- description: 'Disable reachability analytics sharing with Socket. Also disables caching-based optimizations.'
11561
- },
11562
- reachDisableAnalysisSplitting: {
11563
- type: 'boolean',
11564
- default: false,
11565
- hidden: true,
11566
- description: 'Deprecated: Analysis splitting is now disabled by default. This flag is a no-op.'
11567
- },
11568
- reachEnableAnalysisSplitting: {
11569
- type: 'boolean',
11570
- default: false,
11571
- description: 'Allow the reachability analysis to partition CVEs into buckets that are processed in separate analysis runs. May improve accuracy, but not recommended by default.'
11572
- },
11573
- reachEcosystems: {
11574
- type: 'string',
11575
- isMultiple: true,
11576
- description: `List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Supported: ${utils.getReachabilityEcosystemChoices().join(', ')}. Defaults to all supported ecosystems.`
11577
- },
11578
- reachExcludePaths: {
11579
- type: 'string',
11580
- isMultiple: true,
11581
- hidden: true,
11582
- description: 'Deprecated: use --exclude-paths instead. List of paths to exclude from reachability analysis, as either a comma separated value or as multiple flags.'
11583
- },
11584
- reachLazyMode: {
11585
- type: 'boolean',
11586
- default: false,
11587
- description: 'Enable lazy mode for reachability analysis.',
11588
- hidden: true
11589
- },
11590
- reachRetainFactsFile: {
11591
- type: 'boolean',
11592
- default: false,
11593
- description: 'Keep the `.socket.facts.json` reachability report that the analysis writes to the scan directory instead of deleting it after a successful scan. IMPORTANT: you must delete this file before running a fresh full application reachability scan. A stale `.socket.facts.json` left in place is picked up as a pre-generated input and silently overrides fresh analysis, so the new scan results will not be reliable.'
11594
- },
11595
- reachSkipCache: {
11596
- type: 'boolean',
11597
- default: false,
11598
- description: 'Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous runs to speed up the analysis.'
11599
- },
11600
- reachUseOnlyPregeneratedSboms: {
11601
- type: 'boolean',
11602
- default: false,
11603
- description: 'When using this option, the scan is created based only on pre-generated CDX and SPDX files in your project.'
11604
- }
11605
- };
11606
- const excludePathsFlag = {
11607
- excludePaths: {
11608
- type: 'string',
11609
- isMultiple: true,
11610
- description: 'List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
11611
- }
11612
- };
11613
-
11614
- const config$e = {
11615
- commandName: 'auto',
11616
- description: 'Auto-detect build and attempt to generate manifest file',
11617
- hidden: false,
11618
- flags: {
11619
- ...flags.commonFlags,
11620
- ...excludePathsFlag,
11621
- verbose: {
11622
- type: 'boolean',
11623
- default: false,
11624
- description: 'Enable debug output (only for auto itself; sub-steps need to have it pre-configured), may help when running into errors'
11625
- }
11743
+ isMultiple: true,
11744
+ description: 'List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (`--cwd` if set): `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
11745
+ }
11746
+ };
11747
+
11748
+ const config$f = {
11749
+ commandName: 'auto',
11750
+ description: 'Auto-detect build and attempt to generate manifest file',
11751
+ hidden: false,
11752
+ flags: {
11753
+ ...flags.commonFlags,
11754
+ ...excludePathsFlag$1,
11755
+ verbose: {
11756
+ type: 'boolean',
11757
+ default: false,
11758
+ description: 'Enable debug output (only for auto itself; sub-steps need to have it pre-configured), may help when running into errors'
11759
+ }
11626
11760
  },
11627
11761
  help: (command, config) => `
11628
11762
  Usage
@@ -11645,16 +11779,16 @@ const config$e = {
11645
11779
  `
11646
11780
  };
11647
11781
  const cmdManifestAuto = {
11648
- description: config$e.description,
11649
- hidden: config$e.hidden,
11650
- run: run$F
11782
+ description: config$f.description,
11783
+ hidden: config$f.hidden,
11784
+ run: run$G
11651
11785
  };
11652
- async function run$F(argv, importMeta, {
11786
+ async function run$G(argv, importMeta, {
11653
11787
  parentName
11654
11788
  }) {
11655
11789
  const cli = utils.meowOrExit({
11656
11790
  argv,
11657
- config: config$e,
11791
+ config: config$f,
11658
11792
  importMeta,
11659
11793
  parentName
11660
11794
  });
@@ -11672,7 +11806,7 @@ async function run$F(argv, importMeta, {
11672
11806
  cwd = path.resolve(process.cwd(), cwd);
11673
11807
  const outputKind = utils.getOutputKind(json, markdown);
11674
11808
  if (verbose) {
11675
- logger.logger.group('- ', parentName, config$e.commandName, ':');
11809
+ logger.logger.group('- ', parentName, config$f.commandName, ':');
11676
11810
  logger.logger.group('- flags:', cli.flags);
11677
11811
  logger.logger.groupEnd();
11678
11812
  logger.logger.log('- input:', cli.input);
@@ -11710,7 +11844,7 @@ async function run$F(argv, importMeta, {
11710
11844
  logger.logger.success(`Finished. Should have attempted to generate manifest files for ${detected.count} targets.`);
11711
11845
  }
11712
11846
 
11713
- const config$d = {
11847
+ const config$e = {
11714
11848
  commandName: 'conda',
11715
11849
  description: `[beta] Convert a Conda ${constants.ENVIRONMENT_YML} file to a python ${constants.REQUIREMENTS_TXT}`,
11716
11850
  hidden: false,
@@ -11763,16 +11897,16 @@ const config$d = {
11763
11897
  `
11764
11898
  };
11765
11899
  const cmdManifestConda = {
11766
- description: config$d.description,
11767
- hidden: config$d.hidden,
11768
- run: run$E
11900
+ description: config$e.description,
11901
+ hidden: config$e.hidden,
11902
+ run: run$F
11769
11903
  };
11770
- async function run$E(argv, importMeta, {
11904
+ async function run$F(argv, importMeta, {
11771
11905
  parentName
11772
11906
  }) {
11773
11907
  const cli = utils.meowOrExit({
11774
11908
  argv,
11775
- config: config$d,
11909
+ config: config$e,
11776
11910
  importMeta,
11777
11911
  parentName
11778
11912
  });
@@ -11830,7 +11964,7 @@ async function run$E(argv, importMeta, {
11830
11964
  verbose = false;
11831
11965
  }
11832
11966
  if (verbose) {
11833
- logger.logger.group('- ', parentName, config$d.commandName, ':');
11967
+ logger.logger.group('- ', parentName, config$e.commandName, ':');
11834
11968
  logger.logger.group('- flags:', cli.flags);
11835
11969
  logger.logger.groupEnd();
11836
11970
  logger.logger.log('- target:', cwd);
@@ -11866,260 +12000,481 @@ async function run$E(argv, importMeta, {
11866
12000
  });
11867
12001
  }
11868
12002
 
11869
- const config$c = {
11870
- commandName: 'gradle',
11871
- description: '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Gradle/Java/Kotlin/etc project',
11872
- hidden: false,
11873
- flags: {
11874
- ...flags.commonFlags,
11875
- bin: {
11876
- type: 'string',
11877
- description: 'Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH'
11878
- },
11879
- facts: {
11880
- type: 'boolean',
11881
- description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph. This is the default; pass `--pom` to generate `pom.xml` files instead'
11882
- },
11883
- pom: {
11884
- type: 'boolean',
11885
- description: 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)'
11886
- },
11887
- includeConfigs: {
11888
- type: 'string',
11889
- description: 'When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive; `*`, `?`, and `[...]` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. Default: every resolvable configuration'
11890
- },
11891
- excludeConfigs: {
11892
- type: 'string',
11893
- description: 'When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)'
11894
- },
11895
- ...excludePathsFlag,
11896
- ignoreUnresolved: {
11897
- type: 'boolean',
11898
- description: 'When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)'
11899
- },
11900
- gradleOpts: {
11901
- type: 'string',
11902
- description: 'Additional options to pass on to ./gradlew, see `./gradlew --help`'
11903
- },
11904
- verbose: {
11905
- type: 'boolean',
11906
- description: 'Print debug messages'
11907
- }
11908
- },
11909
- help: (command, config) => `
11910
- Usage
11911
- $ ${command} [options] [CWD=.]
11912
-
11913
- Options
11914
- ${utils.getFlagListOutput(config.flags)}
11915
-
11916
- By default, emits a single \`.socket.facts.json\` describing the resolved
11917
- dependency graph of the whole build, using gradle (preferably your local
11918
- \`gradlew\`). An unresolved dependency is a fatal error. You can pass
11919
- --include-configs / --exclude-configs (comma-separated glob patterns) to
11920
- control which configurations are resolved (e.g.
11921
- --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and
11922
- --ignore-unresolved to warn on unresolved dependencies instead of failing.
11923
-
11924
- Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per
11925
- task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm
11926
- (or ${constants.REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is
11927
- Java's dependency repository. Caveats of the \`pom.xml\` conversion:
11928
-
11929
- - each task generates its own xml file (one per task by default)
11930
-
11931
- - certain features may not translate well into the xml; reach out if
11932
- something you need is missing
11933
-
11934
- - it works with your \`gradlew\` from your repo and local settings and config
11935
-
11936
- Support is beta. Please report issues or give us feedback on what's missing.
11937
-
11938
- Examples
12003
+ const BUILD_TOOLS = ['gradle', 'maven', 'sbt'];
11939
12004
 
11940
- $ ${command} .
11941
- $ ${command} --pom .
11942
- $ ${command} --bin=../gradlew .
11943
- `
12005
+ // One glob-suffix set per ecosystem's build-descriptor marker(s); `settings.gradle(.kts)`
12006
+ // covers Kotlin-DSL roots that have no root `build.gradle`.
12007
+ const MARKERS_BY_TOOL = {
12008
+ __proto__: null,
12009
+ gradle: ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts'],
12010
+ maven: ['pom.xml'],
12011
+ sbt: ['build.sbt']
11944
12012
  };
11945
- const cmdManifestGradle = {
11946
- description: config$c.description,
11947
- hidden: config$c.hidden,
11948
- run: run$D
12013
+ const TOOL_BY_MARKER = {
12014
+ __proto__: null
11949
12015
  };
11950
- async function run$D(argv, importMeta, {
11951
- parentName
11952
- }) {
11953
- const cli = utils.meowOrExit({
11954
- argv,
11955
- config: config$c,
11956
- importMeta,
11957
- parentName
11958
- });
11959
- const {
11960
- json = false,
11961
- markdown = false
11962
- } = cli.flags;
11963
- const dryRun = !!cli.flags['dryRun'];
11964
-
11965
- // TODO: Implement json/md further.
11966
- const outputKind = utils.getOutputKind(json, markdown);
11967
- let [cwd = '.'] = cli.input;
11968
- // Note: path.resolve vs .join:
11969
- // If given path is absolute then cwd should not affect it.
11970
- cwd = path.resolve(process.cwd(), cwd);
11971
- const sockJson = utils.readOrDefaultSocketJson(cwd);
11972
- debug.debugFn('inspect', `override: ${constants.SOCKET_JSON} gradle`, sockJson?.defaults?.manifest?.gradle);
11973
- let {
11974
- bin,
11975
- excludeConfigs,
11976
- facts,
11977
- gradleOpts,
11978
- ignoreUnresolved,
11979
- includeConfigs,
11980
- verbose
11981
- } = cli.flags;
12016
+ for (const tool of BUILD_TOOLS) {
12017
+ for (const marker of MARKERS_BY_TOOL[tool]) {
12018
+ TOOL_BY_MARKER[marker] = tool;
12019
+ }
12020
+ }
11982
12021
 
11983
- // Set defaults for any flag/arg that is not given. Check socket.json first.
11984
- if (!bin) {
11985
- if (sockJson.defaults?.manifest?.gradle?.bin) {
11986
- bin = sockJson.defaults?.manifest?.gradle?.bin;
11987
- logger.logger.info(`Using default --bin from ${constants.SOCKET_JSON}:`, bin);
11988
- } else {
11989
- // Prefer the project's ./gradlew wrapper, else `gradle` on PATH.
11990
- bin = resolveBuildToolBin('gradle', cwd);
12022
+ // A disabled ecosystem is skipped entirely by the glob scan below, so a
12023
+ // nested socket.json re-enabling it would never even be searched for. Callers
12024
+ // that need the cascade (not just the root file) to decide skip vs. include
12025
+ // must strip `disabled` here first, and let their own per-directory cascade
12026
+ // check apply it instead.
12027
+ function withoutDisabledFlags(sockJson) {
12028
+ const manifest = sockJson.defaults?.manifest;
12029
+ if (!manifest) {
12030
+ return sockJson;
12031
+ }
12032
+ const stripped = {
12033
+ ...manifest
12034
+ };
12035
+ for (const tool of BUILD_TOOLS) {
12036
+ const section = manifest[tool];
12037
+ if (section?.disabled) {
12038
+ stripped[tool] = {
12039
+ ...section,
12040
+ disabled: false
12041
+ };
11991
12042
  }
11992
12043
  }
11993
- if (!gradleOpts) {
11994
- if (sockJson.defaults?.manifest?.gradle?.gradleOpts) {
11995
- gradleOpts = sockJson.defaults?.manifest?.gradle?.gradleOpts;
11996
- logger.logger.info(`Using default --gradle-opts from ${constants.SOCKET_JSON}:`, gradleOpts);
11997
- } else {
11998
- gradleOpts = '';
12044
+ return {
12045
+ ...sockJson,
12046
+ defaults: {
12047
+ ...sockJson.defaults,
12048
+ manifest: stripped
11999
12049
  }
12050
+ };
12051
+ }
12052
+ async function realpathOrResolved(dir) {
12053
+ try {
12054
+ return await fs$1.promises.realpath(dir);
12055
+ } catch {
12056
+ return path.resolve(dir);
12000
12057
  }
12001
- if (verbose === undefined) {
12002
- if (sockJson.defaults?.manifest?.gradle?.verbose !== undefined) {
12003
- verbose = sockJson.defaults?.manifest?.gradle?.verbose;
12004
- logger.logger.info(`Using default --verbose from ${constants.SOCKET_JSON}:`, verbose);
12005
- } else {
12006
- verbose = false;
12058
+ }
12059
+ function sortByDepthThenPath(dirs, cwd) {
12060
+ return [...dirs].sort((a, b) => {
12061
+ const relA = path.relative(cwd, a);
12062
+ const relB = path.relative(cwd, b);
12063
+ const depthA = relA === '' ? 0 : relA.split(path.sep).length;
12064
+ const depthB = relB === '' ? 0 : relB.split(path.sep).length;
12065
+ if (depthA !== depthB) {
12066
+ return depthA - depthB;
12007
12067
  }
12068
+ return relA < relB ? -1 : relA > relB ? 1 : 0;
12069
+ });
12070
+ }
12071
+
12072
+ // Depth-sorted (root-most first) so the caller, using each build root's
12073
+ // `subprojectDir` facts, visits a reactor root before its own members and can
12074
+ // skip subprojects a parent root already covers.
12075
+ async function findBuildToolCandidates({
12076
+ cwd,
12077
+ excludePaths,
12078
+ sockJson
12079
+ }) {
12080
+ const enabledTools = BUILD_TOOLS.filter(tool => !sockJson.defaults?.manifest?.[tool]?.disabled);
12081
+ const result = new Map();
12082
+ if (!enabledTools.length) {
12083
+ return result;
12008
12084
  }
12009
- if (facts === undefined) {
12010
- if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) {
12011
- facts = sockJson.defaults?.manifest?.gradle?.facts;
12012
- logger.logger.info(`Using default --facts from ${constants.SOCKET_JSON}:`, facts);
12013
- } else {
12014
- // Socket facts generation is the default; pass --pom to generate poms.
12015
- facts = true;
12085
+ const patterns = enabledTools.flatMap(tool => MARKERS_BY_TOOL[tool].map(marker => `**/${marker}`));
12086
+ const additionalIgnores = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
12087
+ const hits = await utils.globWithGitIgnore(patterns, {
12088
+ absolute: true,
12089
+ additionalIgnores,
12090
+ cwd
12091
+ });
12092
+ const dirSetsByTool = new Map(enabledTools.map(tool => [tool, new Set()]));
12093
+ const resolvedHits = await Promise.all(hits.map(async hit => ({
12094
+ dir: await realpathOrResolved(path.dirname(hit)),
12095
+ tool: TOOL_BY_MARKER[path.basename(hit)]
12096
+ })));
12097
+ for (const {
12098
+ dir,
12099
+ tool
12100
+ } of resolvedHits) {
12101
+ if (tool) {
12102
+ dirSetsByTool.get(tool)?.add(dir);
12016
12103
  }
12017
12104
  }
12018
- // --pom opts into legacy pom.xml generation. It overrides the facts default
12019
- // (and the socket.json default) but conflicts with an explicit --facts.
12020
- if (cli.flags['pom']) {
12021
- if (cli.flags['facts'] !== undefined) {
12022
- logger.logger.warn('The `--facts` and `--pom` options are mutually exclusive; generating Socket facts.');
12023
- } else {
12024
- facts = false;
12025
- }
12105
+ const realCwd = await realpathOrResolved(cwd);
12106
+ for (const tool of enabledTools) {
12107
+ result.set(tool, sortByDepthThenPath([...(dirSetsByTool.get(tool) ?? [])], realCwd));
12026
12108
  }
12027
- if (includeConfigs === undefined) {
12028
- if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) {
12029
- includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs;
12030
- logger.logger.info(`Using default --include-configs from ${constants.SOCKET_JSON}:`, includeConfigs);
12031
- } else {
12032
- includeConfigs = '';
12033
- }
12109
+ return result;
12110
+ }
12111
+
12112
+ // A cascaded (not just root-level) disabled additionally skips this one build
12113
+ // root here, on top of its existing root-only ecosystem-wide meaning for
12114
+ // auto/gradle/etc. facts:false has no pom-mode equivalent here (facts-only),
12115
+ // so it skips too.
12116
+ function getSkipReason(disabled, facts) {
12117
+ if (disabled) {
12118
+ return 'defaults.manifest.<ecosystem>.disabled is true';
12034
12119
  }
12035
- if (excludeConfigs === undefined) {
12036
- if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) {
12037
- excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs;
12038
- logger.logger.info(`Using default --exclude-configs from ${constants.SOCKET_JSON}:`, excludeConfigs);
12039
- } else {
12040
- excludeConfigs = '';
12041
- }
12120
+ if (facts === false) {
12121
+ return 'defaults.manifest.<ecosystem>.facts is false (pom mode)';
12042
12122
  }
12043
- if (ignoreUnresolved === undefined) {
12044
- if (sockJson.defaults?.manifest?.gradle?.ignoreUnresolved !== undefined) {
12045
- ignoreUnresolved = sockJson.defaults?.manifest?.gradle?.ignoreUnresolved;
12046
- logger.logger.info(`Using default --ignore-unresolved from ${constants.SOCKET_JSON}:`, ignoreUnresolved);
12047
- } else {
12048
- ignoreUnresolved = false;
12123
+ return undefined;
12124
+ }
12125
+ // Nearest already-confirmed-disabled ancestor of `dir`, if any - lets the
12126
+ // caller shorten `readSocketJsonCascade`'s walk to start there instead of
12127
+ // all the way back at `cwd`, without skipping any nested override.
12128
+ function nearestDisabledRoot(dir, disabledRoots) {
12129
+ let nearest;
12130
+ for (const root of disabledRoots) {
12131
+ if (dir.startsWith(`${root.dir}${path.sep}`) && (!nearest || root.dir.length > nearest.dir.length)) {
12132
+ nearest = root;
12049
12133
  }
12050
12134
  }
12135
+ return nearest;
12136
+ }
12051
12137
 
12052
- // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only
12053
- // affect facts generation; the pom path has no equivalent knobs. Warn rather
12054
- // than silently ignore an explicitly-passed flag. (socket.json defaults don't
12055
- // trip this only a flag actually present on the command line does.)
12056
- if (!facts && (cli.flags['includeConfigs'] !== undefined || cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined)) {
12057
- logger.logger.warn('The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.');
12138
+ // A wrapper-preferred `bin` default is resolved per-root (`dir`, not `cwd`)
12139
+ // since a wrapper script only exists at the actual build root. Exported for
12140
+ // reuse by the recursive setup wizard's reactor-coverage pruning.
12141
+ function resolveEcosystemConfig(ecosystem, dir, sockJson) {
12142
+ if (ecosystem === 'sbt') {
12143
+ const config = sockJson.defaults?.manifest?.sbt;
12144
+ const bin = config?.bin ?? undefined;
12145
+ return {
12146
+ bin: bin ?? 'sbt',
12147
+ buildOpts: parseBuildToolOpts(config?.sbtOpts ?? undefined),
12148
+ excludeConfigs: config?.excludeConfigs ?? '',
12149
+ ignoreUnresolved: Boolean(config?.ignoreUnresolved),
12150
+ includeConfigs: config?.includeConfigs ?? '',
12151
+ javaHome: config?.javaHome ?? undefined,
12152
+ skipReason: getSkipReason(config?.disabled, config?.facts)
12153
+ };
12154
+ }
12155
+ if (ecosystem === 'gradle') {
12156
+ const config = sockJson.defaults?.manifest?.gradle;
12157
+ const bin = config?.bin ?? undefined;
12158
+ return {
12159
+ bin: bin ? path.resolve(dir, bin) : resolveBuildToolBin('gradle', dir),
12160
+ buildOpts: parseBuildToolOpts(config?.gradleOpts ?? undefined),
12161
+ excludeConfigs: config?.excludeConfigs ?? '',
12162
+ ignoreUnresolved: Boolean(config?.ignoreUnresolved),
12163
+ includeConfigs: config?.includeConfigs ?? '',
12164
+ javaHome: config?.javaHome ?? undefined,
12165
+ skipReason: getSkipReason(config?.disabled, config?.facts)
12166
+ };
12167
+ }
12168
+ const config = sockJson.defaults?.manifest?.maven;
12169
+ const bin = config?.bin ?? undefined;
12170
+ return {
12171
+ bin: bin ?? resolveBuildToolBin('maven', dir),
12172
+ buildOpts: parseBuildToolOpts(config?.mavenOpts ?? undefined),
12173
+ excludeConfigs: config?.excludeConfigs ?? '',
12174
+ ignoreUnresolved: Boolean(config?.ignoreUnresolved),
12175
+ includeConfigs: config?.includeConfigs ?? '',
12176
+ javaHome: config?.javaHome ?? undefined,
12177
+ skipReason: getSkipReason(config?.disabled)
12178
+ };
12179
+ }
12180
+
12181
+ // Generates one .socket.facts.json per independent gradle/sbt/maven build
12182
+ // root under `cwd`. Coverage is tracked per ecosystem via the facts SBOM's
12183
+ // own projects[].subprojectDir, not by pruning the whole discovered subtree,
12184
+ // so an unrelated nested project a reactor doesn't declare still gets its
12185
+ // own invocation. Fail-closed per ecosystem, not globally: a root whose
12186
+ // workspace layout can't be determined aborts only that ecosystem's own
12187
+ // remaining walk (marking its untried candidates 'aborted'), since coverage
12188
+ // is tracked per ecosystem and an unrelated one has nothing to lose from it.
12189
+ async function generateRecursiveManifests({
12190
+ cwd,
12191
+ excludePaths,
12192
+ verbose
12193
+ }) {
12194
+ const rootSockJson = utils.readOrDefaultSocketJson(cwd);
12195
+ // Candidate dirs come back realpath-resolved (findBuildToolCandidates); cwd
12196
+ // must match or every boundary/relative-path comparison below breaks as
12197
+ // soon as cwd contains a symlink (macOS /tmp -> /private/tmp, etc.).
12198
+ const realCwd = await realpathOrResolved(cwd);
12199
+ // A root-disabled ecosystem must still be scanned for - a nested socket.json
12200
+ // may re-enable it - so the per-directory cascade below, not this scan, is
12201
+ // what actually decides skip vs. include.
12202
+ const candidatesByTool = await findBuildToolCandidates({
12203
+ cwd,
12204
+ excludePaths,
12205
+ sockJson: withoutDisabledFlags(rootSockJson)
12206
+ });
12207
+ const outcomes = [];
12208
+ for (const [ecosystem, dirs] of candidatesByTool) {
12209
+ const covered = new Set();
12210
+ const disabledRoots = [];
12211
+ for (let dirIndex = 0; dirIndex < dirs.length; dirIndex += 1) {
12212
+ const dir = dirs[dirIndex];
12213
+ if (covered.has(dir)) {
12214
+ outcomes.push({
12215
+ dir,
12216
+ ecosystem,
12217
+ status: 'skippedCovered'
12218
+ });
12219
+ continue;
12220
+ }
12221
+ const nearestRoot = nearestDisabledRoot(dir, disabledRoots);
12222
+ const sockJson = nearestRoot ? utils.readSocketJsonCascade(dir, nearestRoot.dir, nearestRoot.sockJson) : utils.readSocketJsonCascade(dir, realCwd, rootSockJson);
12223
+ const {
12224
+ bin,
12225
+ buildOpts,
12226
+ excludeConfigs,
12227
+ ignoreUnresolved,
12228
+ includeConfigs,
12229
+ javaHome,
12230
+ skipReason
12231
+ } = resolveEcosystemConfig(ecosystem, dir, sockJson);
12232
+ if (skipReason) {
12233
+ // Only warn for a genuinely new disabled root, not one already
12234
+ // covered by an ancestor's warning above - otherwise a big disabled
12235
+ // reactor with hundreds of nested poms would spam one warning line
12236
+ // per pom for what's really a single root cause. The aggregate
12237
+ // count still shows up in the final summary either way.
12238
+ if (!nearestRoot) {
12239
+ logger.logger.warn(`Skipping ${dir} (${ecosystem}): ${skipReason}.`);
12240
+ }
12241
+ outcomes.push({
12242
+ dir,
12243
+ ecosystem,
12244
+ status: 'skippedDisabled'
12245
+ });
12246
+ disabledRoots.push({
12247
+ dir,
12248
+ sockJson
12249
+ });
12250
+ continue;
12251
+ }
12252
+ const excludePathsForRoot = projectIgnorePathsToReachExcludePaths(excludePaths, {
12253
+ cwd: realCwd,
12254
+ target: dir
12255
+ });
12256
+
12257
+ // eslint-disable-next-line no-await-in-loop
12258
+ const result = await runManifestFacts({
12259
+ bin,
12260
+ buildOpts,
12261
+ cwd: dir,
12262
+ ecosystem,
12263
+ excludeConfigs,
12264
+ excludePaths: excludePathsForRoot,
12265
+ ignoreUnresolved,
12266
+ includeConfigs,
12267
+ javaHome,
12268
+ verbose
12269
+ });
12270
+ if (result === null) {
12271
+ outcomes.push({
12272
+ dir,
12273
+ ecosystem,
12274
+ status: 'failed'
12275
+ });
12276
+ logger.logger.warn(`Aborting ${ecosystem} discovery: ${dir}'s workspace layout could not be determined, so its remaining build roots cannot be safely classified as covered or independent.`);
12277
+ for (const abortedDir of dirs.slice(dirIndex + 1)) {
12278
+ outcomes.push({
12279
+ dir: abortedDir,
12280
+ ecosystem,
12281
+ status: 'aborted'
12282
+ });
12283
+ }
12284
+ break;
12285
+ }
12286
+ if (!result) {
12287
+ outcomes.push({
12288
+ dir,
12289
+ ecosystem,
12290
+ status: 'empty'
12291
+ });
12292
+ continue;
12293
+ }
12294
+ covered.add(dir);
12295
+ // eslint-disable-next-line no-await-in-loop
12296
+ const resolvedSubprojectDirs = await Promise.all(result.projects.map(project => realpathOrResolved(path.resolve(dir, project.subprojectDir))));
12297
+ for (const subprojectDir of resolvedSubprojectDirs) {
12298
+ covered.add(subprojectDir);
12299
+ }
12300
+ outcomes.push({
12301
+ dir,
12302
+ ecosystem,
12303
+ factsPath: result.factsPath,
12304
+ status: 'generated'
12305
+ });
12306
+ }
12058
12307
  }
12059
12308
  if (verbose) {
12060
- logger.logger.group('- ', parentName, config$c.commandName, ':');
12309
+ logger.logger.info(`Discovered ${outcomes.length} build-tool candidate(s).`);
12310
+ }
12311
+ return outcomes;
12312
+ }
12313
+
12314
+ function renderTable(outcomes) {
12315
+ // A covered reactor member is implied by its parent's line above it.
12316
+ return outcomes.filter(o => o.status !== 'skippedCovered').map(o => `- ${o.dir} (${o.ecosystem}): ${o.status}${o.factsPath ? ` -> ${o.factsPath}` : ''}`).join('\n');
12317
+ }
12318
+ function summarize(outcomes) {
12319
+ const generated = outcomes.filter(o => o.status === 'generated').length;
12320
+ return `Generated ${generated} Socket facts file(s).`;
12321
+ }
12322
+ async function outputManifestDynamicSbomInference(result, outputKind) {
12323
+ if (!result.ok) {
12324
+ process.exitCode = result.code ?? 1;
12325
+ }
12326
+ if (outputKind === 'json') {
12327
+ logger.logger.log(utils.serializeResultJson(result));
12328
+ return;
12329
+ }
12330
+ if (!result.ok) {
12331
+ logger.logger.fail(utils.failMsgWithBadge(result.message, result.cause));
12332
+ const data = result.data;
12333
+ if (Array.isArray(data)) {
12334
+ const table = renderTable(data);
12335
+ if (table) {
12336
+ logger.logger.log(table);
12337
+ }
12338
+ logger.logger.log(summarize(data));
12339
+ }
12340
+ return;
12341
+ }
12342
+ const table = renderTable(result.data);
12343
+ if (outputKind === 'markdown') {
12344
+ logger.logger.log(['# Dynamic SBOM inference', '', ...(table ? [table, ''] : []), summarize(result.data)].join('\n'));
12345
+ return;
12346
+ }
12347
+ if (table) {
12348
+ logger.logger.log(table);
12349
+ }
12350
+ logger.logger.log(summarize(result.data));
12351
+ }
12352
+
12353
+ async function handleManifestDynamicSbomInference({
12354
+ cwd,
12355
+ excludePaths,
12356
+ outputKind,
12357
+ verbose
12358
+ }) {
12359
+ const outcomes = await generateRecursiveManifests({
12360
+ cwd,
12361
+ excludePaths,
12362
+ verbose
12363
+ });
12364
+ const result = outcomes.some(o => o.status === 'failed') ? {
12365
+ ok: false,
12366
+ code: 1,
12367
+ message: 'One or more build roots failed to generate Socket facts.',
12368
+ data: outcomes
12369
+ } : {
12370
+ ok: true,
12371
+ data: outcomes
12372
+ };
12373
+ await outputManifestDynamicSbomInference(result, outputKind);
12374
+ }
12375
+
12376
+ const config$d = {
12377
+ commandName: 'dynamic-sbom-inference',
12378
+ description: 'Recursively discover gradle/sbt/maven build roots and generate a Socket facts SBOM for each',
12379
+ // Hidden: `--dynamic-sbom-inference` already names an unrelated, root-only
12380
+ // scan create/reach flag (see reachability-flags.mts). Keep this hidden
12381
+ // until the naming collision between the two is resolved.
12382
+ hidden: true,
12383
+ flags: {
12384
+ ...flags.commonFlags,
12385
+ ...flags.outputFlags,
12386
+ ...excludePathsFlag$1,
12387
+ verbose: {
12388
+ type: 'boolean',
12389
+ default: false,
12390
+ description: 'Print debug messages'
12391
+ }
12392
+ },
12393
+ help: (command, config) => `
12394
+ Usage
12395
+ $ ${command} [options] [CWD=.]
12396
+
12397
+ Recursively walks CWD, discovers independent gradle, sbt, and maven build
12398
+ roots, and generates a Socket facts SBOM (.socket.facts.json) for each,
12399
+ skipping subproject/reactor-module directories a parent build root already
12400
+ covers. Unlike \`socket manifest auto\`, this looks beyond CWD itself.
12401
+
12402
+ Options
12403
+ ${utils.getFlagListOutput(config.flags)}
12404
+
12405
+ Examples
12406
+
12407
+ $ ${command}
12408
+ $ ${command} ./monorepo
12409
+ `
12410
+ };
12411
+ const cmdManifestDynamicSbomInference = {
12412
+ description: config$d.description,
12413
+ hidden: config$d.hidden,
12414
+ run: run$E
12415
+ };
12416
+ async function run$E(argv, importMeta, {
12417
+ parentName
12418
+ }) {
12419
+ const cli = utils.meowOrExit({
12420
+ argv,
12421
+ config: config$d,
12422
+ importMeta,
12423
+ parentName
12424
+ });
12425
+ const {
12426
+ dryRun,
12427
+ json,
12428
+ markdown,
12429
+ verbose: verboseFlag
12430
+ } = cli.flags;
12431
+ const verbose = !!verboseFlag;
12432
+ let [cwd = '.'] = cli.input;
12433
+ // Note: path.resolve vs .join:
12434
+ // If given path is absolute then cwd should not affect it.
12435
+ cwd = path.resolve(process.cwd(), cwd);
12436
+
12437
+ // This debug block prints to stdout; --json's payload does too, so skip it
12438
+ // here (unlike the other manifest commands, this one supports --json).
12439
+ if (verbose && !json) {
12440
+ logger.logger.group('- ', parentName, config$d.commandName, ':');
12061
12441
  logger.logger.group('- flags:', cli.flags);
12062
12442
  logger.logger.groupEnd();
12063
- logger.logger.log('- input:', cli.input);
12443
+ logger.logger.log('- target:', cwd);
12064
12444
  logger.logger.groupEnd();
12065
12445
  }
12066
-
12067
- // TODO: We're not sure it's feasible to parse source file from stdin. We could
12068
- // try, store contents in a file in some folder, target that folder... what
12069
- // would the file name be?
12070
-
12446
+ const outputKind = utils.getOutputKind(json, markdown);
12071
12447
  const wasValidInput = utils.checkCommandInput(outputKind, {
12072
12448
  nook: true,
12073
12449
  test: cli.input.length <= 1,
12074
12450
  message: 'Can only accept one DIR (make sure to escape spaces!)',
12075
- fail: 'received ' + cli.input.length
12451
+ fail: `received ${cli.input.length}`
12452
+ }, {
12453
+ nook: true,
12454
+ test: !json || !markdown,
12455
+ message: `The \`${constants.FLAG_JSON}\` and \`${constants.FLAG_MARKDOWN}\` flags can not be used at the same time`,
12456
+ fail: 'bad'
12076
12457
  });
12077
12458
  if (!wasValidInput) {
12078
12459
  return;
12079
12460
  }
12080
- if (verbose) {
12081
- logger.logger.group();
12082
- logger.logger.info('- cwd:', cwd);
12083
- logger.logger.info('- gradle bin:', bin);
12084
- logger.logger.groupEnd();
12085
- }
12086
- if (dryRun) {
12087
- logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
12088
- return;
12089
- }
12090
- const parsedGradleOpts = parseBuildToolOpts(String(gradleOpts || ''));
12091
12461
  const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
12092
12462
  assertValidExcludePaths(excludePaths);
12093
- if (facts) {
12094
- await convertGradleToFacts({
12095
- bin: String(bin),
12096
- cwd,
12097
- excludeConfigs: String(excludeConfigs || ''),
12098
- excludePaths,
12099
- gradleOpts: parsedGradleOpts,
12100
- ignoreUnresolved: Boolean(ignoreUnresolved),
12101
- includeConfigs: String(includeConfigs || ''),
12102
- verbose: Boolean(verbose)
12103
- });
12463
+ if (dryRun) {
12464
+ logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
12104
12465
  return;
12105
12466
  }
12106
- await convertGradleToMaven({
12107
- bin: String(bin),
12467
+ await handleManifestDynamicSbomInference({
12108
12468
  cwd,
12109
12469
  excludePaths,
12110
- gradleOpts: parsedGradleOpts,
12111
- verbose: Boolean(verbose)
12470
+ outputKind,
12471
+ verbose
12112
12472
  });
12113
12473
  }
12114
12474
 
12115
- // TODO: We may want to dedupe some pieces for all gradle languages. I think it
12116
- // makes sense to have separate commands for them and I think it makes
12117
- // sense for the help panels to note the requested language, rather than
12118
- // `socket manifest kotlin` to print help screens with `gradle` as the
12119
- // command. Room for improvement.
12120
- const config$b = {
12121
- commandName: 'kotlin',
12122
- description: '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Kotlin project',
12475
+ const config$c = {
12476
+ commandName: 'gradle',
12477
+ description: '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Gradle/Java/Kotlin/etc project',
12123
12478
  hidden: false,
12124
12479
  flags: {
12125
12480
  ...flags.commonFlags,
@@ -12143,7 +12498,7 @@ const config$b = {
12143
12498
  type: 'string',
12144
12499
  description: 'When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)'
12145
12500
  },
12146
- ...excludePathsFlag,
12501
+ ...excludePathsFlag$1,
12147
12502
  ignoreUnresolved: {
12148
12503
  type: 'boolean',
12149
12504
  description: 'When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)'
@@ -12193,17 +12548,17 @@ const config$b = {
12193
12548
  $ ${command} --bin=../gradlew .
12194
12549
  `
12195
12550
  };
12196
- const cmdManifestKotlin = {
12197
- description: config$b.description,
12198
- hidden: config$b.hidden,
12199
- run: run$C
12551
+ const cmdManifestGradle = {
12552
+ description: config$c.description,
12553
+ hidden: config$c.hidden,
12554
+ run: run$D
12200
12555
  };
12201
- async function run$C(argv, importMeta, {
12556
+ async function run$D(argv, importMeta, {
12202
12557
  parentName
12203
12558
  }) {
12204
12559
  const cli = utils.meowOrExit({
12205
12560
  argv,
12206
- config: config$b,
12561
+ config: config$c,
12207
12562
  importMeta,
12208
12563
  parentName
12209
12564
  });
@@ -12276,16 +12631,16 @@ async function run$C(argv, importMeta, {
12276
12631
  }
12277
12632
  }
12278
12633
  if (includeConfigs === undefined) {
12279
- if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) {
12280
- includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs;
12634
+ if (sockJson.defaults?.manifest?.gradle?.includeConfigs) {
12635
+ includeConfigs = sockJson.defaults.manifest.gradle.includeConfigs;
12281
12636
  logger.logger.info(`Using default --include-configs from ${constants.SOCKET_JSON}:`, includeConfigs);
12282
12637
  } else {
12283
12638
  includeConfigs = '';
12284
12639
  }
12285
12640
  }
12286
12641
  if (excludeConfigs === undefined) {
12287
- if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) {
12288
- excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs;
12642
+ if (sockJson.defaults?.manifest?.gradle?.excludeConfigs) {
12643
+ excludeConfigs = sockJson.defaults.manifest.gradle.excludeConfigs;
12289
12644
  logger.logger.info(`Using default --exclude-configs from ${constants.SOCKET_JSON}:`, excludeConfigs);
12290
12645
  } else {
12291
12646
  excludeConfigs = '';
@@ -12301,12 +12656,14 @@ async function run$C(argv, importMeta, {
12301
12656
  }
12302
12657
 
12303
12658
  // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only
12304
- // affect facts generation; the pom path has no equivalent knobs.
12659
+ // affect facts generation; the pom path has no equivalent knobs. Warn rather
12660
+ // than silently ignore an explicitly-passed flag. (socket.json defaults don't
12661
+ // trip this — only a flag actually present on the command line does.)
12305
12662
  if (!facts && (cli.flags['includeConfigs'] !== undefined || cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined)) {
12306
12663
  logger.logger.warn('The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.');
12307
12664
  }
12308
12665
  if (verbose) {
12309
- logger.logger.group('- ', parentName, config$b.commandName, ':');
12666
+ logger.logger.group('- ', parentName, config$c.commandName, ':');
12310
12667
  logger.logger.group('- flags:', cli.flags);
12311
12668
  logger.logger.groupEnd();
12312
12669
  logger.logger.log('- input:', cli.input);
@@ -12326,10 +12683,12 @@ async function run$C(argv, importMeta, {
12326
12683
  if (!wasValidInput) {
12327
12684
  return;
12328
12685
  }
12686
+ const javaHome = sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined;
12329
12687
  if (verbose) {
12330
12688
  logger.logger.group();
12331
12689
  logger.logger.info('- cwd:', cwd);
12332
12690
  logger.logger.info('- gradle bin:', bin);
12691
+ logger.logger.info('- java home:', javaHome || '(inherited)');
12333
12692
  logger.logger.groupEnd();
12334
12693
  }
12335
12694
  if (dryRun) {
@@ -12348,6 +12707,7 @@ async function run$C(argv, importMeta, {
12348
12707
  gradleOpts: parsedGradleOpts,
12349
12708
  ignoreUnresolved: Boolean(ignoreUnresolved),
12350
12709
  includeConfigs: String(includeConfigs || ''),
12710
+ javaHome,
12351
12711
  verbose: Boolean(verbose)
12352
12712
  });
12353
12713
  return;
@@ -12361,16 +12721,268 @@ async function run$C(argv, importMeta, {
12361
12721
  });
12362
12722
  }
12363
12723
 
12364
- const config$a = {
12365
- commandName: 'maven',
12366
- description: '[beta] Generate a Socket facts file from a Maven `pom.xml` project',
12367
- hidden: false,
12368
- flags: {
12369
- ...flags.commonFlags,
12370
- bin: {
12371
- type: 'string',
12372
- description: 'Location of the maven binary to use, default: ./mvnw if present, else mvn on PATH'
12373
- },
12724
+ // TODO: We may want to dedupe some pieces for all gradle languages. I think it
12725
+ // makes sense to have separate commands for them and I think it makes
12726
+ // sense for the help panels to note the requested language, rather than
12727
+ // `socket manifest kotlin` to print help screens with `gradle` as the
12728
+ // command. Room for improvement.
12729
+ const config$b = {
12730
+ commandName: 'kotlin',
12731
+ description: '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Kotlin project',
12732
+ hidden: false,
12733
+ flags: {
12734
+ ...flags.commonFlags,
12735
+ bin: {
12736
+ type: 'string',
12737
+ description: 'Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH'
12738
+ },
12739
+ facts: {
12740
+ type: 'boolean',
12741
+ description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph. This is the default; pass `--pom` to generate `pom.xml` files instead'
12742
+ },
12743
+ pom: {
12744
+ type: 'boolean',
12745
+ description: 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)'
12746
+ },
12747
+ includeConfigs: {
12748
+ type: 'string',
12749
+ description: 'When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive; `*`, `?`, and `[...]` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. Default: every resolvable configuration'
12750
+ },
12751
+ excludeConfigs: {
12752
+ type: 'string',
12753
+ description: 'When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)'
12754
+ },
12755
+ ...excludePathsFlag$1,
12756
+ ignoreUnresolved: {
12757
+ type: 'boolean',
12758
+ description: 'When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)'
12759
+ },
12760
+ gradleOpts: {
12761
+ type: 'string',
12762
+ description: 'Additional options to pass on to ./gradlew, see `./gradlew --help`'
12763
+ },
12764
+ verbose: {
12765
+ type: 'boolean',
12766
+ description: 'Print debug messages'
12767
+ }
12768
+ },
12769
+ help: (command, config) => `
12770
+ Usage
12771
+ $ ${command} [options] [CWD=.]
12772
+
12773
+ Options
12774
+ ${utils.getFlagListOutput(config.flags)}
12775
+
12776
+ By default, emits a single \`.socket.facts.json\` describing the resolved
12777
+ dependency graph of the whole build, using gradle (preferably your local
12778
+ \`gradlew\`). An unresolved dependency is a fatal error. You can pass
12779
+ --include-configs / --exclude-configs (comma-separated glob patterns) to
12780
+ control which configurations are resolved (e.g.
12781
+ --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and
12782
+ --ignore-unresolved to warn on unresolved dependencies instead of failing.
12783
+
12784
+ Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per
12785
+ task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm
12786
+ (or ${constants.REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is
12787
+ Java's dependency repository. Caveats of the \`pom.xml\` conversion:
12788
+
12789
+ - each task generates its own xml file (one per task by default)
12790
+
12791
+ - certain features may not translate well into the xml; reach out if
12792
+ something you need is missing
12793
+
12794
+ - it works with your \`gradlew\` from your repo and local settings and config
12795
+
12796
+ Support is beta. Please report issues or give us feedback on what's missing.
12797
+
12798
+ Examples
12799
+
12800
+ $ ${command} .
12801
+ $ ${command} --pom .
12802
+ $ ${command} --bin=../gradlew .
12803
+ `
12804
+ };
12805
+ const cmdManifestKotlin = {
12806
+ description: config$b.description,
12807
+ hidden: config$b.hidden,
12808
+ run: run$C
12809
+ };
12810
+ async function run$C(argv, importMeta, {
12811
+ parentName
12812
+ }) {
12813
+ const cli = utils.meowOrExit({
12814
+ argv,
12815
+ config: config$b,
12816
+ importMeta,
12817
+ parentName
12818
+ });
12819
+ const {
12820
+ json = false,
12821
+ markdown = false
12822
+ } = cli.flags;
12823
+ const dryRun = !!cli.flags['dryRun'];
12824
+
12825
+ // TODO: Implement json/md further.
12826
+ const outputKind = utils.getOutputKind(json, markdown);
12827
+ let [cwd = '.'] = cli.input;
12828
+ // Note: path.resolve vs .join:
12829
+ // If given path is absolute then cwd should not affect it.
12830
+ cwd = path.resolve(process.cwd(), cwd);
12831
+ const sockJson = utils.readOrDefaultSocketJson(cwd);
12832
+ debug.debugFn('inspect', `override: ${constants.SOCKET_JSON} gradle`, sockJson?.defaults?.manifest?.gradle);
12833
+ let {
12834
+ bin,
12835
+ excludeConfigs,
12836
+ facts,
12837
+ gradleOpts,
12838
+ ignoreUnresolved,
12839
+ includeConfigs,
12840
+ verbose
12841
+ } = cli.flags;
12842
+
12843
+ // Set defaults for any flag/arg that is not given. Check socket.json first.
12844
+ if (!bin) {
12845
+ if (sockJson.defaults?.manifest?.gradle?.bin) {
12846
+ bin = sockJson.defaults?.manifest?.gradle?.bin;
12847
+ logger.logger.info(`Using default --bin from ${constants.SOCKET_JSON}:`, bin);
12848
+ } else {
12849
+ // Prefer the project's ./gradlew wrapper, else `gradle` on PATH.
12850
+ bin = resolveBuildToolBin('gradle', cwd);
12851
+ }
12852
+ }
12853
+ if (!gradleOpts) {
12854
+ if (sockJson.defaults?.manifest?.gradle?.gradleOpts) {
12855
+ gradleOpts = sockJson.defaults?.manifest?.gradle?.gradleOpts;
12856
+ logger.logger.info(`Using default --gradle-opts from ${constants.SOCKET_JSON}:`, gradleOpts);
12857
+ } else {
12858
+ gradleOpts = '';
12859
+ }
12860
+ }
12861
+ if (verbose === undefined) {
12862
+ if (sockJson.defaults?.manifest?.gradle?.verbose !== undefined) {
12863
+ verbose = sockJson.defaults?.manifest?.gradle?.verbose;
12864
+ logger.logger.info(`Using default --verbose from ${constants.SOCKET_JSON}:`, verbose);
12865
+ } else {
12866
+ verbose = false;
12867
+ }
12868
+ }
12869
+ if (facts === undefined) {
12870
+ if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) {
12871
+ facts = sockJson.defaults?.manifest?.gradle?.facts;
12872
+ logger.logger.info(`Using default --facts from ${constants.SOCKET_JSON}:`, facts);
12873
+ } else {
12874
+ // Socket facts generation is the default; pass --pom to generate poms.
12875
+ facts = true;
12876
+ }
12877
+ }
12878
+ // --pom opts into legacy pom.xml generation. It overrides the facts default
12879
+ // (and the socket.json default) but conflicts with an explicit --facts.
12880
+ if (cli.flags['pom']) {
12881
+ if (cli.flags['facts'] !== undefined) {
12882
+ logger.logger.warn('The `--facts` and `--pom` options are mutually exclusive; generating Socket facts.');
12883
+ } else {
12884
+ facts = false;
12885
+ }
12886
+ }
12887
+ if (includeConfigs === undefined) {
12888
+ if (sockJson.defaults?.manifest?.gradle?.includeConfigs) {
12889
+ includeConfigs = sockJson.defaults.manifest.gradle.includeConfigs;
12890
+ logger.logger.info(`Using default --include-configs from ${constants.SOCKET_JSON}:`, includeConfigs);
12891
+ } else {
12892
+ includeConfigs = '';
12893
+ }
12894
+ }
12895
+ if (excludeConfigs === undefined) {
12896
+ if (sockJson.defaults?.manifest?.gradle?.excludeConfigs) {
12897
+ excludeConfigs = sockJson.defaults.manifest.gradle.excludeConfigs;
12898
+ logger.logger.info(`Using default --exclude-configs from ${constants.SOCKET_JSON}:`, excludeConfigs);
12899
+ } else {
12900
+ excludeConfigs = '';
12901
+ }
12902
+ }
12903
+ if (ignoreUnresolved === undefined) {
12904
+ if (sockJson.defaults?.manifest?.gradle?.ignoreUnresolved !== undefined) {
12905
+ ignoreUnresolved = sockJson.defaults?.manifest?.gradle?.ignoreUnresolved;
12906
+ logger.logger.info(`Using default --ignore-unresolved from ${constants.SOCKET_JSON}:`, ignoreUnresolved);
12907
+ } else {
12908
+ ignoreUnresolved = false;
12909
+ }
12910
+ }
12911
+
12912
+ // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only
12913
+ // affect facts generation; the pom path has no equivalent knobs.
12914
+ if (!facts && (cli.flags['includeConfigs'] !== undefined || cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined)) {
12915
+ logger.logger.warn('The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.');
12916
+ }
12917
+ if (verbose) {
12918
+ logger.logger.group('- ', parentName, config$b.commandName, ':');
12919
+ logger.logger.group('- flags:', cli.flags);
12920
+ logger.logger.groupEnd();
12921
+ logger.logger.log('- input:', cli.input);
12922
+ logger.logger.groupEnd();
12923
+ }
12924
+
12925
+ // TODO: We're not sure it's feasible to parse source file from stdin. We could
12926
+ // try, store contents in a file in some folder, target that folder... what
12927
+ // would the file name be?
12928
+
12929
+ const wasValidInput = utils.checkCommandInput(outputKind, {
12930
+ nook: true,
12931
+ test: cli.input.length <= 1,
12932
+ message: 'Can only accept one DIR (make sure to escape spaces!)',
12933
+ fail: 'received ' + cli.input.length
12934
+ });
12935
+ if (!wasValidInput) {
12936
+ return;
12937
+ }
12938
+ const javaHome = sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined;
12939
+ if (verbose) {
12940
+ logger.logger.group();
12941
+ logger.logger.info('- cwd:', cwd);
12942
+ logger.logger.info('- gradle bin:', bin);
12943
+ logger.logger.info('- java home:', javaHome || '(inherited)');
12944
+ logger.logger.groupEnd();
12945
+ }
12946
+ if (dryRun) {
12947
+ logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
12948
+ return;
12949
+ }
12950
+ const parsedGradleOpts = parseBuildToolOpts(String(gradleOpts || ''));
12951
+ const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
12952
+ assertValidExcludePaths(excludePaths);
12953
+ if (facts) {
12954
+ await convertGradleToFacts({
12955
+ bin: String(bin),
12956
+ cwd,
12957
+ excludeConfigs: String(excludeConfigs || ''),
12958
+ excludePaths,
12959
+ gradleOpts: parsedGradleOpts,
12960
+ ignoreUnresolved: Boolean(ignoreUnresolved),
12961
+ includeConfigs: String(includeConfigs || ''),
12962
+ javaHome,
12963
+ verbose: Boolean(verbose)
12964
+ });
12965
+ return;
12966
+ }
12967
+ await convertGradleToMaven({
12968
+ bin: String(bin),
12969
+ cwd,
12970
+ excludePaths,
12971
+ gradleOpts: parsedGradleOpts,
12972
+ verbose: Boolean(verbose)
12973
+ });
12974
+ }
12975
+
12976
+ const config$a = {
12977
+ commandName: 'maven',
12978
+ description: '[beta] Generate a Socket facts file from a Maven `pom.xml` project',
12979
+ hidden: false,
12980
+ flags: {
12981
+ ...flags.commonFlags,
12982
+ bin: {
12983
+ type: 'string',
12984
+ description: 'Location of the maven binary to use, default: ./mvnw if present, else mvn on PATH'
12985
+ },
12374
12986
  includeConfigs: {
12375
12987
  type: 'string',
12376
12988
  description: 'Comma-separated glob patterns matched against Maven dependency scopes (case-sensitive; `*`, `?`, and `[...]` wildcards). Only scopes matching at least one pattern are resolved. e.g. `compile,runtime`. Default: every scope'
@@ -12379,7 +12991,7 @@ const config$a = {
12379
12991
  type: 'string',
12380
12992
  description: 'Comma-separated glob patterns; Maven scopes matching any pattern are skipped (applied after --include-configs)'
12381
12993
  },
12382
- ...excludePathsFlag,
12994
+ ...excludePathsFlag$1,
12383
12995
  ignoreUnresolved: {
12384
12996
  type: 'boolean',
12385
12997
  description: 'Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)'
@@ -12477,16 +13089,16 @@ async function run$B(argv, importMeta, {
12477
13089
  }
12478
13090
  }
12479
13091
  if (includeConfigs === undefined) {
12480
- if (sockJson.defaults?.manifest?.maven?.includeConfigs !== undefined) {
12481
- includeConfigs = sockJson.defaults?.manifest?.maven?.includeConfigs;
13092
+ if (sockJson.defaults?.manifest?.maven?.includeConfigs) {
13093
+ includeConfigs = sockJson.defaults.manifest.maven.includeConfigs;
12482
13094
  logger.logger.info(`Using default --include-configs from ${constants.SOCKET_JSON}:`, includeConfigs);
12483
13095
  } else {
12484
13096
  includeConfigs = '';
12485
13097
  }
12486
13098
  }
12487
13099
  if (excludeConfigs === undefined) {
12488
- if (sockJson.defaults?.manifest?.maven?.excludeConfigs !== undefined) {
12489
- excludeConfigs = sockJson.defaults?.manifest?.maven?.excludeConfigs;
13100
+ if (sockJson.defaults?.manifest?.maven?.excludeConfigs) {
13101
+ excludeConfigs = sockJson.defaults.manifest.maven.excludeConfigs;
12490
13102
  logger.logger.info(`Using default --exclude-configs from ${constants.SOCKET_JSON}:`, excludeConfigs);
12491
13103
  } else {
12492
13104
  excludeConfigs = '';
@@ -12524,10 +13136,12 @@ async function run$B(argv, importMeta, {
12524
13136
  if (!wasValidInput) {
12525
13137
  return;
12526
13138
  }
13139
+ const javaHome = sockJson.defaults?.manifest?.maven?.javaHome ?? undefined;
12527
13140
  if (verbose) {
12528
13141
  logger.logger.group();
12529
13142
  logger.logger.info('- cwd:', cwd);
12530
13143
  logger.logger.info('- maven bin:', bin);
13144
+ logger.logger.info('- java home:', javaHome || '(inherited)');
12531
13145
  logger.logger.groupEnd();
12532
13146
  }
12533
13147
  if (dryRun) {
@@ -12544,6 +13158,7 @@ async function run$B(argv, importMeta, {
12544
13158
  excludePaths,
12545
13159
  ignoreUnresolved: Boolean(ignoreUnresolved),
12546
13160
  includeConfigs: String(includeConfigs || ''),
13161
+ javaHome,
12547
13162
  mavenOpts: parsedMavenOpts,
12548
13163
  verbose: Boolean(verbose)
12549
13164
  });
@@ -12575,7 +13190,7 @@ const config$9 = {
12575
13190
  type: 'string',
12576
13191
  description: 'When generating facts: comma-separated glob patterns; sbt configurations matching any pattern are skipped (applied after --include-configs)'
12577
13192
  },
12578
- ...excludePathsFlag,
13193
+ ...excludePathsFlag$1,
12579
13194
  ignoreUnresolved: {
12580
13195
  type: 'boolean',
12581
13196
  description: 'When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)'
@@ -12713,16 +13328,16 @@ async function run$A(argv, importMeta, {
12713
13328
  }
12714
13329
  }
12715
13330
  if (includeConfigs === undefined) {
12716
- if (sockJson.defaults?.manifest?.sbt?.includeConfigs !== undefined) {
12717
- includeConfigs = sockJson.defaults?.manifest?.sbt?.includeConfigs;
13331
+ if (sockJson.defaults?.manifest?.sbt?.includeConfigs) {
13332
+ includeConfigs = sockJson.defaults.manifest.sbt.includeConfigs;
12718
13333
  logger.logger.info(`Using default --include-configs from ${constants.SOCKET_JSON}:`, includeConfigs);
12719
13334
  } else {
12720
13335
  includeConfigs = '';
12721
13336
  }
12722
13337
  }
12723
13338
  if (excludeConfigs === undefined) {
12724
- if (sockJson.defaults?.manifest?.sbt?.excludeConfigs !== undefined) {
12725
- excludeConfigs = sockJson.defaults?.manifest?.sbt?.excludeConfigs;
13339
+ if (sockJson.defaults?.manifest?.sbt?.excludeConfigs) {
13340
+ excludeConfigs = sockJson.defaults.manifest.sbt.excludeConfigs;
12726
13341
  logger.logger.info(`Using default --exclude-configs from ${constants.SOCKET_JSON}:`, excludeConfigs);
12727
13342
  } else {
12728
13343
  excludeConfigs = '';
@@ -12803,11 +13418,13 @@ async function run$A(argv, importMeta, {
12803
13418
  if (!wasValidInput) {
12804
13419
  return;
12805
13420
  }
13421
+ const javaHome = sockJson.defaults?.manifest?.sbt?.javaHome ?? undefined;
12806
13422
  if (verbose) {
12807
13423
  logger.logger.group();
12808
13424
  logger.logger.log('- target:', cwd);
12809
13425
  logger.logger.log('- sbt bin:', bin);
12810
13426
  logger.logger.log('- out:', out);
13427
+ logger.logger.log('- java home:', javaHome || '(inherited)');
12811
13428
  logger.logger.groupEnd();
12812
13429
  }
12813
13430
  if (dryRun) {
@@ -12825,6 +13442,7 @@ async function run$A(argv, importMeta, {
12825
13442
  excludePaths,
12826
13443
  ignoreUnresolved: Boolean(ignoreUnresolved),
12827
13444
  includeConfigs: String(includeConfigs || ''),
13445
+ javaHome,
12828
13446
  sbtOpts: parsedSbtOpts,
12829
13447
  tmpDir,
12830
13448
  verbose: Boolean(verbose)
@@ -12980,7 +13598,7 @@ async function setupManifestConfig(cwd, defaultOnReadError = false) {
12980
13598
  }
12981
13599
  default:
12982
13600
  {
12983
- result = canceledByUser$1();
13601
+ result = canceledByUser$2();
12984
13602
  }
12985
13603
  }
12986
13604
  if (!result.ok || result.data.canceled) {
@@ -13003,12 +13621,12 @@ async function setupManifestConfig(cwd, defaultOnReadError = false) {
13003
13621
  })) {
13004
13622
  return await utils.writeSocketJson(cwd, sockJson);
13005
13623
  }
13006
- return canceledByUser$1();
13624
+ return canceledByUser$2();
13007
13625
  }
13008
13626
  async function setupConda(config) {
13009
13627
  const on = await askForEnabled(!config.disabled);
13010
13628
  if (on === undefined) {
13011
- return canceledByUser$1();
13629
+ return canceledByUser$2();
13012
13630
  } else if (on) {
13013
13631
  delete config.disabled;
13014
13632
  } else {
@@ -13016,7 +13634,7 @@ async function setupConda(config) {
13016
13634
  }
13017
13635
  const infile = await askForInputFile(config.infile || 'environment.yml');
13018
13636
  if (infile === undefined) {
13019
- return canceledByUser$1();
13637
+ return canceledByUser$2();
13020
13638
  } else if (infile === '-') {
13021
13639
  config.stdin = true;
13022
13640
  } else {
@@ -13029,7 +13647,7 @@ async function setupConda(config) {
13029
13647
  }
13030
13648
  const stdout = await askForStdout(config.stdout);
13031
13649
  if (stdout === undefined) {
13032
- return canceledByUser$1();
13650
+ return canceledByUser$2();
13033
13651
  } else if (stdout === 'yes') {
13034
13652
  config.stdout = true;
13035
13653
  } else if (stdout === 'no') {
@@ -13040,7 +13658,7 @@ async function setupConda(config) {
13040
13658
  if (!config.stdout) {
13041
13659
  const out = await askForOutputFile(config.outfile || constants.REQUIREMENTS_TXT);
13042
13660
  if (out === undefined) {
13043
- return canceledByUser$1();
13661
+ return canceledByUser$2();
13044
13662
  } else if (out === '-') {
13045
13663
  config.stdout = true;
13046
13664
  } else {
@@ -13054,23 +13672,43 @@ async function setupConda(config) {
13054
13672
  }
13055
13673
  const verbose = await askForVerboseFlag(config.verbose);
13056
13674
  if (verbose === undefined) {
13057
- return canceledByUser$1();
13675
+ return canceledByUser$2();
13058
13676
  } else if (verbose === 'yes' || verbose === 'no') {
13059
13677
  config.verbose = verbose === 'yes';
13060
13678
  } else {
13061
13679
  delete config.verbose;
13062
13680
  }
13063
- return notCanceled$1();
13681
+ return notCanceled$2();
13064
13682
  }
13065
- async function setupGradle(config) {
13066
- const bin = await askForBin(config.bin || './gradlew');
13683
+ async function setupGradle(config,
13684
+ // Set by the recursive dynamic-sbom-inference wizard, which has no
13685
+ // pom-mode equivalent - skips the facts/pom question entirely.
13686
+ {
13687
+ factsOnly = false
13688
+ } = {}) {
13689
+ const priorBin = config.bin;
13690
+ const bin = await askForBin(config.bin || '', './gradlew');
13067
13691
  if (bin === undefined) {
13068
- return canceledByUser$1();
13692
+ return canceledByUser$2();
13069
13693
  } else if (bin) {
13070
13694
  config.bin = bin;
13695
+ } else if (priorBin !== undefined) {
13696
+ config.bin = null;
13071
13697
  } else {
13072
13698
  delete config.bin;
13073
13699
  }
13700
+ const priorJavaHome = config.javaHome;
13701
+ const javaHome = await askForJavaHome(config.javaHome || '');
13702
+ if (javaHome === undefined) {
13703
+ return canceledByUser$2();
13704
+ } else if (javaHome) {
13705
+ config.javaHome = javaHome;
13706
+ } else if (priorJavaHome !== undefined) {
13707
+ config.javaHome = null;
13708
+ } else {
13709
+ delete config.javaHome;
13710
+ }
13711
+ const priorGradleOpts = config.gradleOpts;
13074
13712
  const opts = await prompts.input({
13075
13713
  message: '(--gradle-opts) Enter gradle options to pass through',
13076
13714
  default: config.gradleOpts || '',
@@ -13078,24 +13716,28 @@ async function setupGradle(config) {
13078
13716
  // validate: async string => bool
13079
13717
  });
13080
13718
  if (opts === undefined) {
13081
- return canceledByUser$1();
13719
+ return canceledByUser$2();
13082
13720
  } else if (opts) {
13083
13721
  config.gradleOpts = opts;
13722
+ } else if (priorGradleOpts !== undefined) {
13723
+ config.gradleOpts = null;
13084
13724
  } else {
13085
13725
  delete config.gradleOpts;
13086
13726
  }
13087
- const facts = await askForFactsFlag(config.facts);
13088
- if (facts === undefined) {
13089
- return canceledByUser$1();
13090
- } else if (facts === 'yes' || facts === 'no') {
13091
- config.facts = facts === 'yes';
13092
- } else {
13093
- delete config.facts;
13727
+ if (!factsOnly) {
13728
+ const facts = await askForFactsFlag(config.facts);
13729
+ if (facts === undefined) {
13730
+ return canceledByUser$2();
13731
+ } else if (facts === 'yes' || facts === 'no') {
13732
+ config.facts = facts === 'yes';
13733
+ } else {
13734
+ delete config.facts;
13735
+ }
13094
13736
  }
13095
13737
 
13096
13738
  // The config filters and --ignore-unresolved only apply to facts generation
13097
13739
  // (the default); skip them when pom generation (--pom) is selected.
13098
- if (config.facts !== false) {
13740
+ if (factsOnly || config.facts !== false) {
13099
13741
  const factsOptions = await setupFactsOptions(config);
13100
13742
  if (!factsOptions.ok || factsOptions.data.canceled) {
13101
13743
  return factsOptions;
@@ -13103,32 +13745,49 @@ async function setupGradle(config) {
13103
13745
  }
13104
13746
  const verbose = await askForVerboseFlag(config.verbose);
13105
13747
  if (verbose === undefined) {
13106
- return canceledByUser$1();
13748
+ return canceledByUser$2();
13107
13749
  } else if (verbose === 'yes' || verbose === 'no') {
13108
13750
  config.verbose = verbose === 'yes';
13109
13751
  } else {
13110
13752
  delete config.verbose;
13111
13753
  }
13112
- return notCanceled$1();
13754
+ return notCanceled$2();
13113
13755
  }
13114
13756
  async function setupMaven(config) {
13115
- const bin = await askForBin(config.bin || 'mvn');
13757
+ const priorBin = config.bin;
13758
+ const bin = await askForBin(config.bin || '', 'mvn');
13116
13759
  if (bin === undefined) {
13117
- return canceledByUser$1();
13760
+ return canceledByUser$2();
13118
13761
  } else if (bin) {
13119
13762
  config.bin = bin;
13763
+ } else if (priorBin !== undefined) {
13764
+ config.bin = null;
13120
13765
  } else {
13121
13766
  delete config.bin;
13122
13767
  }
13768
+ const priorJavaHome = config.javaHome;
13769
+ const javaHome = await askForJavaHome(config.javaHome || '');
13770
+ if (javaHome === undefined) {
13771
+ return canceledByUser$2();
13772
+ } else if (javaHome) {
13773
+ config.javaHome = javaHome;
13774
+ } else if (priorJavaHome !== undefined) {
13775
+ config.javaHome = null;
13776
+ } else {
13777
+ delete config.javaHome;
13778
+ }
13779
+ const priorMavenOpts = config.mavenOpts;
13123
13780
  const opts = await prompts.input({
13124
13781
  message: '(--maven-opts) Enter maven options to pass through',
13125
13782
  default: config.mavenOpts || '',
13126
13783
  required: false
13127
13784
  });
13128
13785
  if (opts === undefined) {
13129
- return canceledByUser$1();
13786
+ return canceledByUser$2();
13130
13787
  } else if (opts) {
13131
13788
  config.mavenOpts = opts;
13789
+ } else if (priorMavenOpts !== undefined) {
13790
+ config.mavenOpts = null;
13132
13791
  } else {
13133
13792
  delete config.mavenOpts;
13134
13793
  }
@@ -13141,23 +13800,42 @@ async function setupMaven(config) {
13141
13800
  }
13142
13801
  const verbose = await askForVerboseFlag(config.verbose);
13143
13802
  if (verbose === undefined) {
13144
- return canceledByUser$1();
13803
+ return canceledByUser$2();
13145
13804
  } else if (verbose === 'yes' || verbose === 'no') {
13146
13805
  config.verbose = verbose === 'yes';
13147
13806
  } else {
13148
13807
  delete config.verbose;
13149
13808
  }
13150
- return notCanceled$1();
13809
+ return notCanceled$2();
13151
13810
  }
13152
- async function setupSbt(config) {
13153
- const bin = await askForBin(config.bin || 'sbt');
13811
+ async function setupSbt(config,
13812
+ // See setupGradle's matching parameter for why.
13813
+ {
13814
+ factsOnly = false
13815
+ } = {}) {
13816
+ const priorBin = config.bin;
13817
+ const bin = await askForBin(config.bin || '', 'sbt');
13154
13818
  if (bin === undefined) {
13155
- return canceledByUser$1();
13819
+ return canceledByUser$2();
13156
13820
  } else if (bin) {
13157
13821
  config.bin = bin;
13822
+ } else if (priorBin !== undefined) {
13823
+ config.bin = null;
13158
13824
  } else {
13159
13825
  delete config.bin;
13160
13826
  }
13827
+ const priorJavaHome = config.javaHome;
13828
+ const javaHome = await askForJavaHome(config.javaHome || '');
13829
+ if (javaHome === undefined) {
13830
+ return canceledByUser$2();
13831
+ } else if (javaHome) {
13832
+ config.javaHome = javaHome;
13833
+ } else if (priorJavaHome !== undefined) {
13834
+ config.javaHome = null;
13835
+ } else {
13836
+ delete config.javaHome;
13837
+ }
13838
+ const priorSbtOpts = config.sbtOpts;
13161
13839
  const opts = await prompts.input({
13162
13840
  message: '(--sbt-opts) Enter sbt options to pass through',
13163
13841
  default: config.sbtOpts || '',
@@ -13165,28 +13843,32 @@ async function setupSbt(config) {
13165
13843
  // validate: async string => bool
13166
13844
  });
13167
13845
  if (opts === undefined) {
13168
- return canceledByUser$1();
13846
+ return canceledByUser$2();
13169
13847
  } else if (opts) {
13170
13848
  config.sbtOpts = opts;
13849
+ } else if (priorSbtOpts !== undefined) {
13850
+ config.sbtOpts = null;
13171
13851
  } else {
13172
13852
  delete config.sbtOpts;
13173
13853
  }
13174
- const facts = await askForFactsFlag(config.facts);
13175
- if (facts === undefined) {
13176
- return canceledByUser$1();
13177
- } else if (facts === 'yes' || facts === 'no') {
13178
- config.facts = facts === 'yes';
13179
- } else {
13180
- delete config.facts;
13854
+ if (!factsOnly) {
13855
+ const facts = await askForFactsFlag(config.facts);
13856
+ if (facts === undefined) {
13857
+ return canceledByUser$2();
13858
+ } else if (facts === 'yes' || facts === 'no') {
13859
+ config.facts = facts === 'yes';
13860
+ } else {
13861
+ delete config.facts;
13862
+ }
13181
13863
  }
13182
13864
 
13183
13865
  // Socket facts is the default. The pom output questions (stdout/outfile)
13184
13866
  // only apply when pom generation (--pom) is explicitly selected; otherwise
13185
13867
  // ask the facts-only options.
13186
- if (config.facts === false) {
13868
+ if (!factsOnly && config.facts === false) {
13187
13869
  const stdout = await askForStdout(config.stdout);
13188
13870
  if (stdout === undefined) {
13189
- return canceledByUser$1();
13871
+ return canceledByUser$2();
13190
13872
  } else if (stdout === 'yes') {
13191
13873
  config.stdout = true;
13192
13874
  } else if (stdout === 'no') {
@@ -13197,7 +13879,7 @@ async function setupSbt(config) {
13197
13879
  if (config.stdout !== true) {
13198
13880
  const out = await askForOutputFile(config.outfile || 'sbt.pom.xml');
13199
13881
  if (out === undefined) {
13200
- return canceledByUser$1();
13882
+ return canceledByUser$2();
13201
13883
  } else if (out === '-') {
13202
13884
  config.stdout = true;
13203
13885
  } else {
@@ -13217,13 +13899,13 @@ async function setupSbt(config) {
13217
13899
  }
13218
13900
  const verbose = await askForVerboseFlag(config.verbose);
13219
13901
  if (verbose === undefined) {
13220
- return canceledByUser$1();
13902
+ return canceledByUser$2();
13221
13903
  } else if (verbose === 'yes' || verbose === 'no') {
13222
13904
  config.verbose = verbose === 'yes';
13223
13905
  } else {
13224
13906
  delete config.verbose;
13225
13907
  }
13226
- return notCanceled$1();
13908
+ return notCanceled$2();
13227
13909
  }
13228
13910
  async function askForStdout(defaultValue) {
13229
13911
  return await prompts.select({
@@ -13279,14 +13961,25 @@ async function askForOutputFile(defaultName = '') {
13279
13961
  // validate: async string => bool
13280
13962
  });
13281
13963
  }
13282
- async function askForBin(defaultName = '') {
13964
+
13965
+ // `defaultName` must be a prior explicit/cascaded value, never the tool's
13966
+ // hardcoded fallback (e.g. `mvn`) - pre-filling that would freeze it into
13967
+ // socket.json on a bare Enter. `fallbackHint` is informational text only.
13968
+ async function askForBin(defaultName = '', fallbackHint = '') {
13283
13969
  return await prompts.input({
13284
- message: '(--bin) What should be the command to execute? Usually your build binary.' + (defaultName ? ' (Backspace to leave default)' : ''),
13970
+ message: '(--bin) What should be the command to execute? Usually your build binary.' + (defaultName ? ' (Backspace to leave default)' : fallbackHint ? ` (blank = ${fallbackHint})` : ''),
13285
13971
  default: defaultName,
13286
13972
  required: false
13287
13973
  // validate: async string => bool
13288
13974
  });
13289
13975
  }
13976
+ async function askForJavaHome(defaultName = '') {
13977
+ return await prompts.input({
13978
+ message: 'What JDK should this build tool use? Leave blank to use the JDK already on PATH/JAVA_HOME. Supports $VAR/${VAR}.' + (defaultName ? ' (Backspace to leave default)' : ''),
13979
+ default: defaultName,
13980
+ required: false
13981
+ });
13982
+ }
13290
13983
  async function askForVerboseFlag(current) {
13291
13984
  return await prompts.select({
13292
13985
  message: '(--verbose) Should this run in verbose mode by default?',
@@ -13348,62 +14041,897 @@ async function askForIgnoreUnresolvedFlag(current) {
13348
14041
  // Prompts for the facts-only options shared by gradle and sbt: the config
13349
14042
  // include/exclude filters and --ignore-unresolved. Mutates `config` in place.
13350
14043
  async function setupFactsOptions(config) {
14044
+ const priorIncludeConfigs = config.includeConfigs;
13351
14045
  const includeConfigs = await prompts.input({
13352
14046
  message: '(--include-configs) Comma-separated config-name globs to resolve (blank = all configurations)',
13353
14047
  default: config.includeConfigs || '',
13354
14048
  required: false
13355
14049
  });
13356
14050
  if (includeConfigs === undefined) {
13357
- return canceledByUser$1();
14051
+ return canceledByUser$2();
13358
14052
  } else if (includeConfigs) {
13359
14053
  config.includeConfigs = includeConfigs;
14054
+ } else if (priorIncludeConfigs !== undefined) {
14055
+ // Was previously set; clear it explicitly instead of just deleting the
14056
+ // key, so it doesn't silently start inheriting an ancestor's value again.
14057
+ config.includeConfigs = null;
13360
14058
  } else {
13361
14059
  delete config.includeConfigs;
13362
14060
  }
14061
+ const priorExcludeConfigs = config.excludeConfigs;
13363
14062
  const excludeConfigs = await prompts.input({
13364
14063
  message: '(--exclude-configs) Comma-separated config-name globs to skip (blank = none)',
13365
14064
  default: config.excludeConfigs || '',
13366
14065
  required: false
13367
14066
  });
13368
- if (excludeConfigs === undefined) {
13369
- return canceledByUser$1();
13370
- } else if (excludeConfigs) {
13371
- config.excludeConfigs = excludeConfigs;
13372
- } else {
13373
- delete config.excludeConfigs;
14067
+ if (excludeConfigs === undefined) {
14068
+ return canceledByUser$2();
14069
+ } else if (excludeConfigs) {
14070
+ config.excludeConfigs = excludeConfigs;
14071
+ } else if (priorExcludeConfigs !== undefined) {
14072
+ config.excludeConfigs = null;
14073
+ } else {
14074
+ delete config.excludeConfigs;
14075
+ }
14076
+ const ignoreUnresolved = await askForIgnoreUnresolvedFlag(config.ignoreUnresolved);
14077
+ if (ignoreUnresolved === undefined) {
14078
+ return canceledByUser$2();
14079
+ } else if (ignoreUnresolved === 'yes' || ignoreUnresolved === 'no') {
14080
+ config.ignoreUnresolved = ignoreUnresolved === 'yes';
14081
+ } else {
14082
+ delete config.ignoreUnresolved;
14083
+ }
14084
+ return notCanceled$2();
14085
+ }
14086
+ function canceledByUser$2() {
14087
+ logger.logger.log('');
14088
+ logger.logger.info('User canceled');
14089
+ logger.logger.log('');
14090
+ return {
14091
+ ok: true,
14092
+ data: {
14093
+ canceled: true
14094
+ }
14095
+ };
14096
+ }
14097
+ function notCanceled$2() {
14098
+ return {
14099
+ ok: true,
14100
+ data: {
14101
+ canceled: false
14102
+ }
14103
+ };
14104
+ }
14105
+
14106
+ // Cheap subproject discovery (no dependency resolution) for
14107
+ // `socket manifest setup --dynamic-sbom-inference`; dynamic-sbom-inference
14108
+ // itself instead gets this list for free as a side effect of its own full
14109
+ // facts run.
14110
+ async function enumerateWorkspaces({
14111
+ bin,
14112
+ buildOpts,
14113
+ cwd,
14114
+ ecosystem,
14115
+ excludePaths,
14116
+ javaHome,
14117
+ verbose
14118
+ }) {
14119
+ let resolvedJavaHome;
14120
+ if (javaHome) {
14121
+ const expanded = expandEnvVarRefs(javaHome);
14122
+ if (expanded.missing) {
14123
+ process.exitCode = 1;
14124
+ logger.logger.fail(`javaHome (\`${javaHome}\`) ${formatMissingEnvVarRefs(expanded.missing)}.`);
14125
+ return;
14126
+ }
14127
+ resolvedJavaHome = expanded.value;
14128
+ }
14129
+ const scriptOpts = {
14130
+ bin: bin || undefined,
14131
+ excludePaths: excludePaths?.length ? excludePaths : undefined,
14132
+ // `env` replaces the spawned process's whole environment, not just JAVA_HOME.
14133
+ env: resolvedJavaHome ? {
14134
+ ...process.env,
14135
+ JAVA_HOME: resolvedJavaHome
14136
+ } : undefined,
14137
+ projectDir: cwd,
14138
+ stdio: 'pipe',
14139
+ toolOpts: buildOpts
14140
+ };
14141
+ let result;
14142
+ try {
14143
+ result = await enumerateWorkspaces$1(ecosystem, scriptOpts);
14144
+ } catch (e) {
14145
+ process.exitCode = 1;
14146
+ logger.logger.fail(`Could not run the ${ecosystem} build tool` + (' (run with --verbose for details).'));
14147
+ return;
14148
+ }
14149
+ if (!result.projects.length) {
14150
+ process.exitCode = 1;
14151
+ logger.logger.fail(`The ${ecosystem} build produced no workspace records (exit code ${result.code}); every build reports at least its own root project, so the enumeration task did not run.`);
14152
+ return;
14153
+ }
14154
+ return {
14155
+ projects: result.projects
14156
+ };
14157
+ }
14158
+
14159
+ // A single discovered build root - a directory with its own gradle/sbt/maven
14160
+ // marker file.
14161
+
14162
+ const ROOT_ECOSYSTEMS = ['maven', 'gradle', 'sbt'];
14163
+ const ECOSYSTEM_LABELS = {
14164
+ __proto__: null,
14165
+ gradle: 'Gradle',
14166
+ maven: 'Maven',
14167
+ sbt: 'sbt'
14168
+ };
14169
+
14170
+ // The shallowest directory matching `--exclude-paths`, not necessarily a
14171
+ // project dir itself - one write here covers every project beneath it.
14172
+
14173
+ function canceledByUser$1() {
14174
+ logger.logger.log('');
14175
+ logger.logger.info('User canceled');
14176
+ logger.logger.log('');
14177
+ return {
14178
+ ok: true,
14179
+ data: {
14180
+ canceled: true
14181
+ }
14182
+ };
14183
+ }
14184
+ function notCanceled$1() {
14185
+ return {
14186
+ ok: true,
14187
+ data: {
14188
+ canceled: false
14189
+ }
14190
+ };
14191
+ }
14192
+ function getEcosystemSection(sockJson, ecosystem) {
14193
+ return sockJson.defaults?.manifest?.[ecosystem] ?? {};
14194
+ }
14195
+
14196
+ // Depth-then-path sort: for exclusion roots it's just stable log ordering;
14197
+ // for candidates it's load-bearing, since a parent must be prompted before
14198
+ // its children so a child's shown default reflects a parent's just-written
14199
+ // cascade instead of its pre-run value.
14200
+ function sortCandidatesForDisplay(candidates, cwd) {
14201
+ return [...candidates].sort((a, b) => {
14202
+ const relA = path.relative(cwd, a.dir);
14203
+ const relB = path.relative(cwd, b.dir);
14204
+ const depthA = relA.split(path.sep).length;
14205
+ const depthB = relB.split(path.sep).length;
14206
+ if (depthA !== depthB) {
14207
+ return depthA - depthB;
14208
+ }
14209
+ return relA < relB ? -1 : relA > relB ? 1 : 0;
14210
+ });
14211
+ }
14212
+ function toPosixRelative(cwd, dir) {
14213
+ return path.relative(cwd, dir).split(path.sep).join('/');
14214
+ }
14215
+
14216
+ // Shallowest prefix of `relDir` that matches an --exclude-paths pattern -
14217
+ // the directory to write `disabled` to, so one write covers everything
14218
+ // beneath it, even if that directory isn't itself a build root.
14219
+ function findExclusionRoot(relDir, ignorePatterns) {
14220
+ const segments = relDir.split('/');
14221
+ for (let depth = 1; depth <= segments.length; depth += 1) {
14222
+ const prefix = segments.slice(0, depth).join('/');
14223
+ if (vendor.micromatchExports.isMatch(prefix, ignorePatterns, {
14224
+ dot: true
14225
+ })) {
14226
+ return prefix;
14227
+ }
14228
+ }
14229
+ return relDir;
14230
+ }
14231
+ // Both the unfiltered and --exclude-paths-filtered scans, once, so ecosystem
14232
+ // detection and later include/exclude grouping (see discoverBuildRoots)
14233
+ // never re-walk the same tree.
14234
+ async function scanBuildRoots({
14235
+ cwd,
14236
+ excludePaths,
14237
+ sockJson
14238
+ }) {
14239
+ const [fullByTool, includedByTool] = await Promise.all([findBuildToolCandidates({
14240
+ cwd,
14241
+ sockJson
14242
+ }), findBuildToolCandidates({
14243
+ cwd,
14244
+ excludePaths,
14245
+ sockJson
14246
+ })]);
14247
+ return {
14248
+ fullByTool,
14249
+ includedByTool
14250
+ };
14251
+ }
14252
+ // Splits an already-scanned candidate set into `included` (the interactive
14253
+ // walk) and `excluded`, grouped by the shallowest --exclude-paths match (see
14254
+ // findExclusionRoot). `cwd` is excluded from both (it got its own wizard
14255
+ // pass) and realpath-resolved to match findBuildToolCandidates' own
14256
+ // resolution - otherwise macOS's /tmp -> /private/tmp breaks the comparison.
14257
+ async function discoverBuildRoots({
14258
+ cwd,
14259
+ excludePaths,
14260
+ fullByTool,
14261
+ includedByTool
14262
+ }) {
14263
+ const realCwd = await realpathOrResolved(cwd);
14264
+ const included = [];
14265
+ const ignorePatterns = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
14266
+ const ecosystemsByRoot = new Map();
14267
+ for (const [ecosystem, fullDirs] of fullByTool) {
14268
+ const includedDirs = new Set(includedByTool.get(ecosystem) ?? []);
14269
+ for (const dir of fullDirs) {
14270
+ if (dir === realCwd) {
14271
+ continue;
14272
+ }
14273
+ if (includedDirs.has(dir)) {
14274
+ included.push({
14275
+ dir,
14276
+ ecosystem
14277
+ });
14278
+ continue;
14279
+ }
14280
+ const relDir = toPosixRelative(realCwd, dir);
14281
+ const rootRelDir = findExclusionRoot(relDir, ignorePatterns);
14282
+ const rootDir = path.join(realCwd, rootRelDir);
14283
+ const ecosystems = ecosystemsByRoot.get(rootDir) ?? new Set();
14284
+ ecosystems.add(ecosystem);
14285
+ ecosystemsByRoot.set(rootDir, ecosystems);
14286
+ }
14287
+ }
14288
+ return {
14289
+ excluded: [...ecosystemsByRoot].map(([dir, ecosystems]) => ({
14290
+ dir,
14291
+ ecosystems: [...ecosystems].sort()
14292
+ })),
14293
+ included
14294
+ };
14295
+ }
14296
+
14297
+ // Enumerates one build root's declared workspace members (see
14298
+ // enumerate-workspaces.mts) and folds them into `coveredByEcosystem`, so a
14299
+ // later candidate matching one is recognized as a reactor member rather than
14300
+ // independent. Called per-candidate right after its own prompt, not as a
14301
+ // bulk pass, so the build invocation never blocks candidates that don't need
14302
+ // it. A disabled candidate is skipped (no point invoking an off build tool);
14303
+ // otherwise this fails closed, same reasoning as generateRecursiveManifests -
14304
+ // if enumeration fails there's no way to tell covered from independent, so
14305
+ // the caller aborts rather than guess.
14306
+ async function markWorkspaceCoverage({
14307
+ candidate,
14308
+ coveredByEcosystem,
14309
+ cwd,
14310
+ excludePaths,
14311
+ rootSockJson
14312
+ }) {
14313
+ const cascade = utils.readSocketJsonCascade(candidate.dir, cwd, rootSockJson);
14314
+ const {
14315
+ bin,
14316
+ buildOpts,
14317
+ javaHome,
14318
+ skipReason
14319
+ } = resolveEcosystemConfig(candidate.ecosystem, candidate.dir, cascade);
14320
+ if (skipReason) {
14321
+ return {
14322
+ ok: true,
14323
+ data: undefined
14324
+ };
14325
+ }
14326
+ const excludePathsForCandidate = projectIgnorePathsToReachExcludePaths(excludePaths, {
14327
+ cwd,
14328
+ target: candidate.dir
14329
+ });
14330
+ const enumResult = await enumerateWorkspaces({
14331
+ bin,
14332
+ buildOpts,
14333
+ cwd: candidate.dir,
14334
+ ecosystem: candidate.ecosystem,
14335
+ excludePaths: excludePathsForCandidate,
14336
+ javaHome,
14337
+ verbose: false
14338
+ });
14339
+ if (!enumResult) {
14340
+ const relDir = path.relative(cwd, candidate.dir) || '.';
14341
+ return {
14342
+ ok: false,
14343
+ message: `Could not determine ${candidate.ecosystem} workspace layout for ${relDir}; aborting rather than risk misclassifying its members.`
14344
+ };
14345
+ }
14346
+ const set = coveredByEcosystem.get(candidate.ecosystem) ?? new Set();
14347
+ set.add(candidate.dir);
14348
+ const resolvedSubprojectDirs = await Promise.all(enumResult.projects.map(project => realpathOrResolved(path.resolve(candidate.dir, project.subprojectDir))));
14349
+ for (const subprojectDir of resolvedSubprojectDirs) {
14350
+ set.add(subprojectDir);
14351
+ }
14352
+ coveredByEcosystem.set(candidate.ecosystem, set);
14353
+ return {
14354
+ ok: true,
14355
+ data: undefined
14356
+ };
14357
+ }
14358
+
14359
+ // Marks one exclusion root's own socket.json `disabled: true` for whichever
14360
+ // of its ecosystems aren't already disabled via cascade (an already-disabled
14361
+ // ancestor from a prior run) - a no-op write is skipped entirely so re-running
14362
+ // the wizard doesn't keep rewriting already-disabled roots.
14363
+ async function disableExclusionRoot({
14364
+ cwd,
14365
+ dir,
14366
+ ecosystems,
14367
+ rootSockJson
14368
+ }) {
14369
+ const relDir = path.relative(cwd, dir) || '.';
14370
+ const cascade = utils.readSocketJsonCascade(dir, cwd, rootSockJson);
14371
+ const needsWrite = ecosystems.filter(ecosystem => getEcosystemSection(cascade, ecosystem)['disabled'] !== true);
14372
+ if (!needsWrite.length) {
14373
+ return notCanceled$1();
14374
+ }
14375
+ const ownSockJson = utils.readOrDefaultSocketJson(dir);
14376
+ if (!ownSockJson.defaults) {
14377
+ ownSockJson.defaults = {};
14378
+ }
14379
+ if (!ownSockJson.defaults.manifest) {
14380
+ ownSockJson.defaults.manifest = {};
14381
+ }
14382
+ const manifest = ownSockJson.defaults.manifest;
14383
+ for (const ecosystem of needsWrite) {
14384
+ manifest[ecosystem] = {
14385
+ ...getEcosystemSection(ownSockJson, ecosystem),
14386
+ disabled: true
14387
+ };
14388
+ }
14389
+ const writeResult = await utils.writeSocketJson(dir, ownSockJson);
14390
+ if (!writeResult.ok) {
14391
+ return writeResult;
14392
+ }
14393
+ logger.logger.success(`Disabled ${relDir} (${needsWrite.join(', ')})`);
14394
+ return notCanceled$1();
14395
+ }
14396
+
14397
+ // Dispatches to the right ecosystem-specific wizard. `factsOnly` skips the
14398
+ // facts/pom question - dynamic-sbom-inference never generates a pom.xml, it
14399
+ // just skips a project resolved to `facts: false` (see
14400
+ // generateRecursiveManifests' skipReason), so offering that choice here
14401
+ // would be misleading.
14402
+ async function runEcosystemWizard(ecosystem, config) {
14403
+ if (ecosystem === 'gradle') {
14404
+ return await setupGradle(config, {
14405
+ factsOnly: true
14406
+ });
14407
+ }
14408
+ if (ecosystem === 'maven') {
14409
+ return await setupMaven(config);
14410
+ }
14411
+ return await setupSbt(config, {
14412
+ factsOnly: true
14413
+ });
14414
+ }
14415
+
14416
+ // Seeds the wizard with this candidate's effective (cascaded) value for any
14417
+ // field its own file doesn't already set, so accepting every prompt
14418
+ // unchanged preserves what it currently inherits, while an actual change
14419
+ // writes an explicit override.
14420
+ async function configureCandidate({
14421
+ cwd,
14422
+ dir,
14423
+ ecosystem,
14424
+ rootSockJson
14425
+ }) {
14426
+ const relDir = path.relative(cwd, dir) || '.';
14427
+ const ownSockJson = utils.readOrDefaultSocketJson(dir);
14428
+ if (!ownSockJson.defaults) {
14429
+ ownSockJson.defaults = {};
14430
+ }
14431
+ if (!ownSockJson.defaults.manifest) {
14432
+ ownSockJson.defaults.manifest = {};
14433
+ }
14434
+
14435
+ // Ancestor-only baseline (excludes dir's own file, unlike the cascade below)
14436
+ // so a field left exactly as shown can be told apart from one the user
14437
+ // actually set - otherwise every accepted default gets written verbatim,
14438
+ // permanently pinning it against future changes to an ancestor's value.
14439
+ const inherited = getEcosystemSection(utils.readSocketJsonCascade(path.dirname(dir), cwd, rootSockJson), ecosystem);
14440
+ const cascade = utils.readSocketJsonCascade(dir, cwd, rootSockJson);
14441
+ const seed = {
14442
+ ...getEcosystemSection(cascade, ecosystem),
14443
+ ...getEcosystemSection(ownSockJson, ecosystem)
14444
+ };
14445
+ const result = await runEcosystemWizard(ecosystem, seed);
14446
+ if (!result.ok || result.data.canceled) {
14447
+ return result;
14448
+ }
14449
+ const toWrite = {};
14450
+ for (const key of Object.keys(seed)) {
14451
+ if (seed[key] !== inherited[key]) {
14452
+ toWrite[key] = seed[key];
14453
+ }
14454
+ }
14455
+ // Every field equals what dir would already inherit - writing it would just
14456
+ // be noise (own file unaffected, dir keeps inheriting exactly as before).
14457
+ if (!Object.keys(toWrite).length) {
14458
+ logger.logger.log(`No changes for ${relDir} (${ecosystem}); nothing written.`);
14459
+ return notCanceled$1();
14460
+ }
14461
+ const manifest = ownSockJson.defaults.manifest;
14462
+ manifest[ecosystem] = toWrite;
14463
+ const writeResult = await utils.writeSocketJson(dir, ownSockJson);
14464
+ if (!writeResult.ok) {
14465
+ return writeResult;
14466
+ }
14467
+ logger.logger.success(`Configured ${relDir} (${ecosystem})`);
14468
+ return notCanceled$1();
14469
+ }
14470
+ // No disable choice here - that's --exclude-paths' job (see
14471
+ // findExclusionRoot), so a whole subtree still collapses to one write
14472
+ // instead of one per candidate.
14473
+ async function askCandidateAction(relDir, ecosystem) {
14474
+ return await prompts.select({
14475
+ message: `${relDir} (${ecosystem})`,
14476
+ choices: [{
14477
+ name: 'Leave as-is',
14478
+ value: 'inherit',
14479
+ description: "Make no change - keep this project's current effective configuration"
14480
+ }, {
14481
+ name: 'Configure',
14482
+ value: 'configure',
14483
+ description: 'Set bin/JDK/opts/etc. for this project specifically'
14484
+ }],
14485
+ default: 'inherit'
14486
+ });
14487
+ }
14488
+ // Asks the standard configure-or-leave-as-is question and applies the
14489
+ // answer. Shared by the normal path and the post-re-enable path below, so
14490
+ // re-enabling a candidate isn't a dead end that skips straight past its own
14491
+ // bin/JDK/opts configuration.
14492
+ async function askAndApplyAction({
14493
+ cwd,
14494
+ dir,
14495
+ ecosystem,
14496
+ relDir,
14497
+ rootSockJson
14498
+ }) {
14499
+ const action = await askCandidateAction(relDir, ecosystem);
14500
+ if (action === undefined || action === null) {
14501
+ canceledByUser$1();
14502
+ return {
14503
+ ok: true,
14504
+ data: {
14505
+ canceled: true,
14506
+ outcome: 'inherited'
14507
+ }
14508
+ };
14509
+ }
14510
+ if (action === 'configure') {
14511
+ const result = await configureCandidate({
14512
+ cwd,
14513
+ dir,
14514
+ ecosystem,
14515
+ rootSockJson
14516
+ });
14517
+ if (!result.ok) {
14518
+ return result;
14519
+ }
14520
+ return {
14521
+ ok: true,
14522
+ data: {
14523
+ ...result.data,
14524
+ outcome: 'configured'
14525
+ }
14526
+ };
14527
+ }
14528
+ // 'inherit', or any unexpected value - the safe no-op default.
14529
+ return {
14530
+ ok: true,
14531
+ data: {
14532
+ canceled: false,
14533
+ outcome: 'inherited'
14534
+ }
14535
+ };
14536
+ }
14537
+
14538
+ // Prompts for configure/inherit, unless the cascade shows the candidate
14539
+ // disabled: offer to re-enable if that's its own file, then continue into
14540
+ // the normal prompt; stay silent if it's only inherited from an ancestor's
14541
+ // bulk write (that's noise for a large excluded subtree).
14542
+ async function processCandidate({
14543
+ cwd,
14544
+ dir,
14545
+ ecosystem,
14546
+ rootSockJson
14547
+ }) {
14548
+ const relDir = path.relative(cwd, dir) || '.';
14549
+ const cascade = utils.readSocketJsonCascade(dir, cwd, rootSockJson);
14550
+ if (getEcosystemSection(cascade, ecosystem)['disabled'] === true) {
14551
+ const ownSockJson = utils.readOrDefaultSocketJson(dir);
14552
+ if (getEcosystemSection(ownSockJson, ecosystem)['disabled'] !== true) {
14553
+ // Inherited from an ancestor, not this candidate's own file - stay silent.
14554
+ return {
14555
+ ok: true,
14556
+ data: {
14557
+ canceled: false,
14558
+ outcome: 'skipped',
14559
+ reenabled: false
14560
+ }
14561
+ };
14562
+ }
14563
+ const wantsReenable = await askYesNo(`${relDir} (${ecosystem}) is disabled - re-enable it?`);
14564
+ if (wantsReenable === undefined || wantsReenable === null) {
14565
+ canceledByUser$1();
14566
+ return {
14567
+ ok: true,
14568
+ data: {
14569
+ canceled: true,
14570
+ outcome: 'skipped',
14571
+ reenabled: false
14572
+ }
14573
+ };
14574
+ }
14575
+ if (!wantsReenable) {
14576
+ return {
14577
+ ok: true,
14578
+ data: {
14579
+ canceled: false,
14580
+ outcome: 'skipped',
14581
+ reenabled: false
14582
+ }
14583
+ };
14584
+ }
14585
+ if (!ownSockJson.defaults) {
14586
+ ownSockJson.defaults = {};
14587
+ }
14588
+ if (!ownSockJson.defaults.manifest) {
14589
+ ownSockJson.defaults.manifest = {};
14590
+ }
14591
+ const manifest = ownSockJson.defaults.manifest;
14592
+ manifest[ecosystem] = {
14593
+ ...getEcosystemSection(ownSockJson, ecosystem),
14594
+ disabled: false
14595
+ };
14596
+ const writeResult = await utils.writeSocketJson(dir, ownSockJson);
14597
+ if (!writeResult.ok) {
14598
+ return writeResult;
14599
+ }
14600
+ logger.logger.success(`Re-enabled ${relDir} (${ecosystem})`);
14601
+ const result = await askAndApplyAction({
14602
+ cwd,
14603
+ dir,
14604
+ ecosystem,
14605
+ relDir,
14606
+ rootSockJson
14607
+ });
14608
+ if (!result.ok) {
14609
+ return result;
14610
+ }
14611
+ return {
14612
+ ok: true,
14613
+ data: {
14614
+ ...result.data,
14615
+ reenabled: true
14616
+ }
14617
+ };
14618
+ }
14619
+ const result = await askAndApplyAction({
14620
+ cwd,
14621
+ dir,
14622
+ ecosystem,
14623
+ relDir,
14624
+ rootSockJson
14625
+ });
14626
+ if (!result.ok) {
14627
+ return result;
14628
+ }
14629
+ return {
14630
+ ok: true,
14631
+ data: {
14632
+ ...result.data,
14633
+ reenabled: false
14634
+ }
14635
+ };
14636
+ }
14637
+
14638
+ // Accepting every prompt's default leaves an ecosystem's section genuinely
14639
+ // empty (see askForBin in setup-manifest-config.mts) - drop it so
14640
+ // `configuredAny` reflects what was actually configured, not just which
14641
+ // ecosystems the user said "yes" to.
14642
+ function dropIfEmpty(manifest, ecosystem) {
14643
+ const section = manifest[ecosystem];
14644
+ if (section && Object.keys(section).length) {
14645
+ return true;
14646
+ }
14647
+ delete manifest[ecosystem];
14648
+ return false;
14649
+ }
14650
+ async function askYesNo(message) {
14651
+ return await prompts.select({
14652
+ message,
14653
+ choices: [{
14654
+ name: 'Yes',
14655
+ value: true
14656
+ }, {
14657
+ name: 'No',
14658
+ value: false
14659
+ }]
14660
+ });
14661
+ }
14662
+
14663
+ // Unlike the plain single-project wizard, cwd here is often just a common
14664
+ // ancestor - so `ecosystemsAtCwd` (a subset of `ecosystems`) picks the
14665
+ // wording: cwd's own build root gets "configure this project's settings",
14666
+ // everything else gets "configure defaults for nested projects". A
14667
+ // pre-existing `disabled: true` gets a re-enable question first, otherwise
14668
+ // there'd be no way back from a prior disable. Declining everything is
14669
+ // normal, not an abort.
14670
+ async function setupRecursiveRootDefaults(cwd, defaultOnReadError, ecosystems, ecosystemsAtCwd) {
14671
+ const jsonPath = path.join(cwd, constants.SOCKET_JSON);
14672
+ if (fs$1.existsSync(jsonPath)) {
14673
+ logger.logger.info(`Found ${constants.SOCKET_JSON} at ${jsonPath}`);
14674
+ } else {
14675
+ logger.logger.info(`No ${constants.SOCKET_JSON} found at ${cwd}, will generate a new one`);
14676
+ }
14677
+ logger.logger.log('');
14678
+ logger.logger.log('Note: This tool will set up flag and argument defaults for certain');
14679
+ logger.logger.log(' CLI commands. You can still override them by explicitly');
14680
+ logger.logger.log(' setting the flag.');
14681
+ logger.logger.log('');
14682
+ logger.logger.log(`This command will generate a ${constants.SOCKET_JSON} file in ${cwd}.`);
14683
+ logger.logger.log('socket.json properties are inherited by nested paths.');
14684
+ logger.logger.log('');
14685
+ const sockJsonCResult = utils.readSocketJsonSync(cwd, defaultOnReadError);
14686
+ if (!sockJsonCResult.ok) {
14687
+ return sockJsonCResult;
14688
+ }
14689
+ const sockJson = sockJsonCResult.data;
14690
+ if (!sockJson.defaults) {
14691
+ sockJson.defaults = {};
14692
+ }
14693
+ if (!sockJson.defaults.manifest) {
14694
+ sockJson.defaults.manifest = {};
14695
+ }
14696
+ const manifest = sockJson.defaults.manifest;
14697
+ let configuredAny = false;
14698
+ for (const ecosystem of ecosystems) {
14699
+ const label = ECOSYSTEM_LABELS[ecosystem];
14700
+ const existingSection = manifest[ecosystem];
14701
+ if (existingSection?.['disabled'] === true) {
14702
+ // eslint-disable-next-line no-await-in-loop
14703
+ const wantsReenable = await askYesNo(`${label} is currently disabled here - re-enable it?`);
14704
+ if (wantsReenable === undefined || wantsReenable === null) {
14705
+ return canceledByUser$1();
14706
+ }
14707
+ if (!wantsReenable) {
14708
+ continue;
14709
+ }
14710
+ existingSection['disabled'] = false;
14711
+ configuredAny = true;
14712
+ }
14713
+ const message = ecosystemsAtCwd.includes(ecosystem) ? `Configure ${label} settings for this project?` : `Configure ${label} defaults for any nested projects?`;
14714
+ // eslint-disable-next-line no-await-in-loop
14715
+ const wants = await askYesNo(message);
14716
+ if (wants === undefined || wants === null) {
14717
+ return canceledByUser$1();
14718
+ }
14719
+ if (!wants) {
14720
+ continue;
14721
+ }
14722
+ if (!manifest[ecosystem]) {
14723
+ manifest[ecosystem] = {};
14724
+ }
14725
+ // eslint-disable-next-line no-await-in-loop
14726
+ const result = await runEcosystemWizard(ecosystem, manifest[ecosystem]);
14727
+ if (!result.ok || result.data.canceled) {
14728
+ return result;
14729
+ }
14730
+ if (dropIfEmpty(manifest, ecosystem)) {
14731
+ configuredAny = true;
14732
+ }
14733
+ }
14734
+ if (!configuredAny) {
14735
+ logger.logger.log('');
14736
+ logger.logger.log('No root-level defaults configured.');
14737
+ return notCanceled$1();
14738
+ }
14739
+ logger.logger.log('');
14740
+ logger.logger.log(`Writing ${constants.SOCKET_JSON} to ${jsonPath}`);
14741
+ logger.logger.log('');
14742
+ const writeResult = await utils.writeSocketJson(cwd, sockJson);
14743
+ if (!writeResult.ok) {
14744
+ return writeResult;
14745
+ }
14746
+ return notCanceled$1();
14747
+ }
14748
+
14749
+ // `socket manifest setup --dynamic-sbom-inference`: the up-front scan (see
14750
+ // scanBuildRoots) only ever answers "which build tools exist", never "which
14751
+ // projects to configure". `--exclude-paths` matches are bulk-disabled
14752
+ // unconditionally, independent of the interactive walk below, so re-running
14753
+ // later with one more entry (or none) never re-answers settled decisions.
14754
+ // Everything else gets a configure-or-inherit prompt (see processCandidate),
14755
+ // except a workspace member a parent build already declares - learned via
14756
+ // markWorkspaceCoverage right after the parent's own prompt, not a separate
14757
+ // bulk pass, so a reactor member is silently skipped instead of prompted.
14758
+ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePaths) {
14759
+ logger.logger.log('');
14760
+ logger.logger.log(`Configuring ${cwd} ...`);
14761
+ const initialSockJson = utils.readOrDefaultSocketJson(cwd);
14762
+ // Resolved once here for sortCandidatesForDisplay/disableExclusionRoot's
14763
+ // relative-path math, consistent with discoverBuildRoots' own internal
14764
+ // resolution (see its comment for why this matters).
14765
+ const realCwd = await realpathOrResolved(cwd);
14766
+ logger.logger.log('');
14767
+ logger.logger.log('Scanning for build roots ...');
14768
+ const {
14769
+ fullByTool,
14770
+ includedByTool
14771
+ } = await scanBuildRoots({
14772
+ cwd,
14773
+ excludePaths,
14774
+ sockJson: withoutDisabledFlags(initialSockJson)
14775
+ });
14776
+ // includedByTool (not the unfiltered fullByTool), so an ecosystem whose
14777
+ // only candidate is about to be excluded this same run isn't offered
14778
+ // either - there'd be nothing left for its root defaults to apply to.
14779
+ const detectedEcosystems = ROOT_ECOSYSTEMS.filter(ecosystem => (includedByTool.get(ecosystem)?.length ?? 0) > 0);
14780
+ if (detectedEcosystems.length) {
14781
+ logger.logger.log(`Detected: ${detectedEcosystems.map(ecosystem => ECOSYSTEM_LABELS[ecosystem]).join(', ')}.`);
14782
+ } else {
14783
+ logger.logger.log(`No gradle/maven/sbt build roots found beneath ${cwd}.`);
14784
+ }
14785
+ logger.logger.log('');
14786
+
14787
+ // Which detected ecosystems cwd is itself a build root for - see
14788
+ // setupRecursiveRootDefaults' wording split.
14789
+ const ecosystemsAtCwd = ROOT_ECOSYSTEMS.filter(ecosystem => (fullByTool.get(ecosystem) ?? []).includes(realCwd));
14790
+ const rootResult = await setupRecursiveRootDefaults(cwd, defaultOnReadError, detectedEcosystems, ecosystemsAtCwd);
14791
+ if (!rootResult.ok) {
14792
+ return rootResult;
14793
+ }
14794
+ if (rootResult.data.canceled) {
14795
+ return canceledByUser$1();
14796
+ }
14797
+
14798
+ // Re-read: the root wizard may have just written a new socket.json.
14799
+ const rootSockJson = utils.readOrDefaultSocketJson(cwd);
14800
+ const {
14801
+ excluded,
14802
+ included
14803
+ } = await discoverBuildRoots({
14804
+ cwd,
14805
+ excludePaths,
14806
+ fullByTool,
14807
+ includedByTool
14808
+ });
14809
+
14810
+ // Applies regardless of the interactive walk below - see the module doc
14811
+ // comment on why --exclude-paths is unconditional.
14812
+ if (excludePaths?.length) {
14813
+ if (!excluded.length) {
14814
+ logger.logger.log('None matched --exclude-paths; nothing disabled.');
14815
+ } else {
14816
+ const orderedExcluded = sortCandidatesForDisplay(excluded, realCwd);
14817
+ for (const candidate of orderedExcluded) {
14818
+ // eslint-disable-next-line no-await-in-loop
14819
+ const result = await disableExclusionRoot({
14820
+ cwd: realCwd,
14821
+ dir: candidate.dir,
14822
+ ecosystems: candidate.ecosystems,
14823
+ rootSockJson
14824
+ });
14825
+ if (!result.ok) {
14826
+ return result;
14827
+ }
14828
+ }
14829
+ }
13374
14830
  }
13375
- const ignoreUnresolved = await askForIgnoreUnresolvedFlag(config.ignoreUnresolved);
13376
- if (ignoreUnresolved === undefined) {
14831
+ if (!included.length) {
14832
+ logger.logger.log('');
14833
+ logger.logger.success('Recursive setup complete.');
14834
+ return notCanceled$1();
14835
+ }
14836
+ logger.logger.log('');
14837
+ const wantsIndividual = await askYesNo('Configure other build roots found beneath this one, individually?');
14838
+ if (wantsIndividual === undefined || wantsIndividual === null) {
13377
14839
  return canceledByUser$1();
13378
- } else if (ignoreUnresolved === 'yes' || ignoreUnresolved === 'no') {
13379
- config.ignoreUnresolved = ignoreUnresolved === 'yes';
13380
- } else {
13381
- delete config.ignoreUnresolved;
13382
14840
  }
13383
- return notCanceled$1();
13384
- }
13385
- function canceledByUser$1() {
14841
+ if (!wantsIndividual) {
14842
+ logger.logger.log('');
14843
+ logger.logger.success('Recursive setup complete.');
14844
+ return notCanceled$1();
14845
+ }
13386
14846
  logger.logger.log('');
13387
- logger.logger.info('User canceled');
14847
+ logger.logger.log('For each one, choose to configure it or leave it as-is. To disable one,');
14848
+ logger.logger.log('re-run with --exclude-paths instead.');
13388
14849
  logger.logger.log('');
13389
- return {
13390
- ok: true,
13391
- data: {
13392
- canceled: true
14850
+ const coveredByEcosystem = new Map();
14851
+ // cwd never appears in `included`, but a reactor root often sits exactly
14852
+ // at cwd - seed coverage from it first so its declared members still get
14853
+ // recognized.
14854
+ for (const ecosystem of ecosystemsAtCwd) {
14855
+ // eslint-disable-next-line no-await-in-loop
14856
+ const coverageResult = await markWorkspaceCoverage({
14857
+ candidate: {
14858
+ dir: realCwd,
14859
+ ecosystem
14860
+ },
14861
+ coveredByEcosystem,
14862
+ cwd: realCwd,
14863
+ excludePaths,
14864
+ rootSockJson
14865
+ });
14866
+ if (!coverageResult.ok) {
14867
+ return coverageResult;
13393
14868
  }
14869
+ }
14870
+ const counts = {
14871
+ configured: 0,
14872
+ covered: 0,
14873
+ inherited: 0,
14874
+ reenabled: 0,
14875
+ skipped: 0
13394
14876
  };
13395
- }
13396
- function notCanceled$1() {
13397
- return {
13398
- ok: true,
13399
- data: {
13400
- canceled: false
14877
+ const orderedIncluded = sortCandidatesForDisplay(included, realCwd);
14878
+ for (const candidate of orderedIncluded) {
14879
+ const covered = coveredByEcosystem.get(candidate.ecosystem);
14880
+ if (covered?.has(candidate.dir)) {
14881
+ counts.covered += 1;
14882
+ continue;
13401
14883
  }
13402
- };
14884
+
14885
+ // eslint-disable-next-line no-await-in-loop
14886
+ const result = await processCandidate({
14887
+ cwd: realCwd,
14888
+ dir: candidate.dir,
14889
+ ecosystem: candidate.ecosystem,
14890
+ rootSockJson
14891
+ });
14892
+ if (!result.ok) {
14893
+ return result;
14894
+ }
14895
+ if (result.data.canceled) {
14896
+ // The cancellation itself (select Esc/Ctrl+C, or a sub-wizard's own
14897
+ // cancel) already logged "User canceled" - don't log it twice.
14898
+ return {
14899
+ ok: true,
14900
+ data: {
14901
+ canceled: true
14902
+ }
14903
+ };
14904
+ }
14905
+ counts[result.data.outcome] += 1;
14906
+ if (result.data.reenabled) {
14907
+ // A qualifier on the 'configured'/'inherited' bucket just incremented
14908
+ // above, not a separate bucket - avoids double-counting the candidate.
14909
+ counts.reenabled += 1;
14910
+ }
14911
+
14912
+ // Interleaved rather than a bulk pre-pass, so this (possibly slow)
14913
+ // build-tool invocation only runs for a project already prompted about.
14914
+ // eslint-disable-next-line no-await-in-loop
14915
+ const coverageResult = await markWorkspaceCoverage({
14916
+ candidate,
14917
+ coveredByEcosystem,
14918
+ cwd: realCwd,
14919
+ excludePaths,
14920
+ rootSockJson
14921
+ });
14922
+ if (!coverageResult.ok) {
14923
+ return coverageResult;
14924
+ }
14925
+ }
14926
+ logger.logger.log('');
14927
+ logger.logger.log(`${counts.configured} configured (${counts.reenabled} re-enabled), ${counts.inherited} left as-is, ${counts.skipped} left disabled, ${counts.covered} covered by a parent build.`);
14928
+ logger.logger.log('');
14929
+ logger.logger.success('Recursive setup complete.');
14930
+ return notCanceled$1();
13403
14931
  }
13404
14932
 
13405
- async function handleManifestSetup(cwd, defaultOnReadError) {
13406
- const result = await setupManifestConfig(cwd, defaultOnReadError);
14933
+ async function handleManifestSetup(cwd, defaultOnReadError, dynamicSbomInference = false, excludePaths) {
14934
+ const result = dynamicSbomInference ? await setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePaths) : await setupManifestConfig(cwd, defaultOnReadError);
13407
14935
  await outputManifestSetup(result);
13408
14936
  }
13409
14937
 
@@ -13413,9 +14941,21 @@ const config$8 = {
13413
14941
  hidden: false,
13414
14942
  flags: {
13415
14943
  ...flags.commonFlags,
14944
+ // Only meaningful alongside the hidden --dynamic-sbom-inference below; kept hidden too.
14945
+ excludePaths: {
14946
+ type: 'string',
14947
+ isMultiple: true,
14948
+ hidden: true,
14949
+ description: 'Build roots matching these glob patterns (and everything beneath them) are marked disabled. Patterns are anchored micromatch globs matched relative to CWD: `legacy` matches only `<cwd>/legacy`; use `**/legacy` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
14950
+ },
13416
14951
  defaultOnReadError: {
13417
14952
  type: 'boolean',
13418
14953
  description: `If reading the ${constants.SOCKET_JSON} fails, just use a default config? Warning: This might override the existing json file!`
14954
+ },
14955
+ dynamicSbomInference: {
14956
+ type: 'boolean',
14957
+ hidden: true,
14958
+ description: 'Recursively scans for every gradle/sbt/maven build root beneath CWD first, so the CWD config step only asks about ecosystems actually found somewhere in the tree. A build root matching --exclude-paths is bulk-disabled with no prompt, applied unconditionally; eligible build roots found afterward can be configured individually'
13419
14959
  }
13420
14960
  },
13421
14961
  help: (command, config) => `
@@ -13464,7 +15004,8 @@ async function run$z(argv, importMeta, {
13464
15004
  parentName
13465
15005
  });
13466
15006
  const {
13467
- defaultOnReadError = false
15007
+ defaultOnReadError = false,
15008
+ dynamicSbomInference = false
13468
15009
  } = cli.flags;
13469
15010
  const dryRun = !!cli.flags['dryRun'];
13470
15011
  let [cwd = '.'] = cli.input;
@@ -13475,7 +15016,9 @@ async function run$z(argv, importMeta, {
13475
15016
  logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
13476
15017
  return;
13477
15018
  }
13478
- await handleManifestSetup(cwd, Boolean(defaultOnReadError));
15019
+ const excludePaths = utils.cmdFlagValueToArray(cli.flags['excludePaths']);
15020
+ assertValidExcludePaths(excludePaths);
15021
+ await handleManifestSetup(cwd, Boolean(defaultOnReadError), Boolean(dynamicSbomInference), excludePaths);
13479
15022
  }
13480
15023
 
13481
15024
  const config$7 = {
@@ -13502,6 +15045,7 @@ async function run$y(argv, importMeta, {
13502
15045
  bazel: cmdManifestBazel,
13503
15046
  cdxgen: cmdManifestCdxgen,
13504
15047
  conda: cmdManifestConda,
15048
+ 'dynamic-sbom-inference': cmdManifestDynamicSbomInference,
13505
15049
  gradle: cmdManifestGradle,
13506
15050
  kotlin: cmdManifestKotlin,
13507
15051
  maven: cmdManifestMaven,
@@ -17575,6 +19119,124 @@ const cmdRepository = {
17575
19119
  }
17576
19120
  };
17577
19121
 
19122
+ const reachabilityFlags = {
19123
+ dynamicSbomInference: {
19124
+ type: 'boolean',
19125
+ default: false,
19126
+ hidden: true,
19127
+ description: 'Internal: enables dynamic SBOM inference for full application reachability analysis. Passes --maven-use-only-root-socket-facts to Coana and implies --auto-manifest.'
19128
+ },
19129
+ reachVersion: {
19130
+ type: 'string',
19131
+ description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.default.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION}.`
19132
+ },
19133
+ reachAnalysisMemoryLimit: {
19134
+ type: 'string',
19135
+ default: '8192',
19136
+ description: 'The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB.'
19137
+ },
19138
+ reachAnalysisTimeout: {
19139
+ type: 'string',
19140
+ default: '',
19141
+ description: 'Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly.'
19142
+ },
19143
+ reachConcurrency: {
19144
+ type: 'number',
19145
+ default: 1,
19146
+ description: 'Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available.'
19147
+ },
19148
+ reachContinueOnAnalysisErrors: {
19149
+ type: 'boolean',
19150
+ default: false,
19151
+ description: 'Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back to precomputed reachability results. By default, the CLI halts on analysis errors.'
19152
+ },
19153
+ reachContinueOnInstallErrors: {
19154
+ type: 'boolean',
19155
+ default: false,
19156
+ description: 'Continue reachability analysis when package installation fails, falling back to precomputed reachability results. By default, the CLI halts on installation errors.'
19157
+ },
19158
+ reachContinueOnMissingLockFiles: {
19159
+ type: 'boolean',
19160
+ default: false,
19161
+ description: 'Continue reachability analysis when a Gradle or SBT project is missing its lock file (or version catalog / pre-generated SBOM). By default, the CLI halts.'
19162
+ },
19163
+ reachContinueOnNoSourceFiles: {
19164
+ type: 'boolean',
19165
+ default: false,
19166
+ description: 'Continue reachability analysis when a workspace contains no source files for its ecosystem. By default, the CLI halts.'
19167
+ },
19168
+ reachDisableExternalToolChecks: {
19169
+ type: 'boolean',
19170
+ default: false,
19171
+ description: 'Disable external tool checks during reachability analysis.'
19172
+ },
19173
+ reachDebug: {
19174
+ type: 'boolean',
19175
+ default: false,
19176
+ description: 'Enable debug mode for reachability analysis. Provides verbose logging from the reachability CLI.'
19177
+ },
19178
+ reachDetailedAnalysisLogFile: {
19179
+ type: 'boolean',
19180
+ default: false,
19181
+ description: 'A log file with detailed analysis logs is written to root of each analyzed workspace.'
19182
+ },
19183
+ reachDisableAnalytics: {
19184
+ type: 'boolean',
19185
+ default: false,
19186
+ description: 'Disable reachability analytics sharing with Socket. Also disables caching-based optimizations.'
19187
+ },
19188
+ reachDisableAnalysisSplitting: {
19189
+ type: 'boolean',
19190
+ default: false,
19191
+ hidden: true,
19192
+ description: 'Deprecated: Analysis splitting is now disabled by default. This flag is a no-op.'
19193
+ },
19194
+ reachEnableAnalysisSplitting: {
19195
+ type: 'boolean',
19196
+ default: false,
19197
+ description: 'Allow the reachability analysis to partition CVEs into buckets that are processed in separate analysis runs. May improve accuracy, but not recommended by default.'
19198
+ },
19199
+ reachEcosystems: {
19200
+ type: 'string',
19201
+ isMultiple: true,
19202
+ description: `List of ecosystems to conduct reachability analysis on, as either a comma separated value or as multiple flags. Supported: ${utils.getReachabilityEcosystemChoices().join(', ')}. Defaults to all supported ecosystems.`
19203
+ },
19204
+ reachExcludePaths: {
19205
+ type: 'string',
19206
+ isMultiple: true,
19207
+ hidden: true,
19208
+ description: 'Deprecated: use --exclude-paths instead. List of paths to exclude from reachability analysis, as either a comma separated value or as multiple flags.'
19209
+ },
19210
+ reachLazyMode: {
19211
+ type: 'boolean',
19212
+ default: false,
19213
+ description: 'Enable lazy mode for reachability analysis.',
19214
+ hidden: true
19215
+ },
19216
+ reachRetainFactsFile: {
19217
+ type: 'boolean',
19218
+ default: false,
19219
+ description: 'Keep the `.socket.facts.json` reachability report that the analysis writes to the scan directory instead of deleting it after a successful scan. IMPORTANT: you must delete this file before running a fresh full application reachability scan. A stale `.socket.facts.json` left in place is picked up as a pre-generated input and silently overrides fresh analysis, so the new scan results will not be reliable.'
19220
+ },
19221
+ reachSkipCache: {
19222
+ type: 'boolean',
19223
+ default: false,
19224
+ description: 'Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous runs to speed up the analysis.'
19225
+ },
19226
+ reachUseOnlyPregeneratedSboms: {
19227
+ type: 'boolean',
19228
+ default: false,
19229
+ description: 'When using this option, the scan is created based only on pre-generated CDX and SPDX files in your project.'
19230
+ }
19231
+ };
19232
+ const excludePathsFlag = {
19233
+ excludePaths: {
19234
+ type: 'string',
19235
+ isMultiple: true,
19236
+ description: 'List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (`--cwd` if set), not the reachability target: `tests` matches only `<cwd>/tests`; use `**/tests` to match at any depth. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.'
19237
+ }
19238
+ };
19239
+
17578
19240
  async function suggestTarget() {
17579
19241
  // We could prefill this with sub-dirs of the current
17580
19242
  // dir ... but is that going to be useful?
@@ -18600,17 +20262,39 @@ async function createScanFromGithub({
18600
20262
  return sure;
18601
20263
  }
18602
20264
  }
20265
+ return await runGithubScanLoop(targetRepos, repoSlug => scanRepo(repoSlug, {
20266
+ githubApiUrl,
20267
+ githubToken,
20268
+ orgSlug,
20269
+ orgGithub,
20270
+ outputKind,
20271
+ repos
20272
+ }));
20273
+ }
20274
+
20275
+ /**
20276
+ * Drive the per-repo scan loop and decide the overall run result.
20277
+ *
20278
+ * The loop stops early on a blocking GitHub error (rate limit / auth / abuse
20279
+ * detection) because every remaining repo would fail the same way. Previously
20280
+ * a rate-limited token made every repo fail its API calls, the loop swallowed
20281
+ * each failure, and the final "N repos / 0 manifests" summary misled users and
20282
+ * CI into thinking the scan succeeded when nothing was uploaded. A run where
20283
+ * every attempted repo failed for a non-blocking reason is also surfaced as an
20284
+ * error rather than a silent ok:true.
20285
+ *
20286
+ * `scanRepoFn` is injected so this decision logic can be tested without the
20287
+ * GitHub network path.
20288
+ */
20289
+ async function runGithubScanLoop(targetRepos, scanRepoFn) {
18603
20290
  let scansCreated = 0;
20291
+ let reposScanned = 0;
20292
+ let blockingError;
20293
+ const perRepoFailures = [];
18604
20294
  for (const repoSlug of targetRepos) {
20295
+ reposScanned += 1;
18605
20296
  // eslint-disable-next-line no-await-in-loop
18606
- const scanCResult = await scanRepo(repoSlug, {
18607
- githubApiUrl,
18608
- githubToken,
18609
- orgSlug,
18610
- orgGithub,
18611
- outputKind,
18612
- repos
18613
- });
20297
+ const scanCResult = await scanRepoFn(repoSlug);
18614
20298
  if (scanCResult.ok) {
18615
20299
  const {
18616
20300
  scanCreated
@@ -18618,9 +20302,43 @@ async function createScanFromGithub({
18618
20302
  if (scanCreated) {
18619
20303
  scansCreated += 1;
18620
20304
  }
20305
+ continue;
18621
20306
  }
20307
+ perRepoFailures.push({
20308
+ repo: repoSlug,
20309
+ message: scanCResult.message
20310
+ });
20311
+ // Stop on rate-limit / auth / abuse failures: every remaining repo will
20312
+ // fail for the same reason, and continuing only burns more quota while
20313
+ // delaying the real error.
20314
+ if (utils.isGitHubBlockingError(scanCResult.message)) {
20315
+ blockingError = {
20316
+ ok: false,
20317
+ message: scanCResult.message,
20318
+ cause: scanCResult.cause
20319
+ };
20320
+ break;
20321
+ }
20322
+ }
20323
+ if (blockingError) {
20324
+ logger.logger.fail(blockingError.message);
20325
+ return blockingError;
20326
+ }
20327
+
20328
+ // If every attempted repo failed (but not for a known blocking reason),
20329
+ // treat the run as an error so scripts do not infer success from an
20330
+ // ok:true with zero scans created. Checked before the success lines below
20331
+ // so an all-failed run never prints green "processed" output that a script
20332
+ // reading log lines could mistake for success.
20333
+ if (reposScanned > 0 && scansCreated === 0 && perRepoFailures.length === reposScanned) {
20334
+ const firstFailure = perRepoFailures[0];
20335
+ return {
20336
+ ok: false,
20337
+ message: 'All repos failed to scan',
20338
+ cause: `All ${reposScanned} repos failed to scan. First failure for ${firstFailure.repo}: ${firstFailure.message}. ` + 'See the log above for per-repo details.'
20339
+ };
18622
20340
  }
18623
- logger.logger.success(targetRepos.length, 'GitHub repos detected');
20341
+ logger.logger.success(reposScanned, 'GitHub repos processed');
18624
20342
  logger.logger.success(scansCreated, 'with supported Manifest files');
18625
20343
  return {
18626
20344
  ok: true,
@@ -18801,8 +20519,20 @@ async function testAndDownloadManifestFiles({
18801
20519
  if (result.data.isManifest) {
18802
20520
  fileCount += 1;
18803
20521
  }
18804
- } else if (!firstFailureResult) {
18805
- firstFailureResult = result;
20522
+ } else {
20523
+ // A blocking error (rate limit / auth / abuse detection) is token-wide:
20524
+ // every remaining download would fail the same way, and an earlier
20525
+ // successful download must not mask it into an ok:true scan of a
20526
+ // partial manifest set. Surface it immediately, the same way the repo
20527
+ // loop short-circuits on blocking errors.
20528
+ if (utils.isGitHubBlockingError(result.message)) {
20529
+ logger.logger.groupEnd();
20530
+ logger.logger.fail(result.message);
20531
+ return result;
20532
+ }
20533
+ if (!firstFailureResult) {
20534
+ firstFailureResult = result;
20535
+ }
18806
20536
  }
18807
20537
  }
18808
20538
  logger.logger.groupEnd();
@@ -18873,22 +20603,17 @@ async function downloadManifestFile({
18873
20603
  debug.debugDir('inspect', {
18874
20604
  fileUrl
18875
20605
  });
18876
- utils.debugApiRequest('GET', fileUrl);
18877
- let downloadUrlResponse;
18878
- try {
18879
- downloadUrlResponse = await utils.apiFetch(fileUrl, {
18880
- method: 'GET',
18881
- headers: {
18882
- Authorization: `Bearer ${githubToken}`
18883
- }
18884
- });
18885
- utils.debugApiResponse('GET', fileUrl, downloadUrlResponse.status);
18886
- } catch (e) {
18887
- utils.debugApiResponse('GET', fileUrl, undefined, e);
18888
- throw e;
20606
+ const reqResult = await utils.githubApiRequest(fileUrl, {
20607
+ method: 'GET',
20608
+ headers: {
20609
+ Authorization: `Bearer ${githubToken}`
20610
+ }
20611
+ }, `fetching the download URL for ${file}`);
20612
+ if (!reqResult.ok) {
20613
+ return reqResult;
18889
20614
  }
18890
20615
  debug.debugFn('notice', 'complete: request');
18891
- const downloadUrlText = await downloadUrlResponse.text();
20616
+ const downloadUrlText = reqResult.data.bodyText;
18892
20617
  debug.debugFn('inspect', 'response: raw download url', downloadUrlText);
18893
20618
  let downloadUrl;
18894
20619
  try {
@@ -18927,6 +20652,15 @@ async function streamDownloadWithFetch(localPath, downloadUrl) {
18927
20652
  response = await utils.apiFetch(downloadUrl);
18928
20653
  utils.debugApiResponse('GET', downloadUrl, response.status);
18929
20654
  if (!response.ok) {
20655
+ // Surface rate-limit / auth failures on the raw download host too, so a
20656
+ // bulk run that trips the limit mid-download fails loudly instead of
20657
+ // being counted as just another skipped file. Header/status-only check;
20658
+ // the stream body is left unconsumed.
20659
+ const blocking = utils.classifyGitHubResponse(response.status, response.headers, '', 'downloading a manifest file');
20660
+ if (blocking) {
20661
+ logger.logger.fail(blocking.message);
20662
+ return blocking;
20663
+ }
18930
20664
  const errorMsg = `Download failed due to bad server response: ${response.status} ${response.statusText} for ${downloadUrl}`;
18931
20665
  logger.logger.fail(errorMsg);
18932
20666
  return {
@@ -19008,20 +20742,15 @@ async function getLastCommitDetails({
19008
20742
  logger.logger.info(`Requesting last commit for default branch ${defaultBranch} for ${orgGithub}/${repoSlug}...`);
19009
20743
  const commitApiUrl = `${repoApiUrl}/commits?sha=${defaultBranch}&per_page=1`;
19010
20744
  debug.debugFn('inspect', 'url: commit', commitApiUrl);
19011
- utils.debugApiRequest('GET', commitApiUrl);
19012
- let commitResponse;
19013
- try {
19014
- commitResponse = await utils.apiFetch(commitApiUrl, {
19015
- headers: {
19016
- Authorization: `Bearer ${githubToken}`
19017
- }
19018
- });
19019
- utils.debugApiResponse('GET', commitApiUrl, commitResponse.status);
19020
- } catch (e) {
19021
- utils.debugApiResponse('GET', commitApiUrl, undefined, e);
19022
- throw e;
20745
+ const reqResult = await utils.githubApiRequest(commitApiUrl, {
20746
+ headers: {
20747
+ Authorization: `Bearer ${githubToken}`
20748
+ }
20749
+ }, `fetching the latest commit for ${orgGithub}/${repoSlug}`);
20750
+ if (!reqResult.ok) {
20751
+ return reqResult;
19023
20752
  }
19024
- const commitText = await commitResponse.text();
20753
+ const commitText = reqResult.data.bodyText;
19025
20754
  debug.debugFn('inspect', 'response: commit', commitText);
19026
20755
  let lastCommit;
19027
20756
  try {
@@ -19112,22 +20841,17 @@ async function getRepoDetails({
19112
20841
  debug.debugDir('inspect', {
19113
20842
  repoApiUrl
19114
20843
  });
19115
- let repoDetailsResponse;
19116
- try {
19117
- utils.debugApiRequest('GET', repoApiUrl);
19118
- repoDetailsResponse = await utils.apiFetch(repoApiUrl, {
19119
- method: 'GET',
19120
- headers: {
19121
- Authorization: `Bearer ${githubToken}`
19122
- }
19123
- });
19124
- utils.debugApiResponse('GET', repoApiUrl, repoDetailsResponse.status);
19125
- } catch (e) {
19126
- utils.debugApiResponse('GET', repoApiUrl, undefined, e);
19127
- throw e;
20844
+ const reqResult = await utils.githubApiRequest(repoApiUrl, {
20845
+ method: 'GET',
20846
+ headers: {
20847
+ Authorization: `Bearer ${githubToken}`
20848
+ }
20849
+ }, `fetching repo details for ${orgGithub}/${repoSlug}`);
20850
+ if (!reqResult.ok) {
20851
+ return reqResult;
19128
20852
  }
19129
20853
  logger.logger.success(`Request completed.`);
19130
- const repoDetailsText = await repoDetailsResponse.text();
20854
+ const repoDetailsText = reqResult.data.bodyText;
19131
20855
  debug.debugFn('inspect', 'response: repo', repoDetailsText);
19132
20856
  let repoDetails;
19133
20857
  try {
@@ -19168,21 +20892,16 @@ async function getRepoBranchTree({
19168
20892
  logger.logger.info(`Requesting default branch file tree; branch \`${defaultBranch}\`, repo \`${orgGithub}/${repoSlug}\`...`);
19169
20893
  const treeApiUrl = `${repoApiUrl}/git/trees/${defaultBranch}?recursive=1`;
19170
20894
  debug.debugFn('inspect', 'url: tree', treeApiUrl);
19171
- let treeResponse;
19172
- try {
19173
- utils.debugApiRequest('GET', treeApiUrl);
19174
- treeResponse = await utils.apiFetch(treeApiUrl, {
19175
- method: 'GET',
19176
- headers: {
19177
- Authorization: `Bearer ${githubToken}`
19178
- }
19179
- });
19180
- utils.debugApiResponse('GET', treeApiUrl, treeResponse.status);
19181
- } catch (e) {
19182
- utils.debugApiResponse('GET', treeApiUrl, undefined, e);
19183
- throw e;
20895
+ const reqResult = await utils.githubApiRequest(treeApiUrl, {
20896
+ method: 'GET',
20897
+ headers: {
20898
+ Authorization: `Bearer ${githubToken}`
20899
+ }
20900
+ }, `fetching the file tree for ${orgGithub}/${repoSlug}`);
20901
+ if (!reqResult.ok) {
20902
+ return reqResult;
19184
20903
  }
19185
- const treeText = await treeResponse.text();
20904
+ const treeText = reqResult.data.bodyText;
19186
20905
  debug.debugFn('inspect', 'response: tree', treeText);
19187
20906
  let treeDetails;
19188
20907
  try {
@@ -22168,5 +23887,5 @@ process.on('unhandledRejection', async (reason, promise) => {
22168
23887
  // eslint-disable-next-line n/no-process-exit
22169
23888
  process.exit(1);
22170
23889
  });
22171
- //# debugId=69b61d3f-9a18-4a06-a0cd-4567860caa90
23890
+ //# debugId=684e53fa-a85b-4a32-befc-11d50061b4b0
22172
23891
  //# sourceMappingURL=cli.js.map