@socketsecurity/cli 1.1.154 → 1.1.156

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 (45) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +28 -23
  3. package/dist/cli.js +4135 -3974
  4. package/dist/cli.js.map +1 -1
  5. package/dist/constants.js +4 -4
  6. package/dist/constants.js.map +1 -1
  7. package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
  8. package/dist/shadow-npm-bin2.js +27 -2
  9. package/dist/shadow-npm-bin2.js.map +1 -1
  10. package/dist/tsconfig.dts.tsbuildinfo +1 -1
  11. package/dist/types/commands/ci/handle-ci.d.mts +7 -0
  12. package/dist/types/commands/ci/handle-ci.d.mts.map +1 -1
  13. package/dist/types/commands/manifest/cmd-manifest-dynamic-sbom-inference.d.mts.map +1 -1
  14. package/dist/types/commands/manifest/cmd-manifest-setup.d.mts.map +1 -1
  15. package/dist/types/commands/manifest/discover-manifest-roots.d.mts +2 -1
  16. package/dist/types/commands/manifest/discover-manifest-roots.d.mts.map +1 -1
  17. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts +24 -6
  18. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -1
  19. package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
  20. package/dist/types/commands/manifest/handle-manifest-dynamic-sbom-inference.d.mts.map +1 -1
  21. package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
  22. package/dist/types/commands/manifest/scripts/sidecar.d.mts +47 -21
  23. package/dist/types/commands/manifest/scripts/sidecar.d.mts.map +1 -1
  24. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts +8 -6
  25. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts.map +1 -1
  26. package/dist/types/commands/scan/cmd-scan-reach.d.mts.map +1 -1
  27. package/dist/types/commands/scan/handle-create-new-scan.d.mts.map +1 -1
  28. package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
  29. package/dist/types/commands/scan/reachability-flags.d.mts.map +1 -1
  30. package/dist/types/shadow/npm-base.d.mts +18 -0
  31. package/dist/types/shadow/npm-base.d.mts.map +1 -1
  32. package/dist/types/utils/fs.d.mts +4 -0
  33. package/dist/types/utils/fs.d.mts.map +1 -1
  34. package/dist/types/utils/git.d.mts +6 -0
  35. package/dist/types/utils/git.d.mts.map +1 -1
  36. package/dist/types/utils/github-errors.d.mts +2 -1
  37. package/dist/types/utils/github-errors.d.mts.map +1 -1
  38. package/dist/types/utils/glob.d.mts.map +1 -1
  39. package/dist/types/utils/npm-paths.d.mts +17 -0
  40. package/dist/types/utils/npm-paths.d.mts.map +1 -1
  41. package/dist/utils.js +348 -120
  42. package/dist/utils.js.map +1 -1
  43. package/package.json +16 -12
  44. package/logo-dark.png +0 -0
  45. package/logo-light.png +0 -0
package/dist/utils.js CHANGED
@@ -15,6 +15,7 @@ var flags = require('./flags.js');
15
15
  var path = require('node:path');
16
16
  var regexps = require('../external/@socketsecurity/registry/lib/regexps');
17
17
  var prompts = require('../external/@socketsecurity/registry/lib/prompts');
18
+ var env = require('../external/@socketsecurity/registry/lib/env');
18
19
  var spawn = require('../external/@socketsecurity/registry/lib/spawn');
19
20
  var fs = require('node:fs');
20
21
  var fs$2 = require('node:fs/promises');
@@ -27,16 +28,17 @@ var url = require('../external/@socketsecurity/registry/lib/url');
27
28
  var agent = require('../external/@socketsecurity/registry/lib/agent');
28
29
  var bin = require('../external/@socketsecurity/registry/lib/bin');
29
30
  var packages = require('../external/@socketsecurity/registry/lib/packages');
31
+ var process$1 = require('node:process');
32
+ var require$$7 = require('node:timers/promises');
33
+ var promises = require('../external/@socketsecurity/registry/lib/promises');
30
34
  var require$$13 = require('node:https');
31
35
  var web = require('node:stream/web');
32
36
  var require$$0$1 = require('node:url');
33
37
  var globs = require('../external/@socketsecurity/registry/lib/globs');
34
38
  var streams = require('../external/@socketsecurity/registry/lib/streams');
35
- var require$$7 = require('node:timers/promises');
36
39
  var require$$10 = require('node:util');
37
40
  var require$$12 = require('node:http');
38
41
  var node_tls = require('node:tls');
39
- var process$1 = require('node:process');
40
42
  var require$$0 = require('node:crypto');
41
43
 
42
44
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -4003,6 +4005,7 @@ function extractOwner(owner) {
4003
4005
  * Repository Information:
4004
4006
  * - detectDefaultBranch: Find default branch (main/master/develop/etc)
4005
4007
  * - getBaseBranch: Determine base branch (respects GitHub Actions env)
4008
+ * - getCiBranch: Branch name a GitHub Actions run is on
4006
4009
  * - getRepoInfo: Extract owner/repo from git remote URL
4007
4010
  * - gitBranch: Get current branch or commit hash
4008
4011
  */
@@ -4048,6 +4051,29 @@ async function getBaseBranch(cwd = process.cwd()) {
4048
4051
  // https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches#about-the-default-branch
4049
4052
  return 'main';
4050
4053
  }
4054
+
4055
+ /**
4056
+ * The branch a GitHub Actions workflow run is on, or undefined when the
4057
+ * environment does not identify one. Read straight from process.env because
4058
+ * GITHUB_HEAD_REF is not part of the constants.ENV snapshot.
4059
+ */
4060
+ function getCiBranch() {
4061
+ // The head branch of a pull request, only set for pull_request and
4062
+ // pull_request_target events. Checked first because GITHUB_REF_NAME is
4063
+ // '<pr_number>/merge' on those events, which is not a branch name.
4064
+ // https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
4065
+ const githubHeadRef = env.envAsString(process.env['GITHUB_HEAD_REF']);
4066
+ if (githubHeadRef) {
4067
+ return githubHeadRef;
4068
+ }
4069
+ // The pushed ref. GITHUB_REF_TYPE tells branches and tags apart, and a tag
4070
+ // is not a branch name.
4071
+ const githubRefName = env.envAsString(process.env['GITHUB_REF_NAME']);
4072
+ if (env.envAsString(process.env['GITHUB_REF_TYPE']) === 'branch' && githubRefName) {
4073
+ return githubRefName;
4074
+ }
4075
+ return undefined;
4076
+ }
4051
4077
  async function getRepoInfo(cwd = process.cwd()) {
4052
4078
  let info;
4053
4079
  try {
@@ -4094,6 +4120,14 @@ async function gitBranch(cwd = process.cwd()) {
4094
4120
  error: e
4095
4121
  });
4096
4122
  }
4123
+ // Detached HEAD is the normal state in CI checkouts (actions/checkout),
4124
+ // where the commit-SHA fallback below would mislabel the scan's branch — a
4125
+ // SHA can never match the repo's default branch, so scans vanish from the
4126
+ // Main/PR tabs. Prefer the branch the CI run says it is on.
4127
+ const ciBranch = getCiBranch();
4128
+ if (ciBranch) {
4129
+ return ciBranch;
4130
+ }
4097
4131
  // Fallback to using rev-parse to get the short commit hash in a
4098
4132
  // detached HEAD state.
4099
4133
  try {
@@ -4579,6 +4613,67 @@ function getSupportedFilePatterns(supportedFiles) {
4579
4613
  }
4580
4614
  return patterns;
4581
4615
  }
4616
+ // Whether `targetPath` is ignored by the gitignore matchers on its ancestor
4617
+ // directories, root to leaf, last match wins. Each matcher tests the path
4618
+ // relative to its own directory (how git stores patterns); `isDir` appends a
4619
+ // trailing slash so a directory-only rule like `build/` matches the directory.
4620
+ // `targetPath` is POSIX, relative to cwd.
4621
+ function pathIgnoredByChain(targetPath, isDir, matchersByDir) {
4622
+ let ignored = false;
4623
+ // Ancestor directories of targetPath: '', then each parent dir prefix.
4624
+ const segments = targetPath.split('/');
4625
+ segments.pop();
4626
+ const dirs = [''];
4627
+ let prefix = '';
4628
+ for (let i = 0, {
4629
+ length
4630
+ } = segments; i < length; i += 1) {
4631
+ prefix = prefix ? `${prefix}/${segments[i]}` : segments[i];
4632
+ dirs.push(prefix);
4633
+ }
4634
+ for (let i = 0, {
4635
+ length
4636
+ } = dirs; i < length; i += 1) {
4637
+ const dir = dirs[i];
4638
+ const matchers = matchersByDir.get(dir);
4639
+ if (!matchers) {
4640
+ continue;
4641
+ }
4642
+ const relToDir = dir === '' ? targetPath : targetPath.slice(dir.length + 1);
4643
+ const probe = isDir ? `${relToDir}/` : relToDir;
4644
+ for (let j = 0, len = matchers.length; j < len; j += 1) {
4645
+ const result = matchers[j].test(probe);
4646
+ if (result.ignored) {
4647
+ ignored = true;
4648
+ } else if (result.unignored) {
4649
+ ignored = false;
4650
+ }
4651
+ }
4652
+ }
4653
+ return ignored;
4654
+ }
4655
+
4656
+ // Whether `relPath` is ignored, honoring git's rule that an excluded directory
4657
+ // is never descended into: a file is ignored if any ancestor directory is, and a
4658
+ // deeper `!` cannot re-include a file under an excluded parent. Walks ancestors
4659
+ // top-down, short-circuiting on the first excluded one. POSIX, relative to cwd.
4660
+ function isIgnoredAlongChain(relPath, matchersByDir) {
4661
+ // Outside cwd (a `..` prefix) or empty: outside every gitignore's domain, and
4662
+ // the `ignore` package throws on such input, so report not-ignored.
4663
+ if (!relPath || relPath === '..' || relPath.startsWith('../')) {
4664
+ return false;
4665
+ }
4666
+ const segments = relPath.split('/');
4667
+ const last = segments.length - 1;
4668
+ let prefix = '';
4669
+ for (let i = 0; i <= last; i += 1) {
4670
+ prefix = prefix ? `${prefix}/${segments[i]}` : segments[i];
4671
+ if (pathIgnoredByChain(prefix, i < last, matchersByDir)) {
4672
+ return true;
4673
+ }
4674
+ }
4675
+ return false;
4676
+ }
4582
4677
  async function globWithGitIgnore(patterns, options) {
4583
4678
  const {
4584
4679
  additionalIgnores,
@@ -4590,33 +4685,36 @@ async function globWithGitIgnore(patterns, options) {
4590
4685
  __proto__: null,
4591
4686
  ...options
4592
4687
  };
4688
+
4689
+ // Anchored minimatch patterns for fast-glob: built-in ignored dirs, venv
4690
+ // markers, projectIgnorePaths, and every discovered `.gitignore`, translated
4691
+ // and anchored from cwd. When no pattern is negated, fast-glob does the whole
4692
+ // gitignore match from this set.
4593
4693
  const ignores = new Set(IGNORED_DIR_PATTERNS);
4594
4694
 
4595
- // CLI-supplied `additionalIgnores` are already anchored minimatch they
4596
- // must not pass through the `ignore` package (whose gitignore "match
4597
- // anywhere" semantics would re-interpret a bare `tests` to match
4598
- // `subdir/tests/foo.json`). Keep them in fast-glob's ignore list across
4599
- // both paths; only gitignore-translated entries go into the `ig` matcher.
4695
+ // CLI-supplied `additionalIgnores` are already anchored minimatch from cwd, so
4696
+ // they go straight to fast-glob and never enter the gitignore matchers (whose
4697
+ // "match anywhere" semantics would re-interpret a bare `tests` as `**/tests`).
4600
4698
  const cliMinimatchIgnores = additionalIgnores ?? [];
4601
4699
  const projectIgnorePaths = socketConfig?.projectIgnorePaths;
4602
- const projectIgnoreGlobs = Array.isArray(projectIgnorePaths) ? ignoreFileLinesToGlobPatterns(projectIgnorePaths, path.join(cwd, '.gitignore'), cwd) : [];
4700
+ const projectIgnoreLines = Array.isArray(projectIgnorePaths) ? projectIgnorePaths : [];
4701
+ const projectIgnoreGlobs = projectIgnoreLines.length ? ignoreFileLinesToGlobPatterns(projectIgnoreLines, path.join(cwd, '.gitignore'), cwd) : [];
4603
4702
  for (const pattern of projectIgnoreGlobs) {
4604
4703
  ignores.add(pattern);
4605
4704
  }
4606
4705
 
4607
- // The discovery walk (`.gitignore` files plus `pyvenv.cfg` venv markers) has
4608
- // to honor the same directory exclusions as the package walk below. Otherwise
4609
- // an unreadable subtree (e.g. a postgres `pgdata` dir owned by another uid, or
4610
- // a Docker volume mount) makes fast-glob throw `EACCES: permission denied,
4611
- // scandir` *here* before --exclude-paths (`cliMinimatchIgnores`) or
4612
- // projectIgnorePaths are ever applied to the main walk, which is why excluding
4613
- // the path did not help. `suppressErrors` is the backstop: a directory the
4614
- // user simply cannot read cannot contain manifests they could scan anyway, so
4615
- // skip it instead of aborting the whole `socket fix` / `socket scan` run.
4616
- // Negated patterns are dropped for a discovery walk they could only
4617
- // re-include a subtree (never prevent a crash), and fast-glob treats `!`
4618
- // ignore entries inconsistently. Folding pyvenv.cfg discovery into this same
4619
- // walk avoids a second full-tree traversal.
4706
+ // Raw per-directory `.gitignore` contents from discovery. Matchers are built
4707
+ // from these only when a pattern is negated (see below).
4708
+ const gitignoreFiles = [];
4709
+ // Directory excludes from discovered pyvenv.cfg markers, so virtualenvs with
4710
+ // non-conventional names are pruned. Fed to fast-glob's ignore on every path.
4711
+ const venvGlobs = [];
4712
+
4713
+ // Discover `.gitignore` files and `pyvenv.cfg` venv markers in one walk,
4714
+ // honoring the same dir exclusions as the package walk so an unreadable subtree
4715
+ // (EACCES, foreign-uid pgdata, Docker mount) can't abort it. `suppressErrors`
4716
+ // skips dirs the user cannot read; negated patterns are dropped here because
4717
+ // fast-glob treats `!` ignore entries inconsistently.
4620
4718
  const discoveryStream = vendor.outExports.globStream(['**/.gitignore', `**/${PYVENV_CFG}`], {
4621
4719
  absolute: true,
4622
4720
  cwd,
@@ -4624,21 +4722,40 @@ async function globWithGitIgnore(patterns, options) {
4624
4722
  ignore: [...DEFAULT_IGNORE_FOR_GIT_IGNORE, ...projectIgnoreGlobs, ...cliMinimatchIgnores].filter(p => p.charCodeAt(0) !== 33 /*'!'*/).map(stripTrailingSlash),
4625
4723
  suppressErrors: true
4626
4724
  });
4627
- for await (const ignorePatterns of streams.transform(discoveryStream, async filepath => {
4725
+ for await (const found of streams.transform(discoveryStream, async filepath => {
4726
+ const dirRel = path$1.normalizePath(path.relative(cwd, path.dirname(filepath)));
4628
4727
  if (path.basename(filepath) === PYVENV_CFG) {
4629
- // A pyvenv.cfg sits at the venv root, so exclude the whole directory.
4630
- const relDir = path.relative(cwd, path.dirname(filepath)).replace(/\\/g, '/');
4631
- // An empty relDir means the scan target itself is a venv root; don't
4632
- // emit `/**`, which would exclude everything the user explicitly targeted.
4633
- return relDir ? [`${relDir}/**`] : [];
4728
+ // A pyvenv.cfg sits at the venv root, so exclude the whole directory. An
4729
+ // empty dirRel means the scan target itself is a venv root; don't emit
4730
+ // `/**`, which would exclude everything the user explicitly targeted.
4731
+ return {
4732
+ content: '',
4733
+ dir: dirRel,
4734
+ patterns: dirRel ? [`${dirRel}/**`] : [],
4735
+ venv: true
4736
+ };
4634
4737
  }
4635
- return ignoreFileToGlobPatterns((await fs$1.safeReadFile(filepath)) ?? '', filepath, cwd);
4738
+ const content = (await fs$1.safeReadFile(filepath)) ?? '';
4739
+ return {
4740
+ content,
4741
+ dir: dirRel,
4742
+ patterns: ignoreFileToGlobPatterns(content, filepath, cwd),
4743
+ venv: false
4744
+ };
4636
4745
  }, {
4637
4746
  concurrency: 8
4638
4747
  })) {
4639
- for (const p of ignorePatterns) {
4748
+ for (const p of found.patterns) {
4640
4749
  ignores.add(p);
4641
4750
  }
4751
+ if (found.venv) {
4752
+ venvGlobs.push(...found.patterns);
4753
+ } else if (found.content) {
4754
+ gitignoreFiles.push({
4755
+ content: found.content,
4756
+ dir: found.dir
4757
+ });
4758
+ }
4642
4759
  }
4643
4760
  let hasNegatedPattern = false;
4644
4761
  for (const p of ignores) {
@@ -4652,41 +4769,72 @@ async function globWithGitIgnore(patterns, options) {
4652
4769
  absolute: true,
4653
4770
  cwd,
4654
4771
  dot: true,
4655
- ignore: hasNegatedPattern ? [...globs.defaultIgnore, ...cliMinimatchIgnores] : [...ignores, ...cliMinimatchIgnores].map(stripTrailingSlash),
4772
+ // With a negation, the per-dir matcher chain (below) covers only gitignore
4773
+ // and projectIgnore patterns, so fast-glob still prunes the built-in ignored
4774
+ // dirs and discovered venvs. Without one, the full anchored set goes to
4775
+ // fast-glob, which does the whole match.
4776
+ ignore: hasNegatedPattern ? [...globs.defaultIgnore, ...IGNORED_DIR_PATTERNS, ...venvGlobs, ...cliMinimatchIgnores] : [...ignores, ...cliMinimatchIgnores].map(stripTrailingSlash),
4656
4777
  ...additionalOptions,
4657
- // Skip directories the running user cannot read rather than aborting the
4658
- // whole walk on the first `EACCES` (see the .gitignore discovery walk
4659
- // above for the full rationale). Pinned after `...additionalOptions` so a
4660
- // caller's options bag cannot accidentally flip it back to `false` and
4661
- // re-introduce the crash — `suppressErrors` is a safety invariant here, not
4662
- // a tunable.
4778
+ // Skip dirs the running user cannot read instead of aborting on the first
4779
+ // `EACCES`. Pinned after `...additionalOptions` so a caller's options bag
4780
+ // cannot flip it off; it is a safety invariant, not a tunable.
4663
4781
  suppressErrors: true
4664
4782
  };
4665
4783
 
4666
- // When no filter is provided and no negated patterns exist, use the fast path.
4784
+ // No negation and no filter: fast-glob's anchored ignore set is authoritative.
4667
4785
  if (!hasNegatedPattern && !filter) {
4668
4786
  return await vendor.outExports.glob(patterns, globOptions);
4669
4787
  }
4670
- // Add support for negated "ignore" patterns which many globbing libraries,
4671
- // including 'fast-glob', 'globby', and 'tinyglobby', lack support for.
4672
- // Use streaming to avoid unbounded memory accumulation.
4673
- // This is critical for large monorepos with 100k+ files.
4788
+
4789
+ // When a pattern is negated, match each candidate against the gitignore
4790
+ // ancestor chain. One matcher per `.gitignore` is built from its raw lines and
4791
+ // deduped by content via `igByContent`, keeping compiled-regex memory bounded
4792
+ // by the number of DISTINCT `.gitignore` contents, not by file count (a single
4793
+ // matcher over every anchored pattern can exhaust V8 code space on big repos).
4794
+ let matchersByDir;
4795
+ if (hasNegatedPattern) {
4796
+ const byDir = new Map();
4797
+ const igByContent = new Map();
4798
+ const addMatcher = (dirRel, content) => {
4799
+ let ig = igByContent.get(content);
4800
+ if (!ig) {
4801
+ ig = vendor.ignoreExports().add(content.split(/\r?\n/));
4802
+ igByContent.set(content, ig);
4803
+ }
4804
+ const existing = byDir.get(dirRel);
4805
+ if (existing) {
4806
+ existing.push(ig);
4807
+ } else {
4808
+ byDir.set(dirRel, [ig]);
4809
+ }
4810
+ };
4811
+ // projectIgnorePaths act as a root-level gitignore.
4812
+ if (projectIgnoreLines.length) {
4813
+ addMatcher('', projectIgnoreLines.join('\n'));
4814
+ }
4815
+ for (let i = 0, {
4816
+ length
4817
+ } = gitignoreFiles; i < length; i += 1) {
4818
+ addMatcher(gitignoreFiles[i].dir, gitignoreFiles[i].content);
4819
+ }
4820
+ matchersByDir = byDir;
4821
+ }
4822
+
4823
+ // Stream so memory stays bounded on large monorepos with 100k+ files: the
4824
+ // optional caller filter drops non-matches before they accumulate. On the slow
4825
+ // path each surviving entry is also re-checked against its gitignore ancestor
4826
+ // chain, which carries the full negation support fast-glob lacks.
4674
4827
  const results = [];
4675
- const ig = hasNegatedPattern ? vendor.ignoreExports().add([...ignores]) : null;
4676
4828
  const stream = vendor.outExports.globStream(patterns, globOptions);
4677
4829
  for await (const p of stream) {
4678
- // Check gitignore patterns with negation support.
4679
- if (ig) {
4680
- // Note: the input files must be INSIDE the cwd. If you get strange looking
4681
- // relative path errors here, most likely your path is outside the given cwd.
4682
- const relPath = globOptions.absolute ? path.relative(cwd, p) : p;
4683
- if (ig.ignores(relPath)) {
4830
+ if (matchersByDir) {
4831
+ // Patterns are forward-slash anchored and tested relative to each
4832
+ // gitignore's directory; normalize so a Windows backslash path matches.
4833
+ const relPath = path$1.normalizePath(globOptions.absolute ? path.relative(cwd, p) : p);
4834
+ if (isIgnoredAlongChain(relPath, matchersByDir)) {
4684
4835
  continue;
4685
4836
  }
4686
4837
  }
4687
- // Apply the optional filter to reduce memory usage.
4688
- // When scanning large monorepos, this filters early (e.g., to manifest files only)
4689
- // instead of accumulating all 100k+ files and filtering later.
4690
4838
  if (filter && !filter(p)) {
4691
4839
  continue;
4692
4840
  }
@@ -5087,6 +5235,17 @@ async function withTmpDir(prefix, fn) {
5087
5235
  }
5088
5236
  }
5089
5237
 
5238
+ // Symlink-resolved absolute path when the target exists (e.g. macOS symlinks
5239
+ // /tmp -> /private/tmp); falls back to a plain resolve so a not-yet-existing
5240
+ // path still gets a usable absolute value instead of throwing.
5241
+ async function realpathOrResolved(target) {
5242
+ try {
5243
+ return await fs.promises.realpath(target);
5244
+ } catch {
5245
+ return path.resolve(target);
5246
+ }
5247
+ }
5248
+
5090
5249
  function findBinPathDetailsSync(binName) {
5091
5250
  const rawBinPaths = bin.whichBinSync(binName, {
5092
5251
  all: true,
@@ -6725,6 +6884,35 @@ function findBinNextToNode(binName) {
6725
6884
  }
6726
6885
  return undefined;
6727
6886
  }
6887
+ const JS_ENTRY_EXTENSIONS = new Set(['.cjs', '.js', '.mjs']);
6888
+
6889
+ /**
6890
+ * Resolve a package manager bin path to a JavaScript entry `node` can run.
6891
+ *
6892
+ * Some version managers ship `npm` as a shell script instead of the usual shim
6893
+ * that points at npm's JavaScript entry. mise writes a bash wrapper so it can
6894
+ * run `mise reshim` after a global install. resolveBinPathSync only recognizes
6895
+ * npm's own shim, by the `NPM_CLI_JS=` assignment inside it, and passes anything
6896
+ * else through untouched, so the wrapper reaches us unchanged. We then hand it
6897
+ * to `node` as the main module and the shell syntax throws a SyntaxError before
6898
+ * npm ever starts (issue #946).
6899
+ *
6900
+ * When the resolved path is not JavaScript, fall back to the CLI entry inside
6901
+ * npm's install directory, which findNpmDirPathSync already knows how to locate
6902
+ * for this layout. A path we cannot map keeps its current value, so an
6903
+ * unrecognized layout behaves exactly as it does today.
6904
+ */
6905
+ function resolveBinPathToJsEntry(binPath, cliEntryName) {
6906
+ if (JS_ENTRY_EXTENSIONS.has(path.extname(binPath).toLowerCase())) {
6907
+ return binPath;
6908
+ }
6909
+ const npmDirPath = findNpmDirPathSync(binPath);
6910
+ if (!npmDirPath) {
6911
+ return binPath;
6912
+ }
6913
+ const jsEntryPath = path.join(npmDirPath, 'bin', cliEntryName);
6914
+ return fs.existsSync(jsEntryPath) ? jsEntryPath : binPath;
6915
+ }
6728
6916
  let _npmBinPath;
6729
6917
  function getNpmBinPath() {
6730
6918
  if (_npmBinPath === undefined) {
@@ -6741,15 +6929,15 @@ function getNpmBinPathDetails() {
6741
6929
  // First try to find npm next to the node binary to avoid picking up
6742
6930
  // a project-local npm from node_modules/.bin on PATH.
6743
6931
  const npmNextToNode = findBinNextToNode(constants.NPM);
6744
- if (npmNextToNode) {
6745
- _npmBinPathDetails = {
6746
- name: constants.NPM,
6747
- path: npmNextToNode,
6748
- shadowed: false
6749
- };
6750
- } else {
6751
- _npmBinPathDetails = findBinPathDetailsSync(constants.NPM);
6752
- }
6932
+ const details = npmNextToNode ? {
6933
+ name: constants.NPM,
6934
+ path: npmNextToNode,
6935
+ shadowed: false
6936
+ } : findBinPathDetailsSync(constants.NPM);
6937
+ _npmBinPathDetails = details.path ? {
6938
+ ...details,
6939
+ path: resolveBinPathToJsEntry(details.path, 'npm-cli.js')
6940
+ } : details;
6753
6941
  }
6754
6942
  return _npmBinPathDetails;
6755
6943
  }
@@ -6806,15 +6994,15 @@ function getNpxBinPathDetails() {
6806
6994
  // standalone npx package which bundles npm@5.1.0, incompatible
6807
6995
  // with Node 22+).
6808
6996
  const npxNextToNode = findBinNextToNode('npx');
6809
- if (npxNextToNode) {
6810
- _npxBinPathDetails = {
6811
- name: 'npx',
6812
- path: npxNextToNode,
6813
- shadowed: false
6814
- };
6815
- } else {
6816
- _npxBinPathDetails = findBinPathDetailsSync('npx');
6817
- }
6997
+ const details = npxNextToNode ? {
6998
+ name: 'npx',
6999
+ path: npxNextToNode,
7000
+ shadowed: false
7001
+ } : findBinPathDetailsSync('npx');
7002
+ _npxBinPathDetails = details.path ? {
7003
+ ...details,
7004
+ path: resolveBinPathToJsEntry(details.path, 'npx-cli.js')
7005
+ } : details;
6818
7006
  }
6819
7007
  return _npxBinPathDetails;
6820
7008
  }
@@ -7420,20 +7608,20 @@ async function detectAndValidatePackageEnvironment(cwd, options) {
7420
7608
  * - Classifying a GitHub response as a blocking error (rate limit / abuse
7421
7609
  * detection / auth) with a clear, actionable message.
7422
7610
  * - A bounded-retry request wrapper that respects `Retry-After` /
7423
- * `x-ratelimit-reset` for short reset windows and retries transient 5xx /
7424
- * network failures with capped exponential backoff.
7611
+ * `x-ratelimit-reset` for short reset windows and hands transient 5xx /
7612
+ * network failures to the fleet's shared `pRetry` for backoff.
7425
7613
  */
7426
7614
 
7427
7615
  // GitHub returns 429 for some secondary rate limits; there is no shared
7428
7616
  // constant for it in constants.mts, so define it locally.
7429
7617
  const HTTP_STATUS_TOO_MANY_REQUESTS = 429;
7430
7618
 
7431
- // Retry at most this many times for transient (5xx / network) failures,
7432
- // counting the initial attempt.
7433
- const MAX_TRANSIENT_ATTEMPTS = 3;
7434
-
7435
- // Cap for exponential backoff between transient retries.
7436
- const MAX_BACKOFF_MS = 10_000;
7619
+ // Base delay before the first transient retry. Mirrors the default in
7620
+ // @socketsecurity/lib's releases/github-retry-config, including the env var
7621
+ // name, so both socket-cli lines back off against the GitHub API on the same
7622
+ // schedule. Read live rather than captured at import so a test or a CI job can
7623
+ // set it to 0 and skip the real wallclock wait.
7624
+ const DEFAULT_RETRY_BASE_DELAY_MS = 5000;
7437
7625
 
7438
7626
  // Only wait-and-retry a rate-limited response when the reset window is at
7439
7627
  // most this many seconds. The usual primary-limit reset is up to an hour
@@ -7525,13 +7713,34 @@ function classifyGitHubResponse(status, headers, bodyText, context) {
7525
7713
  }
7526
7714
  return undefined;
7527
7715
  }
7528
- function backoffMs(attempt) {
7529
- return Math.min(1000 * 2 ** (attempt - 1), MAX_BACKOFF_MS);
7716
+
7717
+ /**
7718
+ * Retry policy for transient GitHub failures. Same values as the shared
7719
+ * GITHUB_RETRY_CONFIG in @socketsecurity/lib: two retries on top of the initial
7720
+ * attempt, delay doubling each time, capped at 10 seconds. Built per call
7721
+ * rather than at import so the env override is read live.
7722
+ */
7723
+ function githubRetryOptions() {
7724
+ return {
7725
+ backoffFactor: 2,
7726
+ baseDelayMs: env.envAsNumber(process$1.env['SOCKET_GITHUB_RETRY_BASE_DELAY_MS'], DEFAULT_RETRY_BASE_DELAY_MS),
7727
+ maxDelayMs: 10_000,
7728
+ retries: 2
7729
+ };
7530
7730
  }
7531
- function sleep(ms) {
7532
- return new Promise(resolve => {
7533
- setTimeout(resolve, ms);
7534
- });
7731
+
7732
+ /**
7733
+ * Thrown by one request attempt to tell `pRetry` what happened. `retryable`
7734
+ * false means another attempt cannot help, so the retry loop stops early
7735
+ * instead of burning its budget. The CResult is what the caller sees.
7736
+ */
7737
+ class GitHubRequestFailure extends Error {
7738
+ constructor(result, retryable) {
7739
+ super(result.message);
7740
+ this.name = 'GitHubRequestFailure';
7741
+ this.result = result;
7742
+ this.retryable = retryable;
7743
+ }
7535
7744
  }
7536
7745
 
7537
7746
  /**
@@ -7549,7 +7758,8 @@ function sleep(ms) {
7549
7758
  * short (<= CHEAP_RATE_LIMIT_WAIT_MAX_SECONDS); otherwise surface the error
7550
7759
  * immediately. Long primary-limit resets are not worth blocking on.
7551
7760
  * - Auth: never retried.
7552
- * - 5xx / network: capped exponential backoff, MAX_TRANSIENT_ATTEMPTS total.
7761
+ * - 5xx / network: handed to the fleet's shared `pRetry` for exponential
7762
+ * backoff, on the policy in `githubRetryOptions`.
7553
7763
  */
7554
7764
  async function githubApiRequest(url, init, context,
7555
7765
  // Injectable request implementation. Defaults to the real `apiFetch`;
@@ -7558,72 +7768,89 @@ async function githubApiRequest(url, init, context,
7558
7768
  fetchImpl = apiFetch) {
7559
7769
  const method = init.method || 'GET';
7560
7770
  let rateLimitWaitUsed = false;
7561
- for (let attempt = 1;; attempt += 1) {
7771
+ // pRetry rethrows whichever error it stored first. Track the newest one
7772
+ // ourselves so the caller always sees the failure that actually ended the
7773
+ // run, not an earlier one it had already recovered past.
7774
+ let lastFailure;
7775
+ const fail = (result, retryable) => {
7776
+ const failure = new GitHubRequestFailure(result, retryable);
7777
+ lastFailure = failure;
7778
+ return failure;
7779
+ };
7780
+ const attempt = async () => {
7562
7781
  debugApiRequest(method, url);
7563
7782
  let response;
7564
7783
  try {
7565
- // eslint-disable-next-line no-await-in-loop
7566
7784
  response = await fetchImpl(url, init);
7567
7785
  debugApiResponse(method, url, response.status);
7568
7786
  } catch (e) {
7569
7787
  debugApiResponse(method, url, undefined, e);
7570
- // Network-level failure (DNS, connection reset, timeout). Retry a few
7571
- // times with bounded backoff before giving up.
7572
- if (attempt < MAX_TRANSIENT_ATTEMPTS) {
7573
- debug$2.debugFn('notice', `retry: network error while ${context}`, attempt);
7574
- // eslint-disable-next-line no-await-in-loop
7575
- await sleep(backoffMs(attempt));
7576
- continue;
7577
- }
7578
- return {
7788
+ // Network-level failure (DNS, connection reset, timeout).
7789
+ throw fail({
7579
7790
  ok: false,
7580
7791
  message: 'Network error connecting to GitHub',
7581
7792
  cause: formatErrorWithDetail(`Network error while ${context}`, e)
7582
- };
7793
+ }, true);
7583
7794
  }
7584
-
7585
- // eslint-disable-next-line no-await-in-loop
7586
7795
  const bodyText = await response.text();
7587
7796
  const blocking = classifyGitHubResponse(response.status, response.headers, bodyText, context);
7588
7797
  if (blocking) {
7589
7798
  // Auth failures never succeed on retry.
7590
7799
  if (blocking.message === GITHUB_ERR_AUTH_FAILED) {
7591
- return blocking;
7800
+ throw fail(blocking, false);
7592
7801
  }
7593
- // Rate limit / abuse: retry once, but only when the reset window is
7594
- // short enough to be worth waiting on.
7802
+ // Rate limit / abuse: wait once, but only when the reset window is short
7803
+ // enough to be worth waiting on.
7595
7804
  const waitSeconds = getRateLimitWaitSeconds(response.headers);
7596
7805
  if (!rateLimitWaitUsed && waitSeconds !== undefined && waitSeconds <= CHEAP_RATE_LIMIT_WAIT_MAX_SECONDS) {
7597
7806
  rateLimitWaitUsed = true;
7598
7807
  logger.logger.info(`GitHub rate limit hit while ${context}; waiting ${waitSeconds}s before one retry...`);
7599
- // Add a second of slack so we retry just past the reset boundary.
7600
- // eslint-disable-next-line no-await-in-loop
7601
- await sleep((waitSeconds + 1) * 1000);
7602
- continue;
7808
+ // GitHub told us exactly when the quota comes back, so this wait is
7809
+ // honoring a server instruction rather than backing off. Backoff is
7810
+ // pRetry's job and its delay is capped well below a reset window.
7811
+ // A second of slack lands the retry just past the reset boundary.
7812
+ await require$$7.setTimeout((waitSeconds + 1) * 1000);
7813
+ throw fail(blocking, true);
7603
7814
  }
7604
- return blocking;
7815
+ throw fail(blocking, false);
7605
7816
  }
7606
7817
 
7607
- // Transient server errors: retry with bounded backoff, then surface.
7818
+ // Transient server errors.
7608
7819
  if (response.status >= constants.HTTP_STATUS_INTERNAL_SERVER_ERROR) {
7609
- if (attempt < MAX_TRANSIENT_ATTEMPTS) {
7610
- debug$2.debugFn('notice', `retry: GitHub ${response.status} while ${context}`, attempt);
7611
- // eslint-disable-next-line no-await-in-loop
7612
- await sleep(backoffMs(attempt));
7613
- continue;
7614
- }
7615
- return {
7820
+ throw fail({
7616
7821
  ok: false,
7617
7822
  message: 'GitHub server error',
7618
7823
  cause: `GitHub server error (${response.status}) while ${context}. ` + 'GitHub may be experiencing issues; try again shortly.'
7619
- };
7824
+ }, true);
7620
7825
  }
7826
+ return {
7827
+ response,
7828
+ bodyText
7829
+ };
7830
+ };
7831
+ try {
7832
+ const data = await promises.pRetry(attempt, {
7833
+ ...githubRetryOptions(),
7834
+ onRetry(attemptNumber, e) {
7835
+ if (e instanceof GitHubRequestFailure && !e.retryable) {
7836
+ // Stop now; another attempt cannot change the answer.
7837
+ return false;
7838
+ }
7839
+ debug$2.debugFn('notice', `retry: ${e instanceof Error ? e.message : 'failure'} while ${context}`, attemptNumber);
7840
+ return undefined;
7841
+ },
7842
+ onRetryCancelOnFalse: true
7843
+ });
7621
7844
  return {
7622
7845
  ok: true,
7623
- data: {
7624
- response,
7625
- bodyText
7626
- }
7846
+ data
7847
+ };
7848
+ } catch {
7849
+ /* c8 ignore next - `lastFailure` is set on every throw out of `attempt`. */
7850
+ return lastFailure?.result ?? {
7851
+ ok: false,
7852
+ message: 'GitHub request failed',
7853
+ cause: `GitHub request failed while ${context}.`
7627
7854
  };
7628
7855
  }
7629
7856
  }
@@ -8831,6 +9058,7 @@ exports.readOrDefaultSocketJsonUp = readOrDefaultSocketJsonUp;
8831
9058
  exports.readPnpmLockfile = readPnpmLockfile;
8832
9059
  exports.readSocketJsonCascade = readSocketJsonCascade;
8833
9060
  exports.readSocketJsonSync = readSocketJsonSync;
9061
+ exports.realpathOrResolved = realpathOrResolved;
8834
9062
  exports.runAgentInstall = runAgentInstall;
8835
9063
  exports.runSelfUpdateCheck = runSelfUpdateCheck;
8836
9064
  exports.safeNpa = safeNpa$1;
@@ -8860,5 +9088,5 @@ exports.walkNestedMap = walkNestedMap;
8860
9088
  exports.webLink = webLink;
8861
9089
  exports.withTmpDir = withTmpDir;
8862
9090
  exports.writeSocketJson = writeSocketJson;
8863
- //# debugId=588541a6-eef9-410e-a3c6-6f594e50db6b
9091
+ //# debugId=e72c7655-2d5e-4e48-bfdb-c488b68167b6
8864
9092
  //# sourceMappingURL=utils.js.map