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