env.js 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. "use strict";
  2. /**
  3. * Binary discovery + child environment for GUI-context launches.
  4. *
  5. * From Finder/launchd an app inherits PATH=/usr/bin:/bin:/usr/sbin:/sbin — the
  6. * `#!/usr/bin/env node` shebangs of dsh and npm cannot resolve there. All
  7. * spawned children therefore run through childEnv(), which prepends the
  8. * directories that actually hold dsh, npm, and node: both their symlink spots
  9. * (~/.local/bin) and their realpath homes (e.g. a version manager under
  10. * ~/.hermes/node/bin).
  11. */
  12. const fs = require("node:fs");
  13. const os = require("node:os");
  14. const path = require("node:path");
  15. function firstExisting(candidates) {
  16. for (const candidate of candidates) {
  17. try { if (candidate && fs.existsSync(candidate)) return candidate; } catch { /* next */ }
  18. }
  19. return null;
  20. }
  21. function pathDirs() {
  22. return (process.env.PATH || "").split(":").filter(Boolean);
  23. }
  24. /** The dsh binary: explicit override, common user install spots, then PATH. */
  25. function findDsh() {
  26. const override = process.env.DSH_DESKTOP_DSH_BIN;
  27. if (override) return override;
  28. const home = os.homedir();
  29. return firstExisting([
  30. path.join(home, ".local/bin/dsh"),
  31. "/opt/homebrew/bin/dsh",
  32. "/usr/local/bin/dsh",
  33. ...pathDirs().map((dir) => path.join(dir, "dsh")),
  34. ]) || "dsh";
  35. }
  36. /** The npm binary: the same discovery strategy. */
  37. function findNpm() {
  38. const home = os.homedir();
  39. return firstExisting([
  40. path.join(home, ".local/bin/npm"),
  41. "/opt/homebrew/bin/npm",
  42. "/usr/local/bin/npm",
  43. ...pathDirs().map((dir) => path.join(dir, "npm")),
  44. ]) || "npm";
  45. }
  46. const DSH_BIN = findDsh();
  47. const NPM_BIN = findNpm();
  48. /**
  49. * Re-resolve the dsh binary right before every spawn. `npm install -g`
  50. * replaces the bin symlink during upgrades; a path captured at shell startup
  51. * (or a bare "dsh" fallback resolved while the link was missing) would then
  52. * hit ENOENT — exactly the upgrade-race failure seen in the wild.
  53. */
  54. function resolveDsh() {
  55. return findDsh();
  56. }
  57. /** Environment (PATH augmented) for every child process we spawn. */
  58. function childEnv() {
  59. const dirs = [];
  60. for (const bin of [DSH_BIN, NPM_BIN]) {
  61. if (path.isAbsolute(bin)) dirs.push(path.dirname(bin));
  62. try { dirs.push(path.dirname(fs.realpathSync(bin))); } catch { /* not a file */ }
  63. }
  64. dirs.push(
  65. path.join(os.homedir(), ".local/bin"),
  66. "/opt/homebrew/bin",
  67. "/usr/local/bin",
  68. );
  69. const unique = [...new Set(dirs.filter(Boolean))];
  70. return { ...process.env, PATH: `${unique.join(":")}:${process.env.PATH || ""}` };
  71. }
  72. module.exports = { DSH_BIN, NPM_BIN, findDsh, findNpm, resolveDsh, childEnv };