cli/src/config.tsblame
View source
59e66671import { log } from "@clack/prompts";
e93a9782import { readFile, writeFile, mkdir } from "node:fs/promises";
69d1a723import { existsSync, readFileSync } from "node:fs";
e93a9784import { homedir } from "node:os";
69d1a725import { join, resolve, dirname } from "node:path";
e93a9786
e93a9787export interface GroveConfig {
e93a9788 hub: string;
e93a9789 token?: string;
e93a97810}
e93a97811
e93a97812const CONFIG_DIR = join(homedir(), ".grove");
e93a97813const CONFIG_PATH = join(CONFIG_DIR, "config.json");
e93a97814
e93a97815const DEFAULT_CONFIG: GroveConfig = {
e93a97816 hub: "https://grove.host",
e93a97817};
e93a97818
e93a97819export async function loadConfig(): Promise<GroveConfig> {
e93a97820 try {
e93a97821 const raw = await readFile(CONFIG_PATH, "utf-8");
e93a97822 return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
e93a97823 } catch {
e93a97824 return { ...DEFAULT_CONFIG };
e93a97825 }
e93a97826}
e93a97827
e93a97828export async function saveConfig(config: GroveConfig): Promise<void> {
e93a97829 await mkdir(CONFIG_DIR, { recursive: true });
e93a97830 await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
e93a97831 mode: 0o600,
e93a97832 });
e93a97833}
e93a97834
e93a97835export async function getToken(): Promise<string> {
e93a97836 const config = await loadConfig();
e93a97837 if (!config.token) {
59e666738 log.error("Not logged in. Run: grove auth login");
e93a97839 process.exit(1);
e93a97840 }
e93a97841 return config.token;
e93a97842}
e93a97843
e93a97844export async function getHub(): Promise<string> {
e93a97845 const config = await loadConfig();
e93a97846 return config.hub;
e93a97847}
69d1a7248
69d1a7249/**
69d1a7250 * Infer the repo name from the current directory's .sl/config.
69d1a7251 * Looks for [remotefilelog] reponame = <name>.
69d1a7252 */
69d1a7253function inferRepoName(): string | null {
69d1a7254 let dir = process.cwd();
69d1a7255 while (dir !== dirname(dir)) {
69d1a7256 const configPath = join(dir, ".sl", "config");
69d1a7257 if (existsSync(configPath)) {
69d1a7258 const content = readFileSync(configPath, "utf-8");
69d1a7259 const match = content.match(/reponame\s*=\s*(.+)/);
69d1a7260 if (match) return match[1].trim();
69d1a7261 }
69d1a7262 dir = dirname(dir);
69d1a7263 }
69d1a7264 return null;
69d1a7265}
69d1a7266
69d1a7267/**
69d1a7268 * Get the repo slug (owner/repo) from --repo arg or infer from .sl/config.
69d1a7269 * If inferred, queries the API to find the owner.
69d1a7270 */
69d1a7271export async function getRepoSlug(args: string[]): Promise<string> {
69d1a7272 const repoIdx = args.indexOf("--repo");
69d1a7273 if (repoIdx !== -1 && args[repoIdx + 1]) {
69d1a7274 return args[repoIdx + 1];
69d1a7275 }
69d1a7276
69d1a7277 const repoName = inferRepoName();
69d1a7278 if (!repoName) {
59e666779 log.error("Could not infer repo. Use --repo <owner/repo> or run from a Sapling repo.");
69d1a7280 process.exit(1);
69d1a7281 }
69d1a7282
69d1a7283 // Need to look up the owner via the API
69d1a7284 const hub = await getHub();
69d1a7285 const token = await getToken();
69d1a7286 const res = await fetch(`${hub}/api/repos`, {
69d1a7287 headers: { Authorization: `Bearer ${token}` },
69d1a7288 });
69d1a7289 if (!res.ok) {
59e666790 log.error("Could not fetch repos to infer owner.");
69d1a7291 process.exit(1);
69d1a7292 }
69d1a7293 const { repos } = (await res.json()) as { repos: Array<{ owner_name: string; name: string }> };
69d1a7294 const match = repos.find((r) => r.name === repoName);
69d1a7295 if (!match) {
59e666796 log.error(`Repo '${repoName}' not found. Use --repo <owner/repo>.`);
69d1a7297 process.exit(1);
69d1a7298 }
69d1a7299 return `${match.owner_name}/${match.name}`;
69d1a72100}