system.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { spawnSync } from 'node:child_process';
  2. import { statSync } from 'node:fs';
  3. let cachedOpenCodePath: string | null = null;
  4. function resolvePathCommand(command: string): string | null {
  5. try {
  6. const resolver = process.platform === 'win32' ? 'where' : 'which';
  7. const result = spawnSync(resolver, [command], {
  8. encoding: 'utf-8',
  9. stdio: ['ignore', 'pipe', 'ignore'],
  10. });
  11. if (result.status !== 0) {
  12. return null;
  13. }
  14. const resolved = result.stdout
  15. .split(/\r?\n/)
  16. .map((line) => line.trim())
  17. .find(Boolean);
  18. return resolved ?? null;
  19. } catch {
  20. return null;
  21. }
  22. }
  23. function canExecute(command: string, args: string[]): boolean {
  24. try {
  25. const result = spawnSync(command, args, {
  26. stdio: 'ignore',
  27. });
  28. return result.status === 0;
  29. } catch {
  30. return false;
  31. }
  32. }
  33. function getOpenCodePaths(): string[] {
  34. const home = process.env.HOME || process.env.USERPROFILE || '';
  35. return [
  36. // PATH (try this first)
  37. 'opencode',
  38. // User local installations (Linux & macOS)
  39. `${home}/.local/bin/opencode`,
  40. `${home}/.opencode/bin/opencode`,
  41. `${home}/bin/opencode`,
  42. // System-wide installations
  43. '/usr/local/bin/opencode',
  44. '/opt/opencode/bin/opencode',
  45. '/usr/bin/opencode',
  46. '/bin/opencode',
  47. // macOS specific
  48. '/Applications/OpenCode.app/Contents/MacOS/opencode',
  49. `${home}/Applications/OpenCode.app/Contents/MacOS/opencode`,
  50. // Homebrew (macOS & Linux)
  51. '/opt/homebrew/bin/opencode',
  52. '/home/linuxbrew/.linuxbrew/bin/opencode',
  53. `${home}/homebrew/bin/opencode`,
  54. // macOS user Library
  55. `${home}/Library/Application Support/opencode/bin/opencode`,
  56. // Snap (Linux)
  57. '/snap/bin/opencode',
  58. '/var/snap/opencode/current/bin/opencode',
  59. // Flatpak (Linux)
  60. '/var/lib/flatpak/exports/bin/ai.opencode.OpenCode',
  61. `${home}/.local/share/flatpak/exports/bin/ai.opencode.OpenCode`,
  62. // Nix (Linux/macOS)
  63. '/nix/store/opencode/bin/opencode',
  64. `${home}/.nix-profile/bin/opencode`,
  65. '/run/current-system/sw/bin/opencode',
  66. // Cargo (Rust toolchain)
  67. `${home}/.cargo/bin/opencode`,
  68. // npm/npx global
  69. `${home}/.npm-global/bin/opencode`,
  70. '/usr/local/lib/node_modules/opencode/bin/opencode',
  71. // Yarn global
  72. `${home}/.yarn/bin/opencode`,
  73. // PNPM
  74. `${home}/.pnpm-global/bin/opencode`,
  75. ];
  76. }
  77. export function resolveOpenCodePath(): string {
  78. if (cachedOpenCodePath) {
  79. return cachedOpenCodePath;
  80. }
  81. const pathOpenCodePath = resolvePathCommand('opencode');
  82. if (pathOpenCodePath) {
  83. cachedOpenCodePath = pathOpenCodePath;
  84. return pathOpenCodePath;
  85. }
  86. const paths = getOpenCodePaths();
  87. for (const opencodePath of paths) {
  88. if (opencodePath === 'opencode') continue;
  89. try {
  90. const stat = statSync(opencodePath);
  91. if (stat.isFile()) {
  92. cachedOpenCodePath = opencodePath;
  93. return opencodePath;
  94. }
  95. } catch {
  96. // Try next path
  97. }
  98. }
  99. // Fallback to 'opencode' and hope it's in PATH
  100. return 'opencode';
  101. }
  102. export async function isOpenCodeInstalled(): Promise<boolean> {
  103. const pathOpenCodePath = resolvePathCommand('opencode');
  104. if (pathOpenCodePath && canExecute(pathOpenCodePath, ['--version'])) {
  105. cachedOpenCodePath = pathOpenCodePath;
  106. return true;
  107. }
  108. const paths = getOpenCodePaths();
  109. for (const opencodePath of paths) {
  110. if (opencodePath === 'opencode') continue;
  111. if (canExecute(opencodePath, ['--version'])) {
  112. cachedOpenCodePath = opencodePath;
  113. return true;
  114. }
  115. }
  116. return false;
  117. }
  118. export async function isTmuxInstalled(): Promise<boolean> {
  119. return canExecute('tmux', ['-V']);
  120. }
  121. export async function getOpenCodeVersion(): Promise<string | null> {
  122. const opencodePath = resolveOpenCodePath();
  123. try {
  124. const result = spawnSync(opencodePath, ['--version'], {
  125. encoding: 'utf-8',
  126. stdio: ['ignore', 'pipe', 'ignore'],
  127. });
  128. if (result.status === 0) {
  129. return result.stdout.trim();
  130. }
  131. } catch {
  132. // Failed
  133. }
  134. return null;
  135. }
  136. export function getOpenCodePath(): string | null {
  137. const path = resolveOpenCodePath();
  138. return path === 'opencode' ? null : path;
  139. }
  140. export async function fetchLatestVersion(
  141. packageName: string,
  142. ): Promise<string | null> {
  143. try {
  144. const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
  145. if (!res.ok) return null;
  146. const data = (await res.json()) as { version: string };
  147. return data.version;
  148. } catch {
  149. return null;
  150. }
  151. }