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