serve sl binary via /downloads, install during grove auth login

- Caddy route /downloads/* serves files from /opt/grove/downloads
- Mount downloads dir in Caddy container
- CLI installs sl binary on Linux if old version (0.2) detected
Anton Kaminsky22d ago4ae9b20c8397parent 31abc92
3 files changed+43-2
cli/src/commands/auth-login.ts
@@ -1,7 +1,8 @@
11import { intro, outro, spinner, log } from "@clack/prompts";
2import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2import { mkdirSync, writeFileSync, existsSync, chmodSync } from "node:fs";
33import { join } from "node:path";
4import { homedir } from "node:os";
4import { homedir, platform, arch } from "node:os";
5import { execSync } from "node:child_process";
56import { waitForAuthCallback } from "../auth-server.js";
67import { loadConfig, saveConfig } from "../config.js";
78
@@ -67,6 +68,38 @@
6768 }
6869}
6970
71async function installSapling(hub: string) {
72 // Only install on Linux — macOS users build from source
73 if (platform() !== "linux") return;
74
75 // Check if sl is already installed and working
76 try {
77 const version = execSync("sl --version 2>&1", { stdio: "pipe" }).toString();
78 // Our builds are version 4.x+, old official release is 0.2
79 if (!version.includes("0.2.")) return;
80 } catch {
81 // sl not found — need to install
82 }
83
84 const s = spinner();
85 s.start("Installing Sapling (sl)");
86 try {
87 const binary = `sl-linux-${arch() === "x64" ? "x86_64" : arch()}`;
88 const res = await fetch(`${hub}/downloads/${binary}`);
89 if (!res.ok) {
90 s.stop("Sapling binary not available yet (non-fatal)");
91 return;
92 }
93 const buf = Buffer.from(await res.arrayBuffer());
94 const dest = "/usr/local/bin/sl";
95 writeFileSync(dest, buf);
96 chmodSync(dest, 0o755);
97 s.stop("Sapling (sl) installed");
98 } catch {
99 s.stop("Could not install Sapling (non-fatal)");
100 }
101}
102
70103export async function authLogin(args: string[]) {
71104 const config = await loadConfig();
72105
@@ -85,6 +118,7 @@
85118 config.token = token;
86119 await saveConfig(config);
87120 await provisionTlsCerts(config.hub, token);
121 await installSapling(config.hub);
88122 outro("Authenticated! Token saved to ~/.grove/config.json");
89123 } catch (err: any) {
90124 log.error(err.message);
@@ -113,6 +147,7 @@
113147 config.token = token;
114148 await saveConfig(config);
115149 await provisionTlsCerts(config.hub, token);
150 await installSapling(config.hub);
116151 outro("Authenticated! Token saved to ~/.grove/config.json");
117152 } catch (err: any) {
118153 s.stop("Authentication failed");
119154
hub/Caddyfile
@@ -145,6 +145,11 @@
145145 reverse_proxy grove-api:4000
146146 }
147147
148 handle /downloads/* {
149 root * /srv
150 file_server
151 }
152
148153 handle /health {
149154 reverse_proxy hub-api:4000
150155 }
151156
hub/docker-compose.yml
@@ -153,6 +153,7 @@
153153 - caddy-data:/data
154154 - caddy-config:/config
155155 - pages-sites:/srv/pages/sites:ro
156 - /opt/grove/downloads:/srv/downloads:ro
156157 environment:
157158 - DOMAIN=${DOMAIN:-localhost}
158159 depends_on:
159160