| 1 | import { spinner, log } from "@clack/prompts"; |
| 2 | import { execSync } from "node:child_process"; |
| 3 | import { writeFileSync, existsSync, mkdirSync } from "node:fs"; |
| 4 | import { join, resolve } from "node:path"; |
| 5 | import { hubRequest } from "../api.js"; |
| 6 | import { getHub } from "../config.js"; |
| 7 | import { buildSlConfig } from "../sl-config.js"; |
| 8 | |
| 9 | interface Repo { |
| 10 | id: number; |
| 11 | owner_name: string; |
| 12 | name: string; |
| 13 | default_branch: string; |
| 14 | } |
| 15 | |
| 16 | export async function clone(args: string[]) { |
| 17 | const slug = args.find((a) => !a.startsWith("--")); |
| 18 | if (!slug) { |
| 19 | log.error("Usage: grove clone <owner/repo> [destination]"); |
| 20 | process.exit(1); |
| 21 | } |
| 22 | |
| 23 | let owner: string; |
| 24 | let repoName: string; |
| 25 | |
| 26 | if (slug.includes("/")) { |
| 27 | [owner, repoName] = slug.split("/", 2); |
| 28 | } else { |
| 29 | // Bare repo name — look up owner from API |
| 30 | const s = spinner(); |
| 31 | s.start("Looking up repository"); |
| 32 | const { repos } = await hubRequest<{ repos: Repo[] }>("/api/repos"); |
| 33 | const match = repos.find((r) => r.name === slug); |
| 34 | if (!match) { |
| 35 | s.stop("Not found"); |
| 36 | log.error(`Repository '${slug}' not found.`); |
| 37 | process.exit(1); |
| 38 | } |
| 39 | owner = match.owner_name; |
| 40 | repoName = match.name; |
| 41 | s.stop(`Found ${owner}/${repoName}`); |
| 42 | } |
| 43 | |
| 44 | // Verify repo exists |
| 45 | const s = spinner(); |
| 46 | s.start(`Fetching ${owner}/${repoName}`); |
| 47 | const { repo } = await hubRequest<{ repo: Repo }>( |
| 48 | `/api/repos/${owner}/${repoName}` |
| 49 | ); |
| 50 | s.stop(`${owner}/${repoName}`); |
| 51 | |
| 52 | const dest = args.find((a, i) => i > args.indexOf(slug) && !a.startsWith("--")) ?? repoName; |
| 53 | const destPath = resolve(dest); |
| 54 | |
| 55 | // Use sl init + pull (not sl clone, which uses getbundle protocol that doesn't work with Mononoke) |
| 56 | const hub = await getHub(); |
| 57 | |
| 58 | const s2 = spinner(); |
| 59 | s2.start(`Cloning into ${dest}`); |
| 60 | mkdirSync(destPath, { recursive: true }); |
| 61 | execSync(`sl init --config init.prefer-git=false --config format.use-remotefilelog=true "${destPath}"`, { stdio: "pipe" }); |
| 62 | |
| 63 | // Write config with remote and auth settings before pulling |
| 64 | writeFileSync(join(destPath, ".sl", "config"), buildSlConfig({ name: repoName, owner_name: owner, default_branch: repo.default_branch }, hub)); |
| 65 | |
| 66 | // Pull and checkout |
| 67 | try { |
| 68 | execSync(`sl pull`, { cwd: destPath, stdio: "pipe" }); |
| 69 | execSync(`sl goto ${repo.default_branch}`, { cwd: destPath, stdio: "pipe" }); |
| 70 | s2.stop(`Cloned ${owner}/${repoName}`); |
| 71 | } catch { |
| 72 | s2.stop(`Cloned ${owner}/${repoName} (empty repository)`); |
| 73 | } |
| 74 | |
| 75 | log.success(`Cloned ${owner}/${repoName} into ${dest}`); |
| 76 | } |
| 77 | |