downloader.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import {
  2. chmodSync,
  3. existsSync,
  4. mkdirSync,
  5. readdirSync,
  6. unlinkSync,
  7. } from 'node:fs';
  8. import { join } from 'node:path';
  9. import { spawn } from 'bun';
  10. import { extractZip } from '../../utils';
  11. export function findFileRecursive(
  12. dir: string,
  13. filename: string,
  14. ): string | null {
  15. try {
  16. const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
  17. for (const entry of entries) {
  18. if (entry.isFile() && entry.name === filename) {
  19. return join(entry.parentPath ?? dir, entry.name);
  20. }
  21. }
  22. } catch {
  23. return null;
  24. }
  25. return null;
  26. }
  27. const RG_VERSION = '14.1.1';
  28. // Platform key format: ${process.platform}-${process.arch} (consistent with ast-grep)
  29. const PLATFORM_CONFIG: Record<
  30. string,
  31. { platform: string; extension: 'tar.gz' | 'zip' } | undefined
  32. > = {
  33. 'darwin-arm64': { platform: 'aarch64-apple-darwin', extension: 'tar.gz' },
  34. 'darwin-x64': { platform: 'x86_64-apple-darwin', extension: 'tar.gz' },
  35. 'linux-arm64': { platform: 'aarch64-unknown-linux-gnu', extension: 'tar.gz' },
  36. 'linux-x64': { platform: 'x86_64-unknown-linux-musl', extension: 'tar.gz' },
  37. 'win32-x64': { platform: 'x86_64-pc-windows-msvc', extension: 'zip' },
  38. };
  39. function getPlatformKey(): string {
  40. return `${process.platform}-${process.arch}`;
  41. }
  42. function getInstallDir(): string {
  43. const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
  44. return join(homeDir, '.cache', 'oh-my-opencode-slim', 'bin');
  45. }
  46. function getRgPath(): string {
  47. const isWindows = process.platform === 'win32';
  48. return join(getInstallDir(), isWindows ? 'rg.exe' : 'rg');
  49. }
  50. async function downloadFile(url: string, destPath: string): Promise<void> {
  51. const response = await fetch(url);
  52. if (!response.ok) {
  53. throw new Error(
  54. `Failed to download: ${response.status} ${response.statusText}`,
  55. );
  56. }
  57. const buffer = await response.arrayBuffer();
  58. await Bun.write(destPath, buffer);
  59. }
  60. async function extractTarGz(
  61. archivePath: string,
  62. destDir: string,
  63. ): Promise<void> {
  64. const args = ['tar', '-xzf', archivePath, '--strip-components=1'];
  65. if (process.platform === 'darwin') {
  66. args.push('--include=*/rg');
  67. } else if (process.platform === 'linux') {
  68. args.push('--wildcards', '*/rg');
  69. }
  70. const proc = spawn(args, {
  71. cwd: destDir,
  72. stdout: 'pipe',
  73. stderr: 'pipe',
  74. });
  75. const exitCode = await proc.exited;
  76. if (exitCode !== 0) {
  77. const stderr = await new Response(proc.stderr).text();
  78. throw new Error(`Failed to extract tar.gz: ${stderr}`);
  79. }
  80. }
  81. async function extractZipArchive(
  82. archivePath: string,
  83. destDir: string,
  84. ): Promise<void> {
  85. await extractZip(archivePath, destDir);
  86. const binaryName = process.platform === 'win32' ? 'rg.exe' : 'rg';
  87. const foundPath = findFileRecursive(destDir, binaryName);
  88. if (foundPath) {
  89. const destPath = join(destDir, binaryName);
  90. if (foundPath !== destPath) {
  91. const { renameSync } = await import('node:fs');
  92. renameSync(foundPath, destPath);
  93. }
  94. }
  95. }
  96. export async function downloadAndInstallRipgrep(): Promise<string> {
  97. const platformKey = getPlatformKey();
  98. const config = PLATFORM_CONFIG[platformKey];
  99. if (!config) {
  100. throw new Error(`Unsupported platform: ${platformKey}`);
  101. }
  102. const installDir = getInstallDir();
  103. const rgPath = getRgPath();
  104. if (existsSync(rgPath)) {
  105. return rgPath;
  106. }
  107. mkdirSync(installDir, { recursive: true });
  108. const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`;
  109. const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
  110. const archivePath = join(installDir, filename);
  111. try {
  112. console.log(`[oh-my-opencode-slim] Downloading ripgrep...`);
  113. await downloadFile(url, archivePath);
  114. if (config.extension === 'tar.gz') {
  115. await extractTarGz(archivePath, installDir);
  116. } else {
  117. await extractZipArchive(archivePath, installDir);
  118. }
  119. if (process.platform !== 'win32') {
  120. chmodSync(rgPath, 0o755);
  121. }
  122. if (!existsSync(rgPath)) {
  123. throw new Error('ripgrep binary not found after extraction');
  124. }
  125. console.log(`[oh-my-opencode-slim] ripgrep ready.`);
  126. return rgPath;
  127. } finally {
  128. if (existsSync(archivePath)) {
  129. try {
  130. unlinkSync(archivePath);
  131. } catch {
  132. // Cleanup failures are non-critical
  133. }
  134. }
  135. }
  136. }
  137. export function getInstalledRipgrepPath(): string | null {
  138. const rgPath = getRgPath();
  139. return existsSync(rgPath) ? rgPath : null;
  140. }