@cli4ai/openapi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +136 -0
  2. package/cli4ai.json +48 -0
  3. package/package.json +38 -0
  4. package/run.ts +504 -0
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @cli4ai/openapi
2
+
3
+ Turn any OpenAPI spec into a dynamic CLI tool.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ cli4ai add -g openapi
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Load and inspect a spec
14
+
15
+ ```bash
16
+ # From URL
17
+ cli4ai run openapi load https://petstore3.swagger.io/api/v3/openapi.json
18
+
19
+ # From file
20
+ cli4ai run openapi load ./api-spec.json
21
+ ```
22
+
23
+ ### List all operations
24
+
25
+ ```bash
26
+ # List all operations
27
+ cli4ai run openapi list https://petstore3.swagger.io/api/v3/openapi.json
28
+
29
+ # Filter by tag
30
+ cli4ai run openapi list ./spec.json --tag pets
31
+
32
+ # Filter by method
33
+ cli4ai run openapi list ./spec.json --method POST
34
+ ```
35
+
36
+ ### Call an endpoint
37
+
38
+ ```bash
39
+ # By operationId
40
+ cli4ai run openapi call ./spec.json getPetById id=123
41
+
42
+ # By method and path
43
+ cli4ai run openapi call ./spec.json "GET /pet/{petId}" petId=123
44
+
45
+ # With query parameters
46
+ cli4ai run openapi call ./spec.json findPetsByStatus status=available
47
+
48
+ # With request body
49
+ cli4ai run openapi call ./spec.json addPet body='{"name":"Fluffy","status":"available"}'
50
+
51
+ # Dry run (show request without executing)
52
+ cli4ai run openapi call ./spec.json getPetById id=123 --dry-run
53
+ ```
54
+
55
+ ### View schemas
56
+
57
+ ```bash
58
+ # List all schemas
59
+ cli4ai run openapi schema ./spec.json
60
+
61
+ # View specific schema
62
+ cli4ai run openapi schema ./spec.json Pet
63
+ ```
64
+
65
+ ## Authentication
66
+
67
+ Set environment variables for authentication:
68
+
69
+ ```bash
70
+ # Bearer token
71
+ export OPENAPI_BEARER_TOKEN="your-token"
72
+
73
+ # API key (uses X-API-Key header or spec-defined header)
74
+ export OPENAPI_API_KEY="your-api-key"
75
+
76
+ # Override base URL
77
+ export OPENAPI_BASE_URL="https://api.example.com/v2"
78
+ ```
79
+
80
+ ## Examples
81
+
82
+ ### Petstore API
83
+
84
+ ```bash
85
+ # Load the spec
86
+ cli4ai run openapi load https://petstore3.swagger.io/api/v3/openapi.json
87
+
88
+ # List pets
89
+ cli4ai run openapi call https://petstore3.swagger.io/api/v3/openapi.json findPetsByStatus status=available
90
+
91
+ # Get a specific pet
92
+ cli4ai run openapi call https://petstore3.swagger.io/api/v3/openapi.json getPetById petId=1
93
+ ```
94
+
95
+ ### GitHub API
96
+
97
+ ```bash
98
+ export OPENAPI_BEARER_TOKEN="ghp_your_token"
99
+
100
+ # Get user info
101
+ cli4ai run openapi call https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json "GET /user"
102
+ ```
103
+
104
+ ## Parameter Passing
105
+
106
+ Parameters can be passed in several ways:
107
+
108
+ ```bash
109
+ # key=value format
110
+ cli4ai run openapi call ./spec.json operation param1=value1 param2=value2
111
+
112
+ # --key=value format
113
+ cli4ai run openapi call ./spec.json operation --param1=value1
114
+
115
+ # Request body as JSON
116
+ cli4ai run openapi call ./spec.json operation body='{"key":"value"}'
117
+ ```
118
+
119
+ ## Output
120
+
121
+ All output is JSON for easy parsing:
122
+
123
+ ```json
124
+ {
125
+ "status": 200,
126
+ "statusText": "OK",
127
+ "headers": { ... },
128
+ "data": { ... }
129
+ }
130
+ ```
131
+
132
+ ## Limitations
133
+
134
+ - Only JSON specs are supported (no YAML)
135
+ - OAuth2 flows require manual token acquisition
136
+ - File uploads not yet supported
package/cli4ai.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "openapi",
3
+ "version": "1.0.0",
4
+ "description": "Turn any OpenAPI spec into a dynamic CLI tool",
5
+ "entry": "run.ts",
6
+ "runtime": "node",
7
+ "commands": {
8
+ "load": {
9
+ "description": "Load an OpenAPI spec and list available endpoints",
10
+ "args": [
11
+ { "name": "spec", "required": true, "description": "URL or file path to OpenAPI spec" }
12
+ ]
13
+ },
14
+ "call": {
15
+ "description": "Call an API endpoint",
16
+ "args": [
17
+ { "name": "spec", "required": true, "description": "URL or file path to OpenAPI spec" },
18
+ { "name": "operationId", "required": true, "description": "Operation ID or 'METHOD /path'" }
19
+ ]
20
+ },
21
+ "list": {
22
+ "description": "List all operations in an OpenAPI spec",
23
+ "args": [
24
+ { "name": "spec", "required": true, "description": "URL or file path to OpenAPI spec" }
25
+ ]
26
+ }
27
+ },
28
+ "env": {
29
+ "OPENAPI_BASE_URL": {
30
+ "required": false,
31
+ "description": "Override the base URL from the spec"
32
+ },
33
+ "OPENAPI_API_KEY": {
34
+ "required": false,
35
+ "description": "API key for authentication"
36
+ },
37
+ "OPENAPI_BEARER_TOKEN": {
38
+ "required": false,
39
+ "description": "Bearer token for authentication"
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "@cli4ai/lib": "^1.0.0"
44
+ },
45
+ "mcp": {
46
+ "enabled": true
47
+ }
48
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@cli4ai/openapi",
3
+ "version": "1.0.0",
4
+ "description": "Turn any OpenAPI spec into a dynamic CLI tool",
5
+ "author": "cliforai",
6
+ "license": "MIT",
7
+ "main": "run.ts",
8
+ "bin": {
9
+ "openapi": "./run.ts"
10
+ },
11
+ "type": "module",
12
+ "keywords": [
13
+ "cli4ai",
14
+ "cli",
15
+ "ai-tools"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/cliforai/packages",
20
+ "directory": "packages/openapi"
21
+ },
22
+ "homepage": "https://github.com/cliforai/packages/tree/main/packages/openapi",
23
+ "bugs": {
24
+ "url": "https://github.com/cliforai/packages/issues"
25
+ },
26
+ "dependencies": {
27
+ "@cli4ai/lib": "^1.0.0"
28
+ },
29
+ "files": [
30
+ "run.ts",
31
+ "cli4ai.json",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
package/run.ts ADDED
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env npx tsx
2
+ import { cli, output, log } from '@cli4ai/lib/cli.ts';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { resolve } from 'path';
5
+
6
+ const program = cli('openapi', '1.0.0', 'Turn any OpenAPI spec into a dynamic CLI tool');
7
+
8
+ // Types for OpenAPI 3.x
9
+ interface OpenAPISpec {
10
+ openapi?: string;
11
+ swagger?: string;
12
+ info: {
13
+ title: string;
14
+ version: string;
15
+ description?: string;
16
+ };
17
+ servers?: Array<{ url: string; description?: string }>;
18
+ paths: Record<string, PathItem>;
19
+ components?: {
20
+ schemas?: Record<string, any>;
21
+ securitySchemes?: Record<string, SecurityScheme>;
22
+ };
23
+ }
24
+
25
+ interface PathItem {
26
+ get?: Operation;
27
+ post?: Operation;
28
+ put?: Operation;
29
+ patch?: Operation;
30
+ delete?: Operation;
31
+ options?: Operation;
32
+ head?: Operation;
33
+ parameters?: Parameter[];
34
+ }
35
+
36
+ interface Operation {
37
+ operationId?: string;
38
+ summary?: string;
39
+ description?: string;
40
+ parameters?: Parameter[];
41
+ requestBody?: RequestBody;
42
+ responses?: Record<string, any>;
43
+ security?: Array<Record<string, string[]>>;
44
+ tags?: string[];
45
+ }
46
+
47
+ interface Parameter {
48
+ name: string;
49
+ in: 'path' | 'query' | 'header' | 'cookie';
50
+ required?: boolean;
51
+ description?: string;
52
+ schema?: { type?: string; default?: any; enum?: any[] };
53
+ }
54
+
55
+ interface RequestBody {
56
+ required?: boolean;
57
+ description?: string;
58
+ content?: Record<string, { schema?: any }>;
59
+ }
60
+
61
+ interface SecurityScheme {
62
+ type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';
63
+ name?: string;
64
+ in?: 'query' | 'header' | 'cookie';
65
+ scheme?: string;
66
+ bearerFormat?: string;
67
+ }
68
+
69
+ interface ParsedOperation {
70
+ method: string;
71
+ path: string;
72
+ operationId: string;
73
+ summary: string;
74
+ description?: string;
75
+ parameters: Parameter[];
76
+ requestBody?: RequestBody;
77
+ tags: string[];
78
+ }
79
+
80
+ // Store the source URL for resolving relative server URLs
81
+ let specSourceUrl: string | null = null;
82
+
83
+ // Load OpenAPI spec from URL or file
84
+ async function loadSpec(specPath: string): Promise<OpenAPISpec> {
85
+ let content: string;
86
+
87
+ if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
88
+ log(`Fetching spec from ${specPath}...`);
89
+ specSourceUrl = specPath;
90
+ const response = await fetch(specPath);
91
+ if (!response.ok) {
92
+ throw new Error(`Failed to fetch spec: ${response.status} ${response.statusText}`);
93
+ }
94
+ content = await response.text();
95
+ } else {
96
+ specSourceUrl = null;
97
+ const filePath = resolve(process.env.CLI4AI_CWD || process.cwd(), specPath);
98
+ if (!existsSync(filePath)) {
99
+ throw new Error(`File not found: ${filePath}`);
100
+ }
101
+ content = readFileSync(filePath, 'utf-8');
102
+ }
103
+
104
+ try {
105
+ return JSON.parse(content);
106
+ } catch {
107
+ // Try YAML if JSON fails
108
+ throw new Error('Invalid spec format. Only JSON is currently supported.');
109
+ }
110
+ }
111
+
112
+ // Parse all operations from the spec
113
+ function parseOperations(spec: OpenAPISpec): ParsedOperation[] {
114
+ const operations: ParsedOperation[] = [];
115
+ const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'] as const;
116
+
117
+ for (const [path, pathItem] of Object.entries(spec.paths || {})) {
118
+ const pathParams = pathItem.parameters || [];
119
+
120
+ for (const method of methods) {
121
+ const operation = pathItem[method];
122
+ if (!operation) continue;
123
+
124
+ const operationId = operation.operationId || `${method.toUpperCase()} ${path}`;
125
+
126
+ operations.push({
127
+ method: method.toUpperCase(),
128
+ path,
129
+ operationId,
130
+ summary: operation.summary || operation.description || '',
131
+ description: operation.description,
132
+ parameters: [...pathParams, ...(operation.parameters || [])],
133
+ requestBody: operation.requestBody,
134
+ tags: operation.tags || [],
135
+ });
136
+ }
137
+ }
138
+
139
+ return operations;
140
+ }
141
+
142
+ // Get base URL from spec or environment
143
+ function getBaseUrl(spec: OpenAPISpec): string {
144
+ if (process.env.OPENAPI_BASE_URL) {
145
+ return process.env.OPENAPI_BASE_URL.replace(/\/$/, '');
146
+ }
147
+
148
+ if (spec.servers && spec.servers.length > 0) {
149
+ let serverUrl = spec.servers[0].url.replace(/\/$/, '');
150
+
151
+ // Handle relative server URLs
152
+ if (serverUrl.startsWith('/') && specSourceUrl) {
153
+ try {
154
+ const sourceUrl = new URL(specSourceUrl);
155
+ serverUrl = `${sourceUrl.protocol}//${sourceUrl.host}${serverUrl}`;
156
+ } catch {
157
+ // If URL parsing fails, just use the relative path
158
+ }
159
+ }
160
+
161
+ return serverUrl;
162
+ }
163
+
164
+ // If no servers defined but we have a source URL, use its origin
165
+ if (specSourceUrl) {
166
+ try {
167
+ const sourceUrl = new URL(specSourceUrl);
168
+ return `${sourceUrl.protocol}//${sourceUrl.host}`;
169
+ } catch {
170
+ // Fall through to error
171
+ }
172
+ }
173
+
174
+ throw new Error('No base URL found. Set OPENAPI_BASE_URL or ensure spec has servers defined.');
175
+ }
176
+
177
+ // Build headers for authentication
178
+ function buildAuthHeaders(spec: OpenAPISpec): Record<string, string> {
179
+ const headers: Record<string, string> = {};
180
+
181
+ if (process.env.OPENAPI_BEARER_TOKEN) {
182
+ headers['Authorization'] = `Bearer ${process.env.OPENAPI_BEARER_TOKEN}`;
183
+ }
184
+
185
+ if (process.env.OPENAPI_API_KEY && spec.components?.securitySchemes) {
186
+ // Find API key security scheme
187
+ for (const [, scheme] of Object.entries(spec.components.securitySchemes)) {
188
+ if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name) {
189
+ headers[scheme.name] = process.env.OPENAPI_API_KEY;
190
+ break;
191
+ }
192
+ }
193
+ // Default to common header names if no scheme found
194
+ if (!headers['Authorization'] && !Object.keys(headers).some(k => k.toLowerCase().includes('key'))) {
195
+ headers['X-API-Key'] = process.env.OPENAPI_API_KEY;
196
+ }
197
+ }
198
+
199
+ return headers;
200
+ }
201
+
202
+ // Find operation by ID or method+path
203
+ function findOperation(operations: ParsedOperation[], identifier: string): ParsedOperation | undefined {
204
+ // Try exact operationId match
205
+ let op = operations.find(o => o.operationId === identifier);
206
+ if (op) return op;
207
+
208
+ // Try case-insensitive operationId match
209
+ op = operations.find(o => o.operationId.toLowerCase() === identifier.toLowerCase());
210
+ if (op) return op;
211
+
212
+ // Try "METHOD /path" format
213
+ const match = identifier.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(.+)$/i);
214
+ if (match) {
215
+ const [, method, path] = match;
216
+ op = operations.find(o =>
217
+ o.method.toUpperCase() === method.toUpperCase() &&
218
+ o.path === path
219
+ );
220
+ if (op) return op;
221
+ }
222
+
223
+ return undefined;
224
+ }
225
+
226
+ // Parse CLI arguments into params
227
+ function parseCallArgs(args: string[]): { params: Record<string, string>; body?: any } {
228
+ const params: Record<string, string> = {};
229
+ let body: any = undefined;
230
+
231
+ for (const arg of args) {
232
+ // Handle --param=value or --param value style
233
+ if (arg.startsWith('--')) {
234
+ const eqIndex = arg.indexOf('=');
235
+ if (eqIndex > 0) {
236
+ const key = arg.slice(2, eqIndex);
237
+ const value = arg.slice(eqIndex + 1);
238
+ if (key === 'body') {
239
+ try {
240
+ body = JSON.parse(value);
241
+ } catch {
242
+ body = value;
243
+ }
244
+ } else {
245
+ params[key] = value;
246
+ }
247
+ }
248
+ } else if (arg.startsWith('-')) {
249
+ // Skip short flags for now
250
+ } else if (arg.includes('=')) {
251
+ const [key, value] = arg.split('=', 2);
252
+ if (key === 'body') {
253
+ try {
254
+ body = JSON.parse(value);
255
+ } catch {
256
+ body = value;
257
+ }
258
+ } else {
259
+ params[key] = value;
260
+ }
261
+ }
262
+ }
263
+
264
+ return { params, body };
265
+ }
266
+
267
+ // Execute an API call
268
+ async function executeCall(
269
+ spec: OpenAPISpec,
270
+ operation: ParsedOperation,
271
+ params: Record<string, string>,
272
+ body?: any
273
+ ): Promise<any> {
274
+ const baseUrl = getBaseUrl(spec);
275
+
276
+ // Build URL with path parameters
277
+ let url = operation.path;
278
+ for (const param of operation.parameters.filter(p => p.in === 'path')) {
279
+ const value = params[param.name];
280
+ if (!value && param.required) {
281
+ throw new Error(`Missing required path parameter: ${param.name}`);
282
+ }
283
+ if (value) {
284
+ url = url.replace(`{${param.name}}`, encodeURIComponent(value));
285
+ }
286
+ }
287
+
288
+ // Build query string
289
+ const queryParams = new URLSearchParams();
290
+ for (const param of operation.parameters.filter(p => p.in === 'query')) {
291
+ const value = params[param.name];
292
+ if (!value && param.required) {
293
+ throw new Error(`Missing required query parameter: ${param.name}`);
294
+ }
295
+ if (value) {
296
+ queryParams.set(param.name, value);
297
+ }
298
+ }
299
+
300
+ const queryString = queryParams.toString();
301
+ const fullUrl = `${baseUrl}${url}${queryString ? '?' + queryString : ''}`;
302
+
303
+ // Build headers
304
+ const headers: Record<string, string> = {
305
+ 'Accept': 'application/json',
306
+ ...buildAuthHeaders(spec),
307
+ };
308
+
309
+ // Add header parameters
310
+ for (const param of operation.parameters.filter(p => p.in === 'header')) {
311
+ const value = params[param.name];
312
+ if (value) {
313
+ headers[param.name] = value;
314
+ }
315
+ }
316
+
317
+ // Build request options
318
+ const fetchOptions: RequestInit = {
319
+ method: operation.method,
320
+ headers,
321
+ };
322
+
323
+ // Add body for non-GET requests
324
+ if (body && !['GET', 'HEAD'].includes(operation.method)) {
325
+ headers['Content-Type'] = 'application/json';
326
+ fetchOptions.body = JSON.stringify(body);
327
+ }
328
+
329
+ log(`${operation.method} ${fullUrl}`);
330
+
331
+ const response = await fetch(fullUrl, fetchOptions);
332
+
333
+ const contentType = response.headers.get('content-type') || '';
334
+ let responseData: any;
335
+
336
+ if (contentType.includes('application/json')) {
337
+ responseData = await response.json();
338
+ } else {
339
+ responseData = await response.text();
340
+ }
341
+
342
+ return {
343
+ status: response.status,
344
+ statusText: response.statusText,
345
+ headers: Object.fromEntries(response.headers.entries()),
346
+ data: responseData,
347
+ };
348
+ }
349
+
350
+ // Commands
351
+ program
352
+ .command('load <spec>')
353
+ .description('Load an OpenAPI spec and show info')
354
+ .action(async (specPath: string) => {
355
+ try {
356
+ const spec = await loadSpec(specPath);
357
+ const operations = parseOperations(spec);
358
+
359
+ output({
360
+ title: spec.info.title,
361
+ version: spec.info.version,
362
+ description: spec.info.description,
363
+ openapi: spec.openapi || spec.swagger,
364
+ servers: spec.servers?.map(s => s.url) || [],
365
+ operationCount: operations.length,
366
+ tags: [...new Set(operations.flatMap(o => o.tags))],
367
+ });
368
+ } catch (error) {
369
+ output({ error: error instanceof Error ? error.message : String(error) });
370
+ process.exit(1);
371
+ }
372
+ });
373
+
374
+ program
375
+ .command('list <spec>')
376
+ .description('List all operations in an OpenAPI spec')
377
+ .option('-t, --tag <tag>', 'Filter by tag')
378
+ .option('-m, --method <method>', 'Filter by HTTP method')
379
+ .action(async (specPath: string, options: { tag?: string; method?: string }) => {
380
+ try {
381
+ const spec = await loadSpec(specPath);
382
+ let operations = parseOperations(spec);
383
+
384
+ if (options.tag) {
385
+ operations = operations.filter(o =>
386
+ o.tags.some(t => t.toLowerCase().includes(options.tag!.toLowerCase()))
387
+ );
388
+ }
389
+
390
+ if (options.method) {
391
+ operations = operations.filter(o =>
392
+ o.method.toLowerCase() === options.method!.toLowerCase()
393
+ );
394
+ }
395
+
396
+ output({
397
+ count: operations.length,
398
+ operations: operations.map(o => ({
399
+ operationId: o.operationId,
400
+ method: o.method,
401
+ path: o.path,
402
+ summary: o.summary,
403
+ tags: o.tags,
404
+ parameters: o.parameters.map(p => ({
405
+ name: p.name,
406
+ in: p.in,
407
+ required: p.required || false,
408
+ type: p.schema?.type,
409
+ })),
410
+ hasRequestBody: !!o.requestBody,
411
+ })),
412
+ });
413
+ } catch (error) {
414
+ output({ error: error instanceof Error ? error.message : String(error) });
415
+ process.exit(1);
416
+ }
417
+ });
418
+
419
+ program
420
+ .command('call <spec> <operationId> [args...]')
421
+ .description('Call an API endpoint. Pass parameters as key=value or --key=value')
422
+ .option('--dry-run', 'Show the request without executing')
423
+ .action(async (specPath: string, operationId: string, args: string[], options: { dryRun?: boolean }) => {
424
+ try {
425
+ const spec = await loadSpec(specPath);
426
+ const operations = parseOperations(spec);
427
+ const operation = findOperation(operations, operationId);
428
+
429
+ if (!operation) {
430
+ const suggestions = operations
431
+ .filter(o => o.operationId.toLowerCase().includes(operationId.toLowerCase()))
432
+ .slice(0, 5);
433
+
434
+ output({
435
+ error: `Operation not found: ${operationId}`,
436
+ suggestions: suggestions.map(o => o.operationId),
437
+ hint: 'Use "openapi list <spec>" to see all operations',
438
+ });
439
+ process.exit(1);
440
+ }
441
+
442
+ const { params, body } = parseCallArgs(args);
443
+
444
+ if (options.dryRun) {
445
+ const baseUrl = getBaseUrl(spec);
446
+ let url = operation.path;
447
+ for (const param of operation.parameters.filter(p => p.in === 'path')) {
448
+ const value = params[param.name] || `{${param.name}}`;
449
+ url = url.replace(`{${param.name}}`, value);
450
+ }
451
+
452
+ output({
453
+ dryRun: true,
454
+ method: operation.method,
455
+ url: `${baseUrl}${url}`,
456
+ operation: operation.operationId,
457
+ parameters: params,
458
+ body,
459
+ requiredParams: operation.parameters
460
+ .filter(p => p.required)
461
+ .map(p => ({ name: p.name, in: p.in })),
462
+ });
463
+ return;
464
+ }
465
+
466
+ const result = await executeCall(spec, operation, params, body);
467
+ output(result);
468
+ } catch (error) {
469
+ output({ error: error instanceof Error ? error.message : String(error) });
470
+ process.exit(1);
471
+ }
472
+ });
473
+
474
+ program
475
+ .command('schema <spec> [schemaName]')
476
+ .description('Show schema definitions from the spec')
477
+ .action(async (specPath: string, schemaName?: string) => {
478
+ try {
479
+ const spec = await loadSpec(specPath);
480
+ const schemas = spec.components?.schemas || {};
481
+
482
+ if (schemaName) {
483
+ const schema = schemas[schemaName];
484
+ if (!schema) {
485
+ output({
486
+ error: `Schema not found: ${schemaName}`,
487
+ available: Object.keys(schemas),
488
+ });
489
+ process.exit(1);
490
+ }
491
+ output({ name: schemaName, schema });
492
+ } else {
493
+ output({
494
+ count: Object.keys(schemas).length,
495
+ schemas: Object.keys(schemas),
496
+ });
497
+ }
498
+ } catch (error) {
499
+ output({ error: error instanceof Error ? error.message : String(error) });
500
+ process.exit(1);
501
+ }
502
+ });
503
+
504
+ program.parse();