cli/src/install-sapling.tsblame
View source
17fe7cd1import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync, unlinkSync, symlinkSync } from "node:fs";
17fe7cd2import { join } from "node:path";
17fe7cd3import { homedir, platform, arch } from "node:os";
17fe7cd4import { execSync } from "node:child_process";
17fe7cd5
17fe7cd6/**
17fe7cd7 * Download and install the Sapling (sl) binary to ~/.grove/sapling,
17fe7cd8 * symlink to ~/.grove/bin/sl, and ensure ~/.grove/bin is on PATH.
17fe7cd9 * Returns a status message or null if no install was needed.
17fe7cd10 */
17fe7cd11export async function downloadAndInstallSapling(hub: string): Promise<{ installed: boolean; message: string } | null> {
17fe7cd12 const os = platform();
17fe7cd13 if (os !== "linux" && os !== "darwin") return null;
17fe7cd14
17fe7cd15 const archName = arch() === "x64" ? "x86_64" : arch();
17fe7cd16 const tarball = `sl-${os}-${archName}.tar.gz`;
17fe7cd17 const res = await fetch(`${hub}/downloads/${tarball}`);
17fe7cd18 if (!res.ok) {
17fe7cd19 return { installed: false, message: "Sapling binary not available for this platform yet" };
17fe7cd20 }
17fe7cd21 const buf = Buffer.from(await res.arrayBuffer());
17fe7cd22
17fe7cd23 const groveDir = join(homedir(), ".grove");
17fe7cd24 const installDir = join(groveDir, "sapling");
17fe7cd25 const binDir = join(groveDir, "bin");
17fe7cd26
17fe7cd27 const tmpTar = join(groveDir, "sl-install.tar.gz");
17fe7cd28 mkdirSync(installDir, { recursive: true });
17fe7cd29 mkdirSync(binDir, { recursive: true });
17fe7cd30 writeFileSync(tmpTar, buf);
17fe7cd31 execSync(`rm -rf ${installDir} && mkdir -p ${installDir} && tar xzf ${tmpTar} -C ${installDir}`, { stdio: "pipe" });
17fe7cd32 unlinkSync(tmpTar);
17fe7cd33
17fe7cd34 const symlink = join(binDir, "sl");
17fe7cd35 try { unlinkSync(symlink); } catch {}
17fe7cd36 symlinkSync(join(installDir, "sl"), symlink);
17fe7cd37
17fe7cd38 return { installed: true, message: "Sapling (sl) installed to ~/.grove/bin/sl" };
17fe7cd39}
17fe7cd40
17fe7cd41/**
17fe7cd42 * Ensure ~/.grove/bin is in the user's shell PATH.
17fe7cd43 * Returns a message about what was done.
17fe7cd44 */
17fe7cd45export function ensureGroveBinOnPath(): { added: boolean; message: string } {
17fe7cd46 const os = platform();
17fe7cd47 const binDir = join(homedir(), ".grove", "bin");
17fe7cd48 const pathDirs = (process.env.PATH || "").split(":");
17fe7cd49
17fe7cd50 if (pathDirs.includes(binDir)) {
17fe7cd51 return { added: false, message: "~/.grove/bin already on PATH" };
17fe7cd52 }
17fe7cd53
17fe7cd54 // Detect the user's actual shell to pick the right rc file
17fe7cd55 const shell = process.env.SHELL || "";
17fe7cd56 let rcName: string;
17fe7cd57 if (shell.endsWith("/zsh")) {
17fe7cd58 rcName = ".zshrc";
17fe7cd59 } else if (shell.endsWith("/bash")) {
17fe7cd60 rcName = ".bash_profile";
17fe7cd61 } else if (shell.endsWith("/fish")) {
17fe7cd62 // fish uses a different syntax, but PATH export still works via conf.d
17fe7cd63 rcName = ".bash_profile";
17fe7cd64 } else {
17fe7cd65 rcName = os === "linux" ? ".bashrc" : ".zshrc";
17fe7cd66 }
17fe7cd67 const rcFile = join(homedir(), rcName);
17fe7cd68 const exportLine = `export PATH="$HOME/.grove/bin:$PATH"`;
17fe7cd69
17fe7cd70 try {
17fe7cd71 const existing = existsSync(rcFile) ? readFileSync(rcFile, "utf-8") : "";
17fe7cd72 if (!existing.includes(".grove/bin")) {
17fe7cd73 appendFileSync(rcFile, `\n# Added by Grove\n${exportLine}\n`);
17fe7cd74 }
17fe7cd75 return { added: true, message: `Added to ~/${rcName} — restart your shell or run: source ~/${rcName}` };
17fe7cd76 } catch {
17fe7cd77 return { added: false, message: `Could not write to ~/${rcName}` };
17fe7cd78 }
17fe7cd79}
17fe7cd80
17fe7cd81/**
17fe7cd82 * Check if sl is already installed and up-to-date.
17fe7cd83 */
17fe7cd84export function isSaplingInstalled(): { installed: boolean; outdated: boolean; version?: string } {
17fe7cd85 try {
17fe7cd86 const version = execSync("sl --version 2>&1", { stdio: "pipe" }).toString().trim();
17fe7cd87 const firstLine = version.split("\n")[0];
17fe7cd88 if (firstLine.includes("0.2.")) {
17fe7cd89 return { installed: true, outdated: true, version: firstLine };
17fe7cd90 }
17fe7cd91 return { installed: true, outdated: false, version: firstLine };
17fe7cd92 } catch {
17fe7cd93 return { installed: false, outdated: false };
17fe7cd94 }
17fe7cd95}