@askills/openapi-explorer-cli 0.0.2 → 0.0.4
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/dist/index.d.mts +1 -0
- package/dist/index.mjs +589 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +9 -4
- package/CHANGELOG.md +0 -7
- package/src/cli/args.ts +0 -59
- package/src/cli/dispatch.ts +0 -80
- package/src/commands/endpoint.ts +0 -116
- package/src/commands/info.ts +0 -44
- package/src/commands/paths.ts +0 -43
- package/src/commands/schema.ts +0 -22
- package/src/commands/schemas.ts +0 -45
- package/src/commands/search.ts +0 -49
- package/src/commands/tags.ts +0 -11
- package/src/core/loader.ts +0 -38
- package/src/core/queries.ts +0 -165
- package/src/core/resolve.ts +0 -70
- package/src/formatters/endpoint.ts +0 -74
- package/src/formatters/schema.ts +0 -66
- package/src/main.ts +0 -12
- package/src/types.ts +0 -60
- package/test/openapi.json +0 -30965
- package/test/swagger.test.ts +0 -778
- package/tsconfig.json +0 -10
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
//#region src/core/loader.ts
|
|
5
|
+
async function loadSpec(source) {
|
|
6
|
+
const isUrl = /^https?:\/\//i.test(source);
|
|
7
|
+
let raw;
|
|
8
|
+
if (isUrl) {
|
|
9
|
+
const res = await fetch(source, {
|
|
10
|
+
headers: { Accept: "application/json, text/plain" },
|
|
11
|
+
signal: AbortSignal.timeout(3e4)
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${source}`);
|
|
14
|
+
raw = await res.text();
|
|
15
|
+
} else raw = await readFile(resolve(source), "utf-8");
|
|
16
|
+
const data = JSON.parse(raw);
|
|
17
|
+
if (!data.openapi && !data.swagger) throw new Error("Not a valid OpenAPI/Swagger spec.");
|
|
18
|
+
if (!data.info) throw new Error("Invalid spec: missing 'info' field.");
|
|
19
|
+
data.info.title = data.info.title || "Untitled API";
|
|
20
|
+
data.info.version = data.info.version || "0.0.0";
|
|
21
|
+
data.paths = data.paths || {};
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
function getServerUrl(spec) {
|
|
25
|
+
if (spec.servers?.length) {
|
|
26
|
+
let url = spec.servers[0].url;
|
|
27
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
28
|
+
}
|
|
29
|
+
if (spec.host) return `${spec.schemes?.[0] || "https"}://${spec.host}${spec.basePath || ""}`;
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/queries.ts
|
|
34
|
+
const HTTP_METHODS = [
|
|
35
|
+
"get",
|
|
36
|
+
"post",
|
|
37
|
+
"put",
|
|
38
|
+
"patch",
|
|
39
|
+
"delete",
|
|
40
|
+
"options",
|
|
41
|
+
"head"
|
|
42
|
+
];
|
|
43
|
+
const METHOD_PRIORITY = {
|
|
44
|
+
get: 0,
|
|
45
|
+
post: 1,
|
|
46
|
+
put: 2,
|
|
47
|
+
patch: 3,
|
|
48
|
+
delete: 4,
|
|
49
|
+
options: 5,
|
|
50
|
+
head: 6
|
|
51
|
+
};
|
|
52
|
+
function getEndpoints(spec, tag) {
|
|
53
|
+
const eps = [];
|
|
54
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) for (const method of HTTP_METHODS) {
|
|
55
|
+
const op = pathItem[method];
|
|
56
|
+
if (!op) continue;
|
|
57
|
+
if (tag && (!op.tags || !op.tags.includes(tag))) continue;
|
|
58
|
+
eps.push({
|
|
59
|
+
path,
|
|
60
|
+
method,
|
|
61
|
+
summary: op.summary || op.operationId || `${method.toUpperCase()} ${path}`,
|
|
62
|
+
operationId: op.operationId,
|
|
63
|
+
tags: op.tags || [],
|
|
64
|
+
deprecated: op.deprecated || false
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
eps.sort((a, b) => {
|
|
68
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
69
|
+
return (METHOD_PRIORITY[a.method] ?? 99) - (METHOD_PRIORITY[b.method] ?? 99);
|
|
70
|
+
});
|
|
71
|
+
return eps;
|
|
72
|
+
}
|
|
73
|
+
function getTags(spec) {
|
|
74
|
+
const counts = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const pathItem of Object.values(spec.paths)) for (const method of HTTP_METHODS) {
|
|
76
|
+
const op = pathItem[method];
|
|
77
|
+
if (!op?.tags) continue;
|
|
78
|
+
for (const tag of op.tags) counts.set(tag, (counts.get(tag) || 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
return Array.from(counts.entries()).map(([name, count]) => ({
|
|
81
|
+
name,
|
|
82
|
+
count
|
|
83
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
function getSchemas(spec) {
|
|
86
|
+
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
87
|
+
return Object.entries(schemas).map(([name, s]) => ({
|
|
88
|
+
name,
|
|
89
|
+
type: s.type || "object",
|
|
90
|
+
description: s.description || s.title,
|
|
91
|
+
properties: s.properties ? Object.keys(s.properties).length : 0
|
|
92
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
93
|
+
}
|
|
94
|
+
function getSchemaDetail(spec, name) {
|
|
95
|
+
return (spec.components?.schemas || spec.definitions || {})[name] || null;
|
|
96
|
+
}
|
|
97
|
+
function getPathParams(path) {
|
|
98
|
+
const m = path.match(/\{(\w+)\}/g);
|
|
99
|
+
return m ? m.map((s) => s.slice(1, -1)) : [];
|
|
100
|
+
}
|
|
101
|
+
function searchSpec(spec, query) {
|
|
102
|
+
const results = [];
|
|
103
|
+
const lower = query.toLowerCase();
|
|
104
|
+
for (const [path, pi] of Object.entries(spec.paths)) for (const method of HTTP_METHODS) {
|
|
105
|
+
const op = pi[method];
|
|
106
|
+
if (!op) continue;
|
|
107
|
+
if ([
|
|
108
|
+
path,
|
|
109
|
+
method,
|
|
110
|
+
op.summary,
|
|
111
|
+
op.operationId,
|
|
112
|
+
op.description,
|
|
113
|
+
...op.tags || []
|
|
114
|
+
].filter(Boolean).join(" ").toLowerCase().includes(lower)) results.push({
|
|
115
|
+
type: "endpoint",
|
|
116
|
+
path,
|
|
117
|
+
method,
|
|
118
|
+
match: `${method.toUpperCase()} ${path}`,
|
|
119
|
+
summary: op.summary || op.operationId
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
123
|
+
for (const [name, s] of Object.entries(schemas)) {
|
|
124
|
+
const sAny = s;
|
|
125
|
+
if ([
|
|
126
|
+
name,
|
|
127
|
+
sAny.description,
|
|
128
|
+
sAny.title
|
|
129
|
+
].filter(Boolean).join(" ").toLowerCase().includes(lower)) results.push({
|
|
130
|
+
type: "schema",
|
|
131
|
+
schemaName: name,
|
|
132
|
+
match: `Schema: ${name}`,
|
|
133
|
+
summary: sAny.description
|
|
134
|
+
});
|
|
135
|
+
if (sAny.properties) for (const [pn, ps] of Object.entries(sAny.properties)) {
|
|
136
|
+
const psAny = ps;
|
|
137
|
+
if ([pn, psAny.description].filter(Boolean).join(" ").toLowerCase().includes(lower)) results.push({
|
|
138
|
+
type: "property",
|
|
139
|
+
schemaName: name,
|
|
140
|
+
propertyName: pn,
|
|
141
|
+
match: `Schema ${name} > ${pn}`,
|
|
142
|
+
summary: psAny.description
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/commands/info.ts
|
|
150
|
+
async function cmdInfo(source) {
|
|
151
|
+
const spec = await loadSpec(source);
|
|
152
|
+
const eps = getEndpoints(spec);
|
|
153
|
+
const schemas = getSchemas(spec);
|
|
154
|
+
const tags = getTags(spec);
|
|
155
|
+
const url = getServerUrl(spec);
|
|
156
|
+
const sv = spec.openapi || spec.swagger || "unknown";
|
|
157
|
+
const lines = [
|
|
158
|
+
`# ${spec.info.title}`,
|
|
159
|
+
"",
|
|
160
|
+
`Version: ${spec.info.version}`,
|
|
161
|
+
`OpenAPI Version: ${sv}`,
|
|
162
|
+
`Server: \`${url || "Not specified"}\``,
|
|
163
|
+
"",
|
|
164
|
+
spec.info.description || "",
|
|
165
|
+
"",
|
|
166
|
+
"## Summary",
|
|
167
|
+
`- Endpoints: ${eps.length}`,
|
|
168
|
+
`- Schemas: ${schemas.length}`,
|
|
169
|
+
`- Tags: ${tags.length}`
|
|
170
|
+
];
|
|
171
|
+
if (spec.info.contact) {
|
|
172
|
+
lines.push("", "## Contact");
|
|
173
|
+
for (const k of [
|
|
174
|
+
"name",
|
|
175
|
+
"email",
|
|
176
|
+
"url"
|
|
177
|
+
]) {
|
|
178
|
+
const val = spec.info.contact[k];
|
|
179
|
+
if (val) lines.push(`- ${k.charAt(0).toUpperCase() + k.slice(1)}: ${val}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (spec.info.license) {
|
|
183
|
+
lines.push("", "## License");
|
|
184
|
+
lines.push(`- Name: ${spec.info.license.name}`);
|
|
185
|
+
if (spec.info.license.url) lines.push(`- URL: ${spec.info.license.url}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/commands/tags.ts
|
|
191
|
+
async function cmdTags(source) {
|
|
192
|
+
const tags = getTags(await loadSpec(source));
|
|
193
|
+
if (!tags.length) return "No tags found.";
|
|
194
|
+
const lines = [
|
|
195
|
+
"# Tags",
|
|
196
|
+
"",
|
|
197
|
+
"| Tag | Endpoints |",
|
|
198
|
+
"|-----|-----------|"
|
|
199
|
+
];
|
|
200
|
+
for (const t of tags) lines.push(`| \`${t.name}\` | ${t.count} |`);
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/commands/paths.ts
|
|
205
|
+
async function cmdPaths(source, tag, limit = 50, offset = 0) {
|
|
206
|
+
const all = getEndpoints(await loadSpec(source), tag);
|
|
207
|
+
const paginated = all.slice(offset, offset + limit);
|
|
208
|
+
if (!paginated.length) return all.length === 0 ? "No endpoints found." : `No more endpoints (offset ${offset} of ${all.length}).`;
|
|
209
|
+
const hasMore = all.length > offset + paginated.length;
|
|
210
|
+
const lines = ["# Endpoints"];
|
|
211
|
+
if (tag) lines.push(`\nFiltered by tag: \`${tag}\``);
|
|
212
|
+
lines.push(`\n${all.length} total (showing ${paginated.length})`, "");
|
|
213
|
+
for (const ep of paginated) {
|
|
214
|
+
lines.push(`### \`${ep.method.toUpperCase()}\` ${ep.path}${ep.deprecated ? " ~~(deprecated)~~" : ""}`);
|
|
215
|
+
if (ep.summary) lines.push(ep.summary);
|
|
216
|
+
if (ep.operationId) lines.push(`- Operation ID: \`${ep.operationId}\``);
|
|
217
|
+
if (ep.tags.length) lines.push(`- Tags: ${ep.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
if (hasMore) lines.push(`> Showing ${paginated.length} of ${all.length}. Use --offset=${offset + paginated.length} to see more.`);
|
|
221
|
+
return lines.join("\n");
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/core/resolve.ts
|
|
225
|
+
function resolveRef(spec, ref) {
|
|
226
|
+
const parts = ref.replace(/^#\//, "").split("/");
|
|
227
|
+
let cur = spec;
|
|
228
|
+
for (const p of parts) if (cur && typeof cur === "object" && p in cur) cur = cur[p];
|
|
229
|
+
else return null;
|
|
230
|
+
return cur;
|
|
231
|
+
}
|
|
232
|
+
function deepResolve(schema, spec, visited = /* @__PURE__ */ new Set()) {
|
|
233
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
234
|
+
const obj = schema;
|
|
235
|
+
if (obj.$ref && typeof obj.$ref === "string") {
|
|
236
|
+
if (visited.has(obj.$ref)) return {
|
|
237
|
+
$ref: obj.$ref,
|
|
238
|
+
description: `(circular: ${obj.$ref})`
|
|
239
|
+
};
|
|
240
|
+
visited.add(obj.$ref);
|
|
241
|
+
const resolved = resolveRef(spec, obj.$ref);
|
|
242
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
243
|
+
const merged = { ...deepResolve(resolved, spec, new Set(visited)) };
|
|
244
|
+
for (const k of Object.keys(obj)) if (k !== "$ref") merged[k] = obj[k];
|
|
245
|
+
return merged;
|
|
246
|
+
}
|
|
247
|
+
return obj;
|
|
248
|
+
}
|
|
249
|
+
const result = {};
|
|
250
|
+
for (const [k, v] of Object.entries(obj)) if (k === "properties" && v && typeof v === "object") {
|
|
251
|
+
const props = {};
|
|
252
|
+
for (const [pn, ps] of Object.entries(v)) props[pn] = deepResolve(ps, spec, new Set(visited));
|
|
253
|
+
result[k] = props;
|
|
254
|
+
} else if ((k === "items" || k === "additionalProperties") && v && typeof v === "object" && !Array.isArray(v)) result[k] = deepResolve(v, spec, new Set(visited));
|
|
255
|
+
else if ([
|
|
256
|
+
"oneOf",
|
|
257
|
+
"anyOf",
|
|
258
|
+
"allOf"
|
|
259
|
+
].includes(k) && Array.isArray(v)) result[k] = v.map((s) => deepResolve(s, spec, new Set(visited)));
|
|
260
|
+
else result[k] = v;
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/formatters/schema.ts
|
|
265
|
+
function fmtSchema(schema, indent = 0) {
|
|
266
|
+
const pad = " ".repeat(indent);
|
|
267
|
+
if (!schema || typeof schema !== "object") return `${pad}${String(schema)}`;
|
|
268
|
+
const s = schema;
|
|
269
|
+
if (s.$ref && typeof s.$ref === "string") return `${pad}$ref: "${s.$ref.split("/").pop()}"`;
|
|
270
|
+
if (s.enum && Array.isArray(s.enum)) return `${pad}enum: [${s.enum.map((v) => JSON.stringify(v)).join(", ")}]`;
|
|
271
|
+
for (const kw of [
|
|
272
|
+
"oneOf",
|
|
273
|
+
"anyOf",
|
|
274
|
+
"allOf"
|
|
275
|
+
]) if (s[kw] && Array.isArray(s[kw])) {
|
|
276
|
+
const lines = [`${pad}${kw}:`];
|
|
277
|
+
for (let i = 0; i < s[kw].length; i++) {
|
|
278
|
+
if (i > 0) lines.push(`${pad} ---`);
|
|
279
|
+
lines.push(fmtSchema(s[kw][i], indent + 1));
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
const allLines = [];
|
|
284
|
+
if (s.description && typeof s.description === "string") allLines.push(`${pad}// ${s.description}`);
|
|
285
|
+
allLines.push(`${pad}type: ${s.type || "any"}${s.format ? `<${s.format}>` : ""}${s.nullable ? " | null" : ""}`);
|
|
286
|
+
if (s.properties && typeof s.properties === "object") {
|
|
287
|
+
const req = new Set(Array.isArray(s.required) ? s.required : []);
|
|
288
|
+
for (const [pn, ps] of Object.entries(s.properties)) {
|
|
289
|
+
allLines.push(`${pad}${pn}${req.has(pn) ? " (required)" : ""}:`);
|
|
290
|
+
allLines.push(fmtSchema(ps, indent + 1));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (s.items) {
|
|
294
|
+
allLines.push(`${pad}items:`);
|
|
295
|
+
allLines.push(fmtSchema(s.items, indent + 1));
|
|
296
|
+
}
|
|
297
|
+
if (s.additionalProperties && typeof s.additionalProperties === "object") {
|
|
298
|
+
allLines.push(`${pad}additionalProperties:`);
|
|
299
|
+
allLines.push(fmtSchema(s.additionalProperties, indent + 1));
|
|
300
|
+
}
|
|
301
|
+
if (s.example !== void 0) allLines.push(`${pad}example: ${JSON.stringify(s.example)}`);
|
|
302
|
+
if (s.default !== void 0) allLines.push(`${pad}default: ${JSON.stringify(s.default)}`);
|
|
303
|
+
for (const k of [
|
|
304
|
+
"minLength",
|
|
305
|
+
"maxLength",
|
|
306
|
+
"minimum",
|
|
307
|
+
"maximum"
|
|
308
|
+
]) if (s[k] !== void 0) allLines.push(`${pad}${k}: ${s[k]}`);
|
|
309
|
+
if (s.pattern) allLines.push(`${pad}pattern: ${s.pattern}`);
|
|
310
|
+
return allLines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/formatters/endpoint.ts
|
|
314
|
+
function fmtEndpoint(ep) {
|
|
315
|
+
const lines = [`# ${ep.method.toUpperCase()} ${ep.path}`];
|
|
316
|
+
if (ep.deprecated) lines.push("", "DEPRECATED");
|
|
317
|
+
if (ep.summary) lines.push("", ep.summary);
|
|
318
|
+
if (ep.description) lines.push("", ep.description);
|
|
319
|
+
if (ep.operationId) lines.push("", `Operation ID: \`${ep.operationId}\``);
|
|
320
|
+
if (ep.tags?.length) lines.push("", `Tags: ${ep.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
321
|
+
if (ep.parameters?.length) {
|
|
322
|
+
lines.push("", "## Parameters", "", "| Name | In | Type | Required | Description |", "|------|----|------|----------|-------------|");
|
|
323
|
+
for (const p of ep.parameters) lines.push(`| \`${p.name}\` | ${p.in} | ${p.schema?.type || p.type || "string"} | ${p.required ? "Yes" : "No"} | ${p.description || ""} |`);
|
|
324
|
+
}
|
|
325
|
+
if (ep.requestBody) {
|
|
326
|
+
lines.push("", "## Request Body");
|
|
327
|
+
if (ep.requestBody.required) lines.push("> Required");
|
|
328
|
+
if (ep.requestBody.description) lines.push("", ep.requestBody.description);
|
|
329
|
+
for (const [ct, mt] of Object.entries(ep.requestBody.content || {})) lines.push("", `${ct}:`, "```", fmtSchema(mt.schema || {}), "```");
|
|
330
|
+
}
|
|
331
|
+
if (ep.responses) {
|
|
332
|
+
lines.push("", "## Responses");
|
|
333
|
+
for (const [code, resp] of Object.entries(ep.responses)) {
|
|
334
|
+
const r = resp;
|
|
335
|
+
lines.push("", `### ${code}`, r.description);
|
|
336
|
+
if (r.content) for (const [ct, mt] of Object.entries(r.content)) lines.push("", `${ct}:`, "```", fmtSchema(mt.schema || {}), "```");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (ep.security?.length) {
|
|
340
|
+
lines.push("", "## Security");
|
|
341
|
+
for (const sec of ep.security) for (const [scheme, scopes] of Object.entries(sec)) lines.push(`- \`${scheme}\`${Array.isArray(scopes) && scopes.length ? ` [${scopes.join(", ")}]` : ""}`);
|
|
342
|
+
}
|
|
343
|
+
return lines.join("\n");
|
|
344
|
+
}
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/commands/endpoint.ts
|
|
347
|
+
async function cmdEndpoint(source, path, method, full = false) {
|
|
348
|
+
const spec = await loadSpec(source);
|
|
349
|
+
method = method.toLowerCase();
|
|
350
|
+
const op = spec.paths[path]?.[method];
|
|
351
|
+
if (!op) {
|
|
352
|
+
const similar = getEndpoints(spec).filter((e) => e.path.includes(path) || path.includes(e.path)).slice(0, 5);
|
|
353
|
+
const hint = similar.length ? "\n\nDid you mean?\n" + similar.map((e) => ` ${e.method.toUpperCase()} ${e.path}`).join("\n") : "";
|
|
354
|
+
throw new Error(`No ${method.toUpperCase()} ${path} found.${hint}`);
|
|
355
|
+
}
|
|
356
|
+
const paramNames = getPathParams(path);
|
|
357
|
+
const opParams = op.parameters || [];
|
|
358
|
+
const bodyParam = opParams.find((p) => p.in === "body");
|
|
359
|
+
const nonBodyParams = opParams.filter((p) => p.in !== "body");
|
|
360
|
+
const mergedParams = [...paramNames.map((n) => nonBodyParams.find((p) => p.name === n) || {
|
|
361
|
+
name: n,
|
|
362
|
+
in: "path",
|
|
363
|
+
required: true,
|
|
364
|
+
description: `Path param: ${n}`,
|
|
365
|
+
schema: { type: "string" }
|
|
366
|
+
}), ...nonBodyParams.filter((p) => !paramNames.includes(p.name))];
|
|
367
|
+
const resolve = (schema) => full && schema ? deepResolve(schema, spec) : schema;
|
|
368
|
+
let requestBody;
|
|
369
|
+
if (op.requestBody) requestBody = {
|
|
370
|
+
description: op.requestBody.description,
|
|
371
|
+
required: op.requestBody.required,
|
|
372
|
+
content: Object.fromEntries(Object.entries(op.requestBody.content || {}).map(([ct, mt]) => [ct, { schema: resolve(mt.schema) }]))
|
|
373
|
+
};
|
|
374
|
+
else if (bodyParam) {
|
|
375
|
+
const ct = (op.consumes || spec.consumes || ["application/json"])[0] || "application/json";
|
|
376
|
+
requestBody = {
|
|
377
|
+
description: bodyParam.description,
|
|
378
|
+
required: bodyParam.required,
|
|
379
|
+
content: { [ct]: { schema: resolve(bodyParam.schema) } }
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const produces = op.produces || spec.produces || ["application/json"];
|
|
383
|
+
let responses;
|
|
384
|
+
if (op.responses) responses = Object.fromEntries(Object.entries(op.responses).map(([code, resp]) => {
|
|
385
|
+
let content;
|
|
386
|
+
if (resp.content) content = Object.fromEntries(Object.entries(resp.content).map(([ct, mt]) => [ct, { schema: resolve(mt.schema) }]));
|
|
387
|
+
else if (resp.schema) content = { [produces[0] || "application/json"]: { schema: resolve(resp.schema) } };
|
|
388
|
+
return [code, {
|
|
389
|
+
description: resp.description,
|
|
390
|
+
content
|
|
391
|
+
}];
|
|
392
|
+
}));
|
|
393
|
+
return fmtEndpoint({
|
|
394
|
+
path,
|
|
395
|
+
method,
|
|
396
|
+
summary: op.summary,
|
|
397
|
+
description: op.description,
|
|
398
|
+
operationId: op.operationId,
|
|
399
|
+
tags: op.tags || [],
|
|
400
|
+
deprecated: op.deprecated || false,
|
|
401
|
+
parameters: mergedParams.map((p) => ({
|
|
402
|
+
...p,
|
|
403
|
+
schema: resolve(p.schema)
|
|
404
|
+
})),
|
|
405
|
+
requestBody,
|
|
406
|
+
responses,
|
|
407
|
+
security: op.security
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/commands/schemas.ts
|
|
412
|
+
async function cmdSchemas(source, limit = 50, offset = 0) {
|
|
413
|
+
const all = getSchemas(await loadSpec(source));
|
|
414
|
+
const paginated = all.slice(offset, offset + limit);
|
|
415
|
+
if (!paginated.length) return all.length === 0 ? "No schemas defined." : `No more schemas (offset ${offset} of ${all.length}).`;
|
|
416
|
+
const hasMore = all.length > offset + paginated.length;
|
|
417
|
+
const lines = [
|
|
418
|
+
"# Component Schemas",
|
|
419
|
+
"",
|
|
420
|
+
`${all.length} total (showing ${paginated.length})`,
|
|
421
|
+
"",
|
|
422
|
+
"| Name | Type | Properties | Description |",
|
|
423
|
+
"|------|------|------------|-------------|"
|
|
424
|
+
];
|
|
425
|
+
for (const s of paginated) lines.push(`| \`${s.name}\` | ${s.type} | ${s.properties} | ${(s.description || "").slice(0, 80)} |`);
|
|
426
|
+
if (hasMore) lines.push(`\n> Showing ${paginated.length} of ${all.length}. Use --offset=${offset + paginated.length} to see more.`);
|
|
427
|
+
lines.push("\nUse `openapi-explorer schema <source> <schema_name>` for details.");
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/commands/schema.ts
|
|
432
|
+
async function cmdSchema(source, schemaName) {
|
|
433
|
+
const spec = await loadSpec(source);
|
|
434
|
+
const schema = getSchemaDetail(spec, schemaName);
|
|
435
|
+
if (!schema) {
|
|
436
|
+
const suggestions = getSchemas(spec).slice(0, 10).map((s) => s.name);
|
|
437
|
+
throw new Error(`Schema '${schemaName}' not found.\n\nAvailable:\n${suggestions.map((s) => ` - ${s}`).join("\n")}`);
|
|
438
|
+
}
|
|
439
|
+
return [
|
|
440
|
+
`# Schema: ${schemaName}`,
|
|
441
|
+
"",
|
|
442
|
+
"```",
|
|
443
|
+
fmtSchema(schema),
|
|
444
|
+
"```"
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/commands/search.ts
|
|
449
|
+
async function cmdSearch(source, query) {
|
|
450
|
+
const results = searchSpec(await loadSpec(source), query);
|
|
451
|
+
if (!results.length) return `No results for '${query}'.`;
|
|
452
|
+
const lines = [
|
|
453
|
+
`# Search Results for '${query}'`,
|
|
454
|
+
"",
|
|
455
|
+
`Found ${results.length} matches`,
|
|
456
|
+
""
|
|
457
|
+
];
|
|
458
|
+
const eps = results.filter((r) => r.type === "endpoint");
|
|
459
|
+
const schemas = results.filter((r) => r.type === "schema");
|
|
460
|
+
const props = results.filter((r) => r.type === "property");
|
|
461
|
+
if (eps.length) {
|
|
462
|
+
lines.push(`## Endpoints (${eps.length})`);
|
|
463
|
+
for (const e of eps) lines.push(`- ${e.method?.toUpperCase()} \`${e.path}\` - ${e.summary || ""}`);
|
|
464
|
+
lines.push("");
|
|
465
|
+
}
|
|
466
|
+
if (schemas.length) {
|
|
467
|
+
lines.push(`## Schemas (${schemas.length})`);
|
|
468
|
+
for (const s of schemas) lines.push(`- ${s.schemaName} - ${s.summary || ""}`);
|
|
469
|
+
lines.push("");
|
|
470
|
+
}
|
|
471
|
+
if (props.length) {
|
|
472
|
+
lines.push(`## Properties (${props.length})`);
|
|
473
|
+
for (const p of props) lines.push(`- \`${p.schemaName}.${p.propertyName}\` - ${p.summary || ""}`);
|
|
474
|
+
lines.push("");
|
|
475
|
+
}
|
|
476
|
+
return lines.join("\n");
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/cli/args.ts
|
|
480
|
+
function parseArgs(argv) {
|
|
481
|
+
const args = argv.slice(2);
|
|
482
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") return {
|
|
483
|
+
command: "help",
|
|
484
|
+
source: "",
|
|
485
|
+
flags: {},
|
|
486
|
+
positional: []
|
|
487
|
+
};
|
|
488
|
+
const command = args[0] ?? "";
|
|
489
|
+
const source = args[1] ?? "";
|
|
490
|
+
const positional = [];
|
|
491
|
+
const flags = {};
|
|
492
|
+
let i = 2;
|
|
493
|
+
while (i < args.length) {
|
|
494
|
+
const arg = args[i];
|
|
495
|
+
if (arg.startsWith("--")) {
|
|
496
|
+
const key = arg.slice(2);
|
|
497
|
+
if (key === "full") {
|
|
498
|
+
flags[key] = true;
|
|
499
|
+
i++;
|
|
500
|
+
} else if (key === "tag" || key === "limit" || key === "offset") {
|
|
501
|
+
flags[key] = args[i + 1] ?? "";
|
|
502
|
+
i += 2;
|
|
503
|
+
} else {
|
|
504
|
+
flags[key] = args[i + 1] ?? true;
|
|
505
|
+
i += 2;
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
positional.push(arg);
|
|
509
|
+
i++;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
command,
|
|
514
|
+
source,
|
|
515
|
+
flags,
|
|
516
|
+
positional
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function printHelp() {
|
|
520
|
+
return `Usage: openapi-explorer <command> <source> [args...]
|
|
521
|
+
|
|
522
|
+
source can be a URL (https://...) or a local file path.
|
|
523
|
+
|
|
524
|
+
Commands:
|
|
525
|
+
info <source> API overview
|
|
526
|
+
tags <source> List tag groups
|
|
527
|
+
paths <source> [--tag <t>] [--limit N] [--offset N] List endpoints
|
|
528
|
+
endpoint <source> <path> <method> [--full] Endpoint detail
|
|
529
|
+
schemas <source> [--limit N] [--offset N] List schemas
|
|
530
|
+
schema <source> <schema_name> Schema detail
|
|
531
|
+
search <source> <query> Full-text search
|
|
532
|
+
|
|
533
|
+
Examples:
|
|
534
|
+
openapi-explorer info https://petstore.swagger.io/v2/swagger.json
|
|
535
|
+
openapi-explorer endpoint ./spec.json /pets/{petId} get --full
|
|
536
|
+
openapi-explorer paths ./spec.json --tag pets`;
|
|
537
|
+
}
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/cli/dispatch.ts
|
|
540
|
+
async function dispatch(argv) {
|
|
541
|
+
const { command, source, flags, positional } = parseArgs(argv);
|
|
542
|
+
switch (command) {
|
|
543
|
+
case "help": return printHelp();
|
|
544
|
+
case "info":
|
|
545
|
+
if (!source) throw new Error("Usage: swagger.mjs info <source>");
|
|
546
|
+
return await cmdInfo(source);
|
|
547
|
+
case "tags":
|
|
548
|
+
if (!source) throw new Error("Usage: swagger.mjs tags <source>");
|
|
549
|
+
return await cmdTags(source);
|
|
550
|
+
case "paths":
|
|
551
|
+
if (!source) throw new Error("Usage: swagger.mjs paths <source> [--tag <t>] [--limit N] [--offset N]");
|
|
552
|
+
return await cmdPaths(source, typeof flags.tag === "string" ? flags.tag : void 0, typeof flags.limit === "string" ? parseInt(flags.limit, 10) || 50 : 50, typeof flags.offset === "string" ? parseInt(flags.offset, 10) || 0 : 0);
|
|
553
|
+
case "endpoint":
|
|
554
|
+
case "ep": {
|
|
555
|
+
const path = positional[0] || flags.path;
|
|
556
|
+
const method = positional[1] || flags.method;
|
|
557
|
+
if (!source || !path || !method) throw new Error("Usage: swagger.mjs endpoint <source> <path> <method> [--full]");
|
|
558
|
+
return await cmdEndpoint(source, path, method, flags.full === true);
|
|
559
|
+
}
|
|
560
|
+
case "schemas":
|
|
561
|
+
if (!source) throw new Error("Usage: swagger.mjs schemas <source> [--limit N] [--offset N]");
|
|
562
|
+
return await cmdSchemas(source, typeof flags.limit === "string" ? parseInt(flags.limit, 10) || 50 : 50, typeof flags.offset === "string" ? parseInt(flags.offset, 10) || 0 : 0);
|
|
563
|
+
case "schema": {
|
|
564
|
+
const schemaName = positional[0] || flags.name;
|
|
565
|
+
if (!source || !schemaName) throw new Error("Usage: swagger.mjs schema <source> <schema_name>");
|
|
566
|
+
return await cmdSchema(source, schemaName);
|
|
567
|
+
}
|
|
568
|
+
case "search": {
|
|
569
|
+
const query = positional[0] || flags.query;
|
|
570
|
+
if (!source || !query) throw new Error("Usage: swagger.mjs search <source> <query>");
|
|
571
|
+
return await cmdSearch(source, query);
|
|
572
|
+
}
|
|
573
|
+
default: throw new Error(`Unknown command: ${command}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/index.ts
|
|
578
|
+
async function main() {
|
|
579
|
+
const output = await dispatch(process.argv);
|
|
580
|
+
console.log(output);
|
|
581
|
+
}
|
|
582
|
+
main().catch((err) => {
|
|
583
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
});
|
|
586
|
+
//#endregion
|
|
587
|
+
export {};
|
|
588
|
+
|
|
589
|
+
//# sourceMappingURL=index.mjs.map
|