2.8 KB101 lines
Blame
1import { log } from "@clack/prompts";
2import { readFile, writeFile, mkdir } from "node:fs/promises";
3import { existsSync, readFileSync } from "node:fs";
4import { homedir } from "node:os";
5import { join, resolve, dirname } from "node:path";
6
7export interface GroveConfig {
8 hub: string;
9 token?: string;
10}
11
12const CONFIG_DIR = join(homedir(), ".grove");
13const CONFIG_PATH = join(CONFIG_DIR, "config.json");
14
15const DEFAULT_CONFIG: GroveConfig = {
16 hub: "https://grove.host",
17};
18
19export async function loadConfig(): Promise<GroveConfig> {
20 try {
21 const raw = await readFile(CONFIG_PATH, "utf-8");
22 return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
23 } catch {
24 return { ...DEFAULT_CONFIG };
25 }
26}
27
28export async function saveConfig(config: GroveConfig): Promise<void> {
29 await mkdir(CONFIG_DIR, { recursive: true });
30 await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
31 mode: 0o600,
32 });
33}
34
35export async function getToken(): Promise<string> {
36 const config = await loadConfig();
37 if (!config.token) {
38 log.error("Not logged in. Run: grove auth login");
39 process.exit(1);
40 }
41 return config.token;
42}
43
44export async function getHub(): Promise<string> {
45 const config = await loadConfig();
46 return config.hub;
47}
48
49/**
50 * Infer the repo name from the current directory's .sl/config.
51 * Looks for [remotefilelog] reponame = <name>.
52 */
53function inferRepoName(): string | null {
54 let dir = process.cwd();
55 while (dir !== dirname(dir)) {
56 const configPath = join(dir, ".sl", "config");
57 if (existsSync(configPath)) {
58 const content = readFileSync(configPath, "utf-8");
59 const match = content.match(/reponame\s*=\s*(.+)/);
60 if (match) return match[1].trim();
61 }
62 dir = dirname(dir);
63 }
64 return null;
65}
66
67/**
68 * Get the repo slug (owner/repo) from --repo arg or infer from .sl/config.
69 * If inferred, queries the API to find the owner.
70 */
71export async function getRepoSlug(args: string[]): Promise<string> {
72 const repoIdx = args.indexOf("--repo");
73 if (repoIdx !== -1 && args[repoIdx + 1]) {
74 return args[repoIdx + 1];
75 }
76
77 const repoName = inferRepoName();
78 if (!repoName) {
79 log.error("Could not infer repo. Use --repo <owner/repo> or run from a Sapling repo.");
80 process.exit(1);
81 }
82
83 // Need to look up the owner via the API
84 const hub = await getHub();
85 const token = await getToken();
86 const res = await fetch(`${hub}/api/repos`, {
87 headers: { Authorization: `Bearer ${token}` },
88 });
89 if (!res.ok) {
90 log.error("Could not fetch repos to infer owner.");
91 process.exit(1);
92 }
93 const { repos } = (await res.json()) as { repos: Array<{ owner_name: string; name: string }> };
94 const match = repos.find((r) => r.name === repoName);
95 if (!match) {
96 log.error(`Repo '${repoName}' not found. Use --repo <owner/repo>.`);
97 process.exit(1);
98 }
99 return `${match.owner_name}/${match.name}`;
100}
101