@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.
- package/README.md +136 -0
- package/cli4ai.json +48 -0
- package/package.json +38 -0
- 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();
|