8.6 KB257 lines
Blame
1#!/usr/bin/env node
2
3import { log } from "@clack/prompts";
4import { authLogin } from "./commands/auth-login.js";
5import { authStatus } from "./commands/auth-status.js";
6import { whoami } from "./commands/whoami.js";
7import { clone } from "./commands/clone.js";
8import { init } from "./commands/init.js";
9import { status } from "./commands/status.js";
10import { repoList } from "./commands/repo-list.js";
11import { repoCreate } from "./commands/repo-create.js";
12import { instanceCreate } from "./commands/instance-create.js";
13import { ciRuns } from "./commands/ci-runs.js";
14import { ciStatus } from "./commands/ci-status.js";
15import { ciLogs } from "./commands/ci-logs.js";
16import { ciTrigger } from "./commands/ci-trigger.js";
17import { ciCancel } from "./commands/ci-cancel.js";
18
19const USAGE = `Usage: grove <command>
20
21Commands:
22 init Create a Grove repo and initialize Sapling in current directory
23 clone Clone a Grove repository
24 status Show current repository info
25 auth login Authenticate with Grove (opens browser)
26 auth status Show current authentication status
27 whoami Show current user
28 repo list List repositories
29 repo create Create a repository
30 instance create Register a Grove instance
31 ci runs List pipeline runs
32 ci status Show pipeline run details
33 ci logs Show step logs
34 ci trigger Manually trigger pipelines
35 ci cancel Cancel a running pipeline
36
37Options:
38 --help Show this help message
39 --version Show version`;
40
41const COMMAND_HELP: Record<string, string> = {
42 init: `Usage: grove init [directory] [options]
43
44Create a Grove repository and initialize Sapling in the current directory.
45If the directory contains a .git repo, imports full git history.
46
47Options:
48 --owner <org> Set the repository owner (org name)
49 --description <desc> Set the repository description
50 --private Create a private repository
51 --branch <branch> Set the default branch name`,
52
53 clone: `Usage: grove clone <owner/repo|repo> [destination]
54
55Clone a Grove repository into a new directory.
56If only a repo name is given (no owner/), looks up the owner via the API.`,
57
58 status: `Usage: grove status
59
60Show info about the current Grove repository (name, owner, branch, URL).`,
61
62 whoami: `Usage: grove whoami
63
64Show the currently authenticated user.`,
65
66 "auth login": `Usage: grove auth login [--hub <url>]
67
68Authenticate with Grove by opening the browser.
69
70Options:
71 --hub <url> Override the Grove hub URL`,
72
73 "auth status": `Usage: grove auth status
74
75Show current authentication status, token expiry, and hub URL.`,
76
77 "repo list": `Usage: grove repo list
78
79List all repositories you have access to.`,
80
81 "repo create": `Usage: grove repo create <name> [options]
82
83Create a new repository on Grove without initializing a local directory.
84Use 'grove init' to create and initialize in one step.
85
86Options:
87 --owner <org> Set the repository owner (org name)
88 --description <desc> Set the repository description
89 --branch <branch> Set the default branch name
90 --private Create a private repository
91 --no-seed Skip creating an initial commit`,
92
93 "instance create": `Usage: grove instance create --region <region> --size <size> [options]
94
95Register a new Grove instance.
96
97Options:
98 --name <name> Instance name (default: "grove")
99 --region <region> Region (required)
100 --size <size> Instance size (required)
101 --ip <ip> IP address
102 --domain <domain> Domain name`,
103
104 "ci runs": `Usage: grove ci runs [options]
105
106List pipeline runs for the current repository.
107
108Options:
109 --repo <owner/repo> Specify repository (default: inferred from .sl/config)
110 --status <status> Filter by status (passed, failed, running, pending)
111 --limit <n> Number of runs to show (default: 10)`,
112
113 "ci status": `Usage: grove ci status [run-id] [options]
114
115Show details and steps for a pipeline run.
116If no run ID is given, shows the latest run.
117
118Options:
119 --repo <owner/repo> Specify repository (default: inferred from .sl/config)`,
120
121 "ci logs": `Usage: grove ci logs <run-id> <step-index> [options]
122
123Show logs for a specific step in a pipeline run.
124
125Options:
126 --repo <owner/repo> Specify repository (default: inferred from .sl/config)`,
127
128 "ci trigger": `Usage: grove ci trigger [options]
129
130Manually trigger pipelines for the current repository.
131
132Options:
133 --repo <owner/repo> Specify repository (default: inferred from .sl/config)
134 --ref <branch> Branch to trigger on`,
135
136 "ci cancel": `Usage: grove ci cancel <run-id> [options]
137
138Cancel a running pipeline.
139
140Options:
141 --repo <owner/repo> Specify repository (default: inferred from .sl/config)`,
142};
143
144function showHelp(command: string): boolean {
145 const help = COMMAND_HELP[command];
146 if (help) {
147 console.log(help);
148 return true;
149 }
150 return false;
151}
152
153const args = process.argv.slice(2);
154const cmd = args[0];
155const sub = args[1];
156const rest = args.slice(2);
157
158function wantsHelp(a: string[]): boolean {
159 return a.includes("--help") || a.includes("-h");
160}
161
162async function main() {
163 if (!cmd || cmd === "--help" || cmd === "-h") {
164 console.log(USAGE);
165 process.exit(0);
166 }
167
168 if (cmd === "--version" || cmd === "-v") {
169 const { readFile } = await import("node:fs/promises");
170 const { fileURLToPath } = await import("node:url");
171 const { dirname, join } = await import("node:path");
172 const dir = dirname(fileURLToPath(import.meta.url));
173 const pkg = JSON.parse(await readFile(join(dir, "..", "package.json"), "utf-8"));
174 console.log(`grove ${pkg.version}`);
175 process.exit(0);
176 }
177
178 if (cmd === "auth") {
179 if (wantsHelp(args)) {
180 const key = sub && !sub.startsWith("-") ? `auth ${sub}` : undefined;
181 if (key && showHelp(key)) process.exit(0);
182 console.log(`Usage: grove auth <login|status>\n\nSubcommands:\n login Authenticate with Grove (opens browser)\n status Show current authentication status`);
183 process.exit(0);
184 }
185 if (sub === "login") return authLogin(rest);
186 if (sub === "status") return authStatus();
187 log.error(`Unknown command: auth ${sub || ""}\nRun: grove auth --help`);
188 process.exit(1);
189 }
190
191 if (cmd === "init") {
192 if (wantsHelp(args)) { showHelp("init"); process.exit(0); }
193 return init(args.slice(1));
194 }
195 if (cmd === "clone") {
196 if (wantsHelp(args)) { showHelp("clone"); process.exit(0); }
197 return clone(args.slice(1));
198 }
199 if (cmd === "status") {
200 if (wantsHelp(args)) { showHelp("status"); process.exit(0); }
201 return status();
202 }
203 if (cmd === "whoami") {
204 if (wantsHelp(args)) { showHelp("whoami"); process.exit(0); }
205 return whoami();
206 }
207
208 if (cmd === "repo") {
209 if (wantsHelp(args)) {
210 const key = sub && !sub.startsWith("-") ? `repo ${sub}` : undefined;
211 if (key && showHelp(key)) process.exit(0);
212 console.log(`Usage: grove repo <list|create>\n\nSubcommands:\n list List repositories\n create Create a repository`);
213 process.exit(0);
214 }
215 if (sub === "list" || sub === "ls") return repoList();
216 if (sub === "create") return repoCreate(rest);
217 log.error(`Unknown command: repo ${sub || ""}\nRun: grove repo --help`);
218 process.exit(1);
219 }
220
221 if (cmd === "instance") {
222 if (wantsHelp(args)) {
223 const key = sub && !sub.startsWith("-") ? `instance ${sub}` : undefined;
224 if (key && showHelp(key)) process.exit(0);
225 console.log(`Usage: grove instance <create>\n\nSubcommands:\n create Register a Grove instance`);
226 process.exit(0);
227 }
228 if (sub === "create") return instanceCreate(rest);
229 log.error(`Unknown command: instance ${sub || ""}\nRun: grove instance --help`);
230 process.exit(1);
231 }
232
233 if (cmd === "ci") {
234 if (wantsHelp(args)) {
235 const key = sub && !sub.startsWith("-") ? `ci ${sub}` : undefined;
236 if (key && showHelp(key)) process.exit(0);
237 console.log(`Usage: grove ci <runs|status|logs|trigger|cancel>\n\nSubcommands:\n runs List pipeline runs\n status Show pipeline run details\n logs Show step logs\n trigger Manually trigger pipelines\n cancel Cancel a running pipeline`);
238 process.exit(0);
239 }
240 if (sub === "runs" || sub === "ls") return ciRuns(rest);
241 if (sub === "status" || sub === "show") return ciStatus(rest);
242 if (sub === "logs" || sub === "log") return ciLogs(rest);
243 if (sub === "trigger" || sub === "run") return ciTrigger(rest);
244 if (sub === "cancel") return ciCancel(rest);
245 log.error(`Unknown command: ci ${sub || ""}\nRun: grove ci --help`);
246 process.exit(1);
247 }
248
249 log.error(`Unknown command: ${cmd}\nRun: grove --help`);
250 process.exit(1);
251}
252
253main().catch((err) => {
254 log.error(err.message || String(err));
255 process.exit(1);
256});
257