| 17fe7cd | | | 1 | import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync, unlinkSync, symlinkSync } from "node:fs"; |
| 17fe7cd | | | 2 | import { join } from "node:path"; |
| 17fe7cd | | | 3 | import { homedir, platform, arch } from "node:os"; |
| 17fe7cd | | | 4 | import { execSync } from "node:child_process"; |
| 17fe7cd | | | 5 | |
| 17fe7cd | | | 6 | /** |
| 17fe7cd | | | 7 | * Download and install the Sapling (sl) binary to ~/.grove/sapling, |
| 17fe7cd | | | 8 | * symlink to ~/.grove/bin/sl, and ensure ~/.grove/bin is on PATH. |
| 17fe7cd | | | 9 | * Returns a status message or null if no install was needed. |
| 17fe7cd | | | 10 | */ |
| 17fe7cd | | | 11 | export async function downloadAndInstallSapling(hub: string): Promise<{ installed: boolean; message: string } | null> { |
| 17fe7cd | | | 12 | const os = platform(); |
| 17fe7cd | | | 13 | if (os !== "linux" && os !== "darwin") return null; |
| 17fe7cd | | | 14 | |
| 17fe7cd | | | 15 | const archName = arch() === "x64" ? "x86_64" : arch(); |
| 17fe7cd | | | 16 | const tarball = `sl-${os}-${archName}.tar.gz`; |
| 17fe7cd | | | 17 | const res = await fetch(`${hub}/downloads/${tarball}`); |
| 17fe7cd | | | 18 | if (!res.ok) { |
| 17fe7cd | | | 19 | return { installed: false, message: "Sapling binary not available for this platform yet" }; |
| 17fe7cd | | | 20 | } |
| 17fe7cd | | | 21 | const buf = Buffer.from(await res.arrayBuffer()); |
| 17fe7cd | | | 22 | |
| 17fe7cd | | | 23 | const groveDir = join(homedir(), ".grove"); |
| 17fe7cd | | | 24 | const installDir = join(groveDir, "sapling"); |
| 17fe7cd | | | 25 | const binDir = join(groveDir, "bin"); |
| 17fe7cd | | | 26 | |
| 17fe7cd | | | 27 | const tmpTar = join(groveDir, "sl-install.tar.gz"); |
| 17fe7cd | | | 28 | mkdirSync(installDir, { recursive: true }); |
| 17fe7cd | | | 29 | mkdirSync(binDir, { recursive: true }); |
| 17fe7cd | | | 30 | writeFileSync(tmpTar, buf); |
| 17fe7cd | | | 31 | execSync(`rm -rf ${installDir} && mkdir -p ${installDir} && tar xzf ${tmpTar} -C ${installDir}`, { stdio: "pipe" }); |
| 17fe7cd | | | 32 | unlinkSync(tmpTar); |
| 17fe7cd | | | 33 | |
| 17fe7cd | | | 34 | const symlink = join(binDir, "sl"); |
| 17fe7cd | | | 35 | try { unlinkSync(symlink); } catch {} |
| 17fe7cd | | | 36 | symlinkSync(join(installDir, "sl"), symlink); |
| 17fe7cd | | | 37 | |
| 17fe7cd | | | 38 | return { installed: true, message: "Sapling (sl) installed to ~/.grove/bin/sl" }; |
| 17fe7cd | | | 39 | } |
| 17fe7cd | | | 40 | |
| 17fe7cd | | | 41 | /** |
| 17fe7cd | | | 42 | * Ensure ~/.grove/bin is in the user's shell PATH. |
| 17fe7cd | | | 43 | * Returns a message about what was done. |
| 17fe7cd | | | 44 | */ |
| 17fe7cd | | | 45 | export function ensureGroveBinOnPath(): { added: boolean; message: string } { |
| 17fe7cd | | | 46 | const os = platform(); |
| 17fe7cd | | | 47 | const binDir = join(homedir(), ".grove", "bin"); |
| 17fe7cd | | | 48 | const pathDirs = (process.env.PATH || "").split(":"); |
| 17fe7cd | | | 49 | |
| 17fe7cd | | | 50 | if (pathDirs.includes(binDir)) { |
| 17fe7cd | | | 51 | return { added: false, message: "~/.grove/bin already on PATH" }; |
| 17fe7cd | | | 52 | } |
| 17fe7cd | | | 53 | |
| 17fe7cd | | | 54 | // Detect the user's actual shell to pick the right rc file |
| 17fe7cd | | | 55 | const shell = process.env.SHELL || ""; |
| 17fe7cd | | | 56 | let rcName: string; |
| 17fe7cd | | | 57 | if (shell.endsWith("/zsh")) { |
| 17fe7cd | | | 58 | rcName = ".zshrc"; |
| 17fe7cd | | | 59 | } else if (shell.endsWith("/bash")) { |
| 17fe7cd | | | 60 | rcName = ".bash_profile"; |
| 17fe7cd | | | 61 | } else if (shell.endsWith("/fish")) { |
| 17fe7cd | | | 62 | // fish uses a different syntax, but PATH export still works via conf.d |
| 17fe7cd | | | 63 | rcName = ".bash_profile"; |
| 17fe7cd | | | 64 | } else { |
| 17fe7cd | | | 65 | rcName = os === "linux" ? ".bashrc" : ".zshrc"; |
| 17fe7cd | | | 66 | } |
| 17fe7cd | | | 67 | const rcFile = join(homedir(), rcName); |
| 17fe7cd | | | 68 | const exportLine = `export PATH="$HOME/.grove/bin:$PATH"`; |
| 17fe7cd | | | 69 | |
| 17fe7cd | | | 70 | try { |
| 17fe7cd | | | 71 | const existing = existsSync(rcFile) ? readFileSync(rcFile, "utf-8") : ""; |
| 17fe7cd | | | 72 | if (!existing.includes(".grove/bin")) { |
| 17fe7cd | | | 73 | appendFileSync(rcFile, `\n# Added by Grove\n${exportLine}\n`); |
| 17fe7cd | | | 74 | } |
| 17fe7cd | | | 75 | return { added: true, message: `Added to ~/${rcName} — restart your shell or run: source ~/${rcName}` }; |
| 17fe7cd | | | 76 | } catch { |
| 17fe7cd | | | 77 | return { added: false, message: `Could not write to ~/${rcName}` }; |
| 17fe7cd | | | 78 | } |
| 17fe7cd | | | 79 | } |
| 17fe7cd | | | 80 | |
| 17fe7cd | | | 81 | /** |
| 17fe7cd | | | 82 | * Check if sl is already installed and up-to-date. |
| 17fe7cd | | | 83 | */ |
| 17fe7cd | | | 84 | export function isSaplingInstalled(): { installed: boolean; outdated: boolean; version?: string } { |
| 17fe7cd | | | 85 | try { |
| 17fe7cd | | | 86 | const version = execSync("sl --version 2>&1", { stdio: "pipe" }).toString().trim(); |
| 17fe7cd | | | 87 | const firstLine = version.split("\n")[0]; |
| 17fe7cd | | | 88 | if (firstLine.includes("0.2.")) { |
| 17fe7cd | | | 89 | return { installed: true, outdated: true, version: firstLine }; |
| 17fe7cd | | | 90 | } |
| 17fe7cd | | | 91 | return { installed: true, outdated: false, version: firstLine }; |
| 17fe7cd | | | 92 | } catch { |
| 17fe7cd | | | 93 | return { installed: false, outdated: false }; |
| 17fe7cd | | | 94 | } |
| 17fe7cd | | | 95 | } |