-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli-program.ts
More file actions
234 lines (203 loc) · 8.19 KB
/
cli-program.ts
File metadata and controls
234 lines (203 loc) · 8.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import { Command } from "commander";
import { setMode, type Mode } from "./mode.js";
import { init } from "./commands/init/index.js";
import { login } from "./commands/auth/login.js";
import { logout } from "./commands/auth/logout.js";
import { whoami } from "./commands/whoami/index.js";
import { pull } from "./commands/env/pull.js";
import { deploy } from "./commands/deploy/index.js";
import { configPull } from "./commands/config/pull.js";
import { configPatch, configPut } from "./commands/config/push.js";
import { configSchema } from "./commands/config/schema.js";
import { api } from "./commands/api/index.js";
import { link } from "./commands/link/index.js";
import { unlink } from "./commands/unlink/index.js";
import { doctor } from "./commands/doctor/index.js";
import { schema } from "./commands/schema/index.js";
import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.js";
import { red } from "./lib/color.js";
export function createProgram(): Command {
const program = new Command();
program
.name("clerk")
.description("Clerk CLI")
.version(require("../package.json").version)
.option(
"--mode <mode>",
"Force interaction mode (human or agent). Defaults to auto-detect based on TTY.",
)
.option("--verbose", "Show detailed error output");
program.hook("preAction", () => {
const opts = program.opts();
if (opts.mode) {
if (opts.mode !== "human" && opts.mode !== "agent") {
throwUsageError(`Invalid mode "${opts.mode}". Must be "human" or "agent".`);
}
setMode(opts.mode as Mode);
}
});
program.command("init").description("Initialize Clerk in your project").action(init);
const auth = program.command("auth").description("Manage authentication");
auth
.command("login")
.aliases(["signup", "signin", "sign-in"])
.description("Log in to your Clerk account")
.action(async () => {
await login();
});
auth
.command("logout")
.aliases(["signout", "sign-out"])
.description("Log out of your Clerk account")
.action(logout);
program
.command("link")
.description("Link this project to a Clerk application")
.option("--app <id>", "Application ID to link (skips interactive picker)")
.action(link);
program
.command("unlink")
.description("Unlink this project from its Clerk application")
.option("--yes", "Skip confirmation prompt")
.action(unlink);
program.command("whoami").description("Show the current logged-in user").action(whoami);
const env = program.command("env").description("Manage environment variables");
env
.command("pull")
.description("Pull environment variables from Clerk to .env.local")
.option("--instance <id>", "Instance to target (dev, prod, or a full instance ID)")
.option("--file <path>", "Target env file (default: auto-detect)")
.action(pull);
const config = program.command("config").description("Manage instance configuration");
config
.command("pull")
.description("Pull instance configuration from Clerk")
.option("--instance <id>", "Instance to target (dev, prod, or a full instance ID)")
.option("--output <file>", "Write config to a file instead of stdout")
.action(configPull);
config
.command("schema")
.description("Pull instance config schema from Clerk")
.option("--instance <id>", "Instance to target (dev, prod, or a full instance ID)")
.option("--output <file>", "Write schema to a file instead of stdout")
.option("--keys <keys...>", "Config keys to retrieve schema for")
.action(configSchema);
config
.command("patch")
.description("Partially update instance configuration (PATCH)")
.option("--instance <id>", "Instance to target (dev, prod, or a full instance ID)")
.option("--file <path>", "Read config JSON from a file")
.option("--json <string>", "Pass config JSON inline")
.option("--dry-run", "Show what would be sent without making the API call")
.option("--yes", "Skip confirmation prompts")
.action(configPatch);
config
.command("put")
.description("Replace entire instance configuration (PUT)")
.option("--instance <id>", "Instance to target (dev, prod, or a full instance ID)")
.option("--file <path>", "Read config JSON from a file")
.option("--json <string>", "Pass config JSON inline")
.option("--dry-run", "Show what would be sent without making the API call")
.option("--yes", "Skip confirmation prompts")
.action(configPut);
program
.command("api")
.description("Make authenticated requests to the Clerk API")
.argument(
"[endpoint]",
"API endpoint path, 'ls' to list endpoints, or omit for interactive mode",
)
.argument("[filter]", "Filter keyword (used with 'ls')")
.option("-X, --method <method>", "HTTP method (default: GET, or POST if body provided)")
.option("-d, --data <json>", "JSON request body")
.option("--file <path>", "Read request body from a file")
.option("--include", "Show response headers")
.option("--secret-key <key>", "Override the secret key")
.option("--instance <id>", "Instance to target (dev, prod, or instance ID)")
.option("--platform", "Use Platform API instead of Backend API")
.option("--dry-run", "Show the request without executing it")
.option("--yes", "Skip confirmation for mutating requests")
.action(api);
program
.command("schema")
.description("Fetch the OpenAPI spec for a Clerk API")
.argument("[api]", "API name: backend, frontend, platform, or webhooks")
.argument("[path]", "Endpoint path (e.g. /users) or schema type (e.g. User)")
.option("--spec-version <version>", "Spec version (default: latest)")
.option("--format <format>", "Output format: yaml (default) or json")
.option("--output <file>", "Write spec to a file instead of stdout")
.option("--resolve-refs", "Inline $ref references for self-contained output")
.action(schema);
program
.command("doctor")
.description("Check your project's Clerk integration health")
.option("--verbose", "Show detailed output for each check")
.option("--json", "Output results as JSON")
.option("--spotlight", "Only show warnings and failures")
.option("--fix", "Attempt to auto-fix issues")
.action(doctor);
program
.command("deploy", { hidden: true })
.description("Deploy your Clerk application")
.option("--debug", "Show debug output")
.action(deploy);
return program;
}
function formatApiBody(body: string, verbose: boolean): string {
if (verbose) {
try {
return "\n" + JSON.stringify(JSON.parse(body), null, 2);
} catch {
return "\n" + body;
}
}
try {
const parsed = JSON.parse(body);
if (parsed.errors?.[0]?.message) return parsed.errors[0].message;
if (parsed.error) return parsed.error;
if (parsed.message) return parsed.message;
} catch {
// not JSON
}
if (body.length > 200) return body.slice(0, 200) + "...";
return body;
}
/**
* Parse and run a program, handling all typed errors with user-facing messages.
* Used by `cli.ts` for real execution and by integration tests.
*/
export async function runProgram(
program: Command,
args?: string[],
options?: { from?: "user" | "node" },
): Promise<void> {
try {
await program.parseAsync(args, options);
} catch (error) {
const verbose = program.opts().verbose ?? false;
if (error instanceof UserAbortError) {
process.exit(EXIT_CODE.SUCCESS);
}
if (error instanceof CliError) {
if (error.message) {
console.error(red(`error: ${error.message}`));
}
if (error.docsUrl) {
console.error(`\nFor more information, see: ${error.docsUrl}`);
}
process.exit(error.exitCode);
}
if (error instanceof ApiError) {
const detail = formatApiBody(error.body, verbose);
const prefix = error.context ?? "Request failed";
console.error(red(`error: ${prefix} (${error.status}): ${detail}`));
process.exit(EXIT_CODE.GENERAL);
}
if (error instanceof Error) {
console.error(red(`error: ${error.message}`));
process.exit(EXIT_CODE.GENERAL);
}
console.error(red("error: An unexpected error occurred"));
process.exit(EXIT_CODE.GENERAL);
}
}