Configure Sapling ui.username on auth login, serve macOS sl binary, publish CLI under @letterpress-labs/grove-scm

- auth-login now sets ui.username in global sapling config after login
- installSapling now works on macOS (downloads sl-darwin-arm64)
- Renamed npm package from grove-scm to @letterpress-labs/grove-scm
- Bumped CLI to 0.1.5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Anton Kaminsky21d ago7a611b5ad230parent 6210853
2 files changed+36-6
cli/package.json
@@ -1,6 +1,6 @@
11{
2 "name": "grove-scm",
3 "version": "0.1.4",
2 "name": "@letterpress-labs/grove-scm",
3 "version": "0.1.5",
44 "description": "CLI for Grove — self-hosted source control built on Sapling and Mononoke",
55 "type": "module",
66 "bin": {
77
cli/src/commands/auth-login.ts
@@ -1,5 +1,5 @@
11import { intro, outro, spinner, log } from "@clack/prompts";
2import { mkdirSync, writeFileSync, existsSync, chmodSync } from "node:fs";
2import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync } from "node:fs";
33import { join } from "node:path";
44import { homedir, platform, arch } from "node:os";
55import { execSync } from "node:child_process";
@@ -68,9 +68,34 @@
6868 }
6969}
7070
71function getSaplingConfigPath(): string {
72 if (platform() === "darwin") {
73 return join(homedir(), "Library", "Preferences", "sapling", "sapling.conf");
74 }
75 return join(homedir(), ".config", "sapling", "sapling.conf");
76}
77
78function configureSaplingUsername(username: string) {
79 try {
80 const configPath = getSaplingConfigPath();
81 mkdirSync(join(configPath, ".."), { recursive: true });
82
83 if (existsSync(configPath)) {
84 const existing = readFileSync(configPath, "utf-8");
85 if (existing.includes("username =")) return;
86 // Prepend [ui] section
87 writeFileSync(configPath, `[ui]\nusername = ${username}\n\n` + existing);
88 } else {
89 writeFileSync(configPath, `[ui]\nusername = ${username}\n`);
90 }
91 } catch {
92 // non-fatal
93 }
94}
95
7196async function installSapling(hub: string) {
72 // Only install on Linux — macOS users build from source
73 if (platform() !== "linux") return;
97 const os = platform();
98 if (os !== "linux" && os !== "darwin") return;
7499
75100 // Check if sl is already installed and working
76101 try {
@@ -84,7 +109,8 @@
84109 const s = spinner();
85110 s.start("Installing Sapling (sl)");
86111 try {
87 const binary = `sl-linux-${arch() === "x64" ? "x86_64" : arch()}`;
112 const archName = arch() === "x64" ? "x86_64" : arch();
113 const binary = `sl-${os}-${archName}`;
88114 const res = await fetch(`${hub}/downloads/${binary}`);
89115 if (!res.ok) {
90116 s.stop("Sapling binary not available yet (non-fatal)");
@@ -118,6 +144,8 @@
118144 config.token = token;
119145 await saveConfig(config);
120146 await provisionTlsCerts(config.hub, token);
147 const payload = JSON.parse(atob(token.split(".")[1]));
148 configureSaplingUsername(`${payload.display_name || payload.username} <${payload.username}@grove.host>`);
121149 await installSapling(config.hub);
122150 outro("Authenticated! Token saved to ~/.grove/config.json");
123151 } catch (err: any) {
@@ -147,6 +175,8 @@
147175 config.token = token;
148176 await saveConfig(config);
149177 await provisionTlsCerts(config.hub, token);
178 const payload = JSON.parse(atob(token.split(".")[1]));
179 configureSaplingUsername(`${payload.display_name || payload.username} <${payload.username}@grove.host>`);
150180 await installSapling(config.hub);
151181 outro("Authenticated! Token saved to ~/.grove/config.json");
152182 } catch (err: any) {
153183