@cli4ai/openapi 1.0.0 → 1.0.2

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.
package/cli4ai.json CHANGED
@@ -1,27 +1,43 @@
1
1
  {
2
2
  "name": "openapi",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Turn any OpenAPI spec into a dynamic CLI tool",
5
- "entry": "run.ts",
5
+ "entry": "dist/run.js",
6
6
  "runtime": "node",
7
7
  "commands": {
8
8
  "load": {
9
9
  "description": "Load an OpenAPI spec and list available endpoints",
10
10
  "args": [
11
- { "name": "spec", "required": true, "description": "URL or file path to OpenAPI spec" }
11
+ {
12
+ "name": "spec",
13
+ "required": true,
14
+ "description": "URL or file path to OpenAPI spec"
15
+ }
12
16
  ]
13
17
  },
14
18
  "call": {
15
19
  "description": "Call an API endpoint",
16
20
  "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'" }
21
+ {
22
+ "name": "spec",
23
+ "required": true,
24
+ "description": "URL or file path to OpenAPI spec"
25
+ },
26
+ {
27
+ "name": "operationId",
28
+ "required": true,
29
+ "description": "Operation ID or 'METHOD /path'"
30
+ }
19
31
  ]
20
32
  },
21
33
  "list": {
22
34
  "description": "List all operations in an OpenAPI spec",
23
35
  "args": [
24
- { "name": "spec", "required": true, "description": "URL or file path to OpenAPI spec" }
36
+ {
37
+ "name": "spec",
38
+ "required": true,
39
+ "description": "URL or file path to OpenAPI spec"
40
+ }
25
41
  ]
26
42
  }
27
43
  },
package/dist/run.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/run.js ADDED
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ import { cli, output, log } from '@cli4ai/lib';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ const program = cli('openapi', '1.0.0', 'Turn any OpenAPI spec into a dynamic CLI tool');
6
+ // Store the source URL for resolving relative server URLs
7
+ let specSourceUrl = null;
8
+ // Load OpenAPI spec from URL or file
9
+ async function loadSpec(specPath) {
10
+ let content;
11
+ if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
12
+ log(`Fetching spec from ${specPath}...`);
13
+ specSourceUrl = specPath;
14
+ const response = await fetch(specPath);
15
+ if (!response.ok) {
16
+ throw new Error(`Failed to fetch spec: ${response.status} ${response.statusText}`);
17
+ }
18
+ content = await response.text();
19
+ }
20
+ else {
21
+ specSourceUrl = null;
22
+ const filePath = resolve(process.env.CLI4AI_CWD || process.cwd(), specPath);
23
+ if (!existsSync(filePath)) {
24
+ throw new Error(`File not found: ${filePath}`);
25
+ }
26
+ content = readFileSync(filePath, 'utf-8');
27
+ }
28
+ try {
29
+ return JSON.parse(content);
30
+ }
31
+ catch {
32
+ // Try YAML if JSON fails
33
+ throw new Error('Invalid spec format. Only JSON is currently supported.');
34
+ }
35
+ }
36
+ // Parse all operations from the spec
37
+ function parseOperations(spec) {
38
+ const operations = [];
39
+ const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
40
+ for (const [path, pathItem] of Object.entries(spec.paths || {})) {
41
+ const pathParams = pathItem.parameters || [];
42
+ for (const method of methods) {
43
+ const operation = pathItem[method];
44
+ if (!operation)
45
+ continue;
46
+ const operationId = operation.operationId || `${method.toUpperCase()} ${path}`;
47
+ operations.push({
48
+ method: method.toUpperCase(),
49
+ path,
50
+ operationId,
51
+ summary: operation.summary || operation.description || '',
52
+ description: operation.description,
53
+ parameters: [...pathParams, ...(operation.parameters || [])],
54
+ requestBody: operation.requestBody,
55
+ tags: operation.tags || [],
56
+ });
57
+ }
58
+ }
59
+ return operations;
60
+ }
61
+ // Get base URL from spec or environment
62
+ function getBaseUrl(spec) {
63
+ if (process.env.OPENAPI_BASE_URL) {
64
+ return process.env.OPENAPI_BASE_URL.replace(/\/$/, '');
65
+ }
66
+ if (spec.servers && spec.servers.length > 0) {
67
+ let serverUrl = spec.servers[0].url.replace(/\/$/, '');
68
+ // Handle relative server URLs
69
+ if (serverUrl.startsWith('/') && specSourceUrl) {
70
+ try {
71
+ const sourceUrl = new URL(specSourceUrl);
72
+ serverUrl = `${sourceUrl.protocol}//${sourceUrl.host}${serverUrl}`;
73
+ }
74
+ catch {
75
+ // If URL parsing fails, just use the relative path
76
+ }
77
+ }
78
+ return serverUrl;
79
+ }
80
+ // If no servers defined but we have a source URL, use its origin
81
+ if (specSourceUrl) {
82
+ try {
83
+ const sourceUrl = new URL(specSourceUrl);
84
+ return `${sourceUrl.protocol}//${sourceUrl.host}`;
85
+ }
86
+ catch {
87
+ // Fall through to error
88
+ }
89
+ }
90
+ throw new Error('No base URL found. Set OPENAPI_BASE_URL or ensure spec has servers defined.');
91
+ }
92
+ // Build headers for authentication
93
+ function buildAuthHeaders(spec) {
94
+ const headers = {};
95
+ if (process.env.OPENAPI_BEARER_TOKEN) {
96
+ headers['Authorization'] = `Bearer ${process.env.OPENAPI_BEARER_TOKEN}`;
97
+ }
98
+ if (process.env.OPENAPI_API_KEY && spec.components?.securitySchemes) {
99
+ // Find API key security scheme
100
+ for (const [, scheme] of Object.entries(spec.components.securitySchemes)) {
101
+ if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name) {
102
+ headers[scheme.name] = process.env.OPENAPI_API_KEY;
103
+ break;
104
+ }
105
+ }
106
+ // Default to common header names if no scheme found
107
+ if (!headers['Authorization'] && !Object.keys(headers).some(k => k.toLowerCase().includes('key'))) {
108
+ headers['X-API-Key'] = process.env.OPENAPI_API_KEY;
109
+ }
110
+ }
111
+ return headers;
112
+ }
113
+ // Find operation by ID or method+path
114
+ function findOperation(operations, identifier) {
115
+ // Try exact operationId match
116
+ let op = operations.find(o => o.operationId === identifier);
117
+ if (op)
118
+ return op;
119
+ // Try case-insensitive operationId match
120
+ op = operations.find(o => o.operationId.toLowerCase() === identifier.toLowerCase());
121
+ if (op)
122
+ return op;
123
+ // Try "METHOD /path" format
124
+ const match = identifier.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(.+)$/i);
125
+ if (match) {
126
+ const [, method, path] = match;
127
+ op = operations.find(o => o.method.toUpperCase() === method.toUpperCase() &&
128
+ o.path === path);
129
+ if (op)
130
+ return op;
131
+ }
132
+ return undefined;
133
+ }
134
+ // Parse CLI arguments into params
135
+ function parseCallArgs(args) {
136
+ const params = {};
137
+ let body = undefined;
138
+ for (const arg of args) {
139
+ // Handle --param=value or --param value style
140
+ if (arg.startsWith('--')) {
141
+ const eqIndex = arg.indexOf('=');
142
+ if (eqIndex > 0) {
143
+ const key = arg.slice(2, eqIndex);
144
+ const value = arg.slice(eqIndex + 1);
145
+ if (key === 'body') {
146
+ try {
147
+ body = JSON.parse(value);
148
+ }
149
+ catch {
150
+ body = value;
151
+ }
152
+ }
153
+ else {
154
+ params[key] = value;
155
+ }
156
+ }
157
+ }
158
+ else if (arg.startsWith('-')) {
159
+ // Skip short flags for now
160
+ }
161
+ else if (arg.includes('=')) {
162
+ const [key, value] = arg.split('=', 2);
163
+ if (key === 'body') {
164
+ try {
165
+ body = JSON.parse(value);
166
+ }
167
+ catch {
168
+ body = value;
169
+ }
170
+ }
171
+ else {
172
+ params[key] = value;
173
+ }
174
+ }
175
+ }
176
+ return { params, body };
177
+ }
178
+ // Execute an API call
179
+ async function executeCall(spec, operation, params, body) {
180
+ const baseUrl = getBaseUrl(spec);
181
+ // Build URL with path parameters
182
+ let url = operation.path;
183
+ for (const param of operation.parameters.filter(p => p.in === 'path')) {
184
+ const value = params[param.name];
185
+ if (!value && param.required) {
186
+ throw new Error(`Missing required path parameter: ${param.name}`);
187
+ }
188
+ if (value) {
189
+ url = url.replace(`{${param.name}}`, encodeURIComponent(value));
190
+ }
191
+ }
192
+ // Build query string
193
+ const queryParams = new URLSearchParams();
194
+ for (const param of operation.parameters.filter(p => p.in === 'query')) {
195
+ const value = params[param.name];
196
+ if (!value && param.required) {
197
+ throw new Error(`Missing required query parameter: ${param.name}`);
198
+ }
199
+ if (value) {
200
+ queryParams.set(param.name, value);
201
+ }
202
+ }
203
+ const queryString = queryParams.toString();
204
+ const fullUrl = `${baseUrl}${url}${queryString ? '?' + queryString : ''}`;
205
+ // Build headers
206
+ const headers = {
207
+ 'Accept': 'application/json',
208
+ ...buildAuthHeaders(spec),
209
+ };
210
+ // Add header parameters
211
+ for (const param of operation.parameters.filter(p => p.in === 'header')) {
212
+ const value = params[param.name];
213
+ if (value) {
214
+ headers[param.name] = value;
215
+ }
216
+ }
217
+ // Build request options
218
+ const fetchOptions = {
219
+ method: operation.method,
220
+ headers,
221
+ };
222
+ // Add body for non-GET requests
223
+ if (body && !['GET', 'HEAD'].includes(operation.method)) {
224
+ headers['Content-Type'] = 'application/json';
225
+ fetchOptions.body = JSON.stringify(body);
226
+ }
227
+ log(`${operation.method} ${fullUrl}`);
228
+ const response = await fetch(fullUrl, fetchOptions);
229
+ const contentType = response.headers.get('content-type') || '';
230
+ let responseData;
231
+ if (contentType.includes('application/json')) {
232
+ responseData = await response.json();
233
+ }
234
+ else {
235
+ responseData = await response.text();
236
+ }
237
+ return {
238
+ status: response.status,
239
+ statusText: response.statusText,
240
+ headers: Object.fromEntries(response.headers.entries()),
241
+ data: responseData,
242
+ };
243
+ }
244
+ // Commands
245
+ program
246
+ .command('load <spec>')
247
+ .description('Load an OpenAPI spec and show info')
248
+ .action(async (specPath) => {
249
+ try {
250
+ const spec = await loadSpec(specPath);
251
+ const operations = parseOperations(spec);
252
+ output({
253
+ title: spec.info.title,
254
+ version: spec.info.version,
255
+ description: spec.info.description,
256
+ openapi: spec.openapi || spec.swagger,
257
+ servers: spec.servers?.map(s => s.url) || [],
258
+ operationCount: operations.length,
259
+ tags: [...new Set(operations.flatMap(o => o.tags))],
260
+ });
261
+ }
262
+ catch (error) {
263
+ output({ error: error instanceof Error ? error.message : String(error) });
264
+ process.exit(1);
265
+ }
266
+ });
267
+ program
268
+ .command('list <spec>')
269
+ .description('List all operations in an OpenAPI spec')
270
+ .option('-t, --tag <tag>', 'Filter by tag')
271
+ .option('-m, --method <method>', 'Filter by HTTP method')
272
+ .action(async (specPath, options) => {
273
+ try {
274
+ const spec = await loadSpec(specPath);
275
+ let operations = parseOperations(spec);
276
+ if (options.tag) {
277
+ operations = operations.filter(o => o.tags.some(t => t.toLowerCase().includes(options.tag.toLowerCase())));
278
+ }
279
+ if (options.method) {
280
+ operations = operations.filter(o => o.method.toLowerCase() === options.method.toLowerCase());
281
+ }
282
+ output({
283
+ count: operations.length,
284
+ operations: operations.map(o => ({
285
+ operationId: o.operationId,
286
+ method: o.method,
287
+ path: o.path,
288
+ summary: o.summary,
289
+ tags: o.tags,
290
+ parameters: o.parameters.map(p => ({
291
+ name: p.name,
292
+ in: p.in,
293
+ required: p.required || false,
294
+ type: p.schema?.type,
295
+ })),
296
+ hasRequestBody: !!o.requestBody,
297
+ })),
298
+ });
299
+ }
300
+ catch (error) {
301
+ output({ error: error instanceof Error ? error.message : String(error) });
302
+ process.exit(1);
303
+ }
304
+ });
305
+ program
306
+ .command('call <spec> <operationId> [args...]')
307
+ .description('Call an API endpoint. Pass parameters as key=value or --key=value')
308
+ .option('--dry-run', 'Show the request without executing')
309
+ .action(async (specPath, operationId, args, options) => {
310
+ try {
311
+ const spec = await loadSpec(specPath);
312
+ const operations = parseOperations(spec);
313
+ const operation = findOperation(operations, operationId);
314
+ if (!operation) {
315
+ const suggestions = operations
316
+ .filter(o => o.operationId.toLowerCase().includes(operationId.toLowerCase()))
317
+ .slice(0, 5);
318
+ output({
319
+ error: `Operation not found: ${operationId}`,
320
+ suggestions: suggestions.map(o => o.operationId),
321
+ hint: 'Use "openapi list <spec>" to see all operations',
322
+ });
323
+ process.exit(1);
324
+ }
325
+ const { params, body } = parseCallArgs(args);
326
+ if (options.dryRun) {
327
+ const baseUrl = getBaseUrl(spec);
328
+ let url = operation.path;
329
+ for (const param of operation.parameters.filter(p => p.in === 'path')) {
330
+ const value = params[param.name] || `{${param.name}}`;
331
+ url = url.replace(`{${param.name}}`, value);
332
+ }
333
+ output({
334
+ dryRun: true,
335
+ method: operation.method,
336
+ url: `${baseUrl}${url}`,
337
+ operation: operation.operationId,
338
+ parameters: params,
339
+ body,
340
+ requiredParams: operation.parameters
341
+ .filter(p => p.required)
342
+ .map(p => ({ name: p.name, in: p.in })),
343
+ });
344
+ return;
345
+ }
346
+ const result = await executeCall(spec, operation, params, body);
347
+ output(result);
348
+ }
349
+ catch (error) {
350
+ output({ error: error instanceof Error ? error.message : String(error) });
351
+ process.exit(1);
352
+ }
353
+ });
354
+ program
355
+ .command('schema <spec> [schemaName]')
356
+ .description('Show schema definitions from the spec')
357
+ .action(async (specPath, schemaName) => {
358
+ try {
359
+ const spec = await loadSpec(specPath);
360
+ const schemas = spec.components?.schemas || {};
361
+ if (schemaName) {
362
+ const schema = schemas[schemaName];
363
+ if (!schema) {
364
+ output({
365
+ error: `Schema not found: ${schemaName}`,
366
+ available: Object.keys(schemas),
367
+ });
368
+ process.exit(1);
369
+ }
370
+ output({ name: schemaName, schema });
371
+ }
372
+ else {
373
+ output({
374
+ count: Object.keys(schemas).length,
375
+ schemas: Object.keys(schemas),
376
+ });
377
+ }
378
+ }
379
+ catch (error) {
380
+ output({ error: error instanceof Error ? error.message : String(error) });
381
+ process.exit(1);
382
+ }
383
+ });
384
+ program.parse();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cli4ai/openapi",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Turn any OpenAPI spec into a dynamic CLI tool",
5
5
  "author": "cliforai",
6
- "license": "MIT",
7
- "main": "run.ts",
6
+ "license": "BUSL-1.1",
7
+ "main": "dist/run.js",
8
8
  "bin": {
9
- "openapi": "./run.ts"
9
+ "openapi": "./dist/run.js"
10
10
  },
11
11
  "type": "module",
12
12
  "keywords": [
@@ -27,12 +27,20 @@
27
27
  "@cli4ai/lib": "^1.0.0"
28
28
  },
29
29
  "files": [
30
- "run.ts",
30
+ "dist",
31
31
  "cli4ai.json",
32
32
  "README.md",
33
33
  "LICENSE"
34
34
  ],
35
35
  "publishConfig": {
36
36
  "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^5.0.0",
44
+ "@types/node": "^22.0.0"
37
45
  }
38
46
  }
package/run.ts DELETED
@@ -1,504 +0,0 @@
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();