cli/src/commands/clone.tsblame
View source
59e66671import { spinner, log } from "@clack/prompts";
8d8e8152import { execSync } from "node:child_process";
bf25d293import { writeFileSync, existsSync, mkdirSync } from "node:fs";
8d8e8154import { join, resolve } from "node:path";
8d8e8155import { hubRequest } from "../api.js";
8d8e8156import { getHub } from "../config.js";
0d9d7237import { buildSlConfig } from "../sl-config.js";
8d8e8158
8d8e8159interface Repo {
8d8e81510 id: number;
8d8e81511 owner_name: string;
8d8e81512 name: string;
8d8e81513 default_branch: string;
8d8e81514}
8d8e81515
8d8e81516export async function clone(args: string[]) {
8d8e81517 const slug = args.find((a) => !a.startsWith("--"));
8d8e81518 if (!slug) {
59e666719 log.error("Usage: grove clone <owner/repo> [destination]");
8d8e81520 process.exit(1);
8d8e81521 }
8d8e81522
8d8e81523 let owner: string;
8d8e81524 let repoName: string;
8d8e81525
8d8e81526 if (slug.includes("/")) {
8d8e81527 [owner, repoName] = slug.split("/", 2);
8d8e81528 } else {
8d8e81529 // Bare repo name — look up owner from API
59e666730 const s = spinner();
59e666731 s.start("Looking up repository");
8d8e81532 const { repos } = await hubRequest<{ repos: Repo[] }>("/api/repos");
8d8e81533 const match = repos.find((r) => r.name === slug);
8d8e81534 if (!match) {
59e666735 s.stop("Not found");
59e666736 log.error(`Repository '${slug}' not found.`);
8d8e81537 process.exit(1);
8d8e81538 }
8d8e81539 owner = match.owner_name;
8d8e81540 repoName = match.name;
59e666741 s.stop(`Found ${owner}/${repoName}`);
8d8e81542 }
8d8e81543
8d8e81544 // Verify repo exists
59e666745 const s = spinner();
59e666746 s.start(`Fetching ${owner}/${repoName}`);
8d8e81547 const { repo } = await hubRequest<{ repo: Repo }>(
8d8e81548 `/api/repos/${owner}/${repoName}`
8d8e81549 );
59e666750 s.stop(`${owner}/${repoName}`);
8d8e81551
8d8e81552 const dest = args.find((a, i) => i > args.indexOf(slug) && !a.startsWith("--")) ?? repoName;
8d8e81553 const destPath = resolve(dest);
8d8e81554
bf25d2955 // Use sl init + pull (not sl clone, which uses getbundle protocol that doesn't work with Mononoke)
bf25d2956 const hub = await getHub();
bf25d2957
bf25d2958 const s2 = spinner();
bf25d2959 s2.start(`Cloning into ${dest}`);
bf25d2960 mkdirSync(destPath, { recursive: true });
bf25d2961 execSync(`sl init --config init.prefer-git=false --config format.use-remotefilelog=true "${destPath}"`, { stdio: "pipe" });
bf25d2962
bf25d2963 // Write config with remote and auth settings before pulling
bf25d2964 writeFileSync(join(destPath, ".sl", "config"), buildSlConfig({ name: repoName, owner_name: owner, default_branch: repo.default_branch }, hub));
bf25d2965
bf25d2966 // Pull and checkout
8d8e81567 try {
bf25d2968 execSync(`sl pull`, { cwd: destPath, stdio: "pipe" });
bf25d2969 execSync(`sl goto ${repo.default_branch}`, { cwd: destPath, stdio: "pipe" });
bf25d2970 s2.stop(`Cloned ${owner}/${repoName}`);
8d8e81571 } catch {
bf25d2972 s2.stop(`Cloned ${owner}/${repoName} (empty repository)`);
8d8e81573 }
8d8e81574
59e666775 log.success(`Cloned ${owner}/${repoName} into ${dest}`);
8d8e81576}