env.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. "use strict";
  2. /**
  3. * Binary discovery + child environment for GUI-context launches.
  4. *
  5. * macOS (Finder/launchd) and Windows (Explorer/scheduled-task) both start GUI
  6. * apps with a minimal PATH that cannot resolve the `#!/usr/bin/env node`
  7. * shebangs of dsh/npm. All spawned children therefore run through childEnv(),
  8. * which prepends the directories that actually hold dsh, npm, and node.
  9. */
  10. const fs = require("node:fs");
  11. const os = require("node:os");
  12. const path = require("node:path");
  13. const IS_WIN = process.platform === "win32";
  14. function firstExisting(candidates) {
  15. for (const candidate of candidates) {
  16. try { if (candidate && fs.existsSync(candidate)) return candidate; } catch { /* next */ }
  17. }
  18. return null;
  19. }
  20. function pathDirs() {
  21. const sep = IS_WIN ? ";" : ":";
  22. return (process.env.PATH || "").split(sep).filter(Boolean);
  23. }
  24. /** The executable extension for node/npm shims (.cmd on Windows, else none). */
  25. function exe(name) { return IS_WIN ? `${name}.cmd` : name; }
  26. /**
  27. * The dsh binary: explicit override, common user install spots, then PATH.
  28. * On Windows, `npm i -g @deepseek-ai/dsh` places dsh.cmd in the npm prefix
  29. * (%APPDATA%\npm); on macOS, ~/.local/bin/dsh (a symlink to the package).
  30. */
  31. function findDsh() {
  32. const override = process.env.DSH_DESKTOP_DSH_BIN;
  33. if (override) return override;
  34. const home = os.homedir();
  35. const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming");
  36. const candidates = IS_WIN ? [
  37. path.join(appdata, "npm", "dsh.cmd"),
  38. path.join(home, ".local", "bin", "dsh.cmd"),
  39. path.join(home, "AppData", "Roaming", "npm", "dsh.cmd"),
  40. ...pathDirs().map((dir) => path.join(dir, "dsh.cmd")),
  41. ] : [
  42. path.join(home, ".local/bin/dsh"),
  43. "/opt/homebrew/bin/dsh",
  44. "/usr/local/bin/dsh",
  45. ...pathDirs().map((dir) => path.join(dir, "dsh")),
  46. ];
  47. return firstExisting(candidates) || "dsh";
  48. }
  49. /** The npm binary: the same discovery strategy. */
  50. function findNpm() {
  51. const home = os.homedir();
  52. const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming");
  53. const candidates = IS_WIN ? [
  54. path.join(appdata, "npm", "npm.cmd"),
  55. path.join(home, ".local", "bin", "npm.cmd"),
  56. path.join(home, "AppData", "Roaming", "npm", "npm.cmd"),
  57. ...pathDirs().map((dir) => path.join(dir, "npm.cmd")),
  58. ] : [
  59. path.join(home, ".local/bin/npm"),
  60. "/opt/homebrew/bin/npm",
  61. "/usr/local/bin/npm",
  62. ...pathDirs().map((dir) => path.join(dir, "npm")),
  63. ];
  64. return firstExisting(candidates) || "npm";
  65. }
  66. const DSH_BIN = findDsh();
  67. const NPM_BIN = findNpm();
  68. /**
  69. * Re-resolve the dsh binary right before every spawn. `npm install -g`
  70. * replaces the bin symlink/cmd during upgrades; a path captured at shell
  71. * startup would then hit ENOENT — exactly the upgrade-race failure in the wild.
  72. */
  73. function resolveDsh() {
  74. return findDsh();
  75. }
  76. /** Environment (PATH augmented) for every child process we spawn. */
  77. function childEnv() {
  78. const sep = IS_WIN ? ";" : ":";
  79. const dirs = [];
  80. for (const bin of [DSH_BIN, NPM_BIN]) {
  81. if (path.isAbsolute(bin)) dirs.push(path.dirname(bin));
  82. try { dirs.push(path.dirname(fs.realpathSync(bin))); } catch { /* not a file */ }
  83. }
  84. if (IS_WIN) {
  85. const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
  86. dirs.push(path.join(appdata, "npm"));
  87. dirs.push(path.join(os.homedir(), ".local", "bin"));
  88. } else {
  89. dirs.push(
  90. path.join(os.homedir(), ".local/bin"),
  91. "/opt/homebrew/bin",
  92. "/usr/local/bin",
  93. );
  94. }
  95. const unique = [...new Set(dirs.filter(Boolean))];
  96. const augmented = unique.join(sep);
  97. const existing = process.env.PATH || "";
  98. return { ...process.env, PATH: existing ? `${augmented}${sep}${existing}` : augmented };
  99. }
  100. module.exports = { DSH_BIN, NPM_BIN, findDsh, findNpm, resolveDsh, childEnv, IS_WIN };