@askills/openapi-explorer-cli 0.0.1
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 +93 -0
- package/package.json +31 -0
- package/src/cli/args.ts +59 -0
- package/src/cli/dispatch.ts +80 -0
- package/src/commands/endpoint.ts +104 -0
- package/src/commands/info.ts +44 -0
- package/src/commands/paths.ts +43 -0
- package/src/commands/schema.ts +22 -0
- package/src/commands/schemas.ts +45 -0
- package/src/commands/search.ts +49 -0
- package/src/commands/tags.ts +11 -0
- package/src/core/loader.ts +38 -0
- package/src/core/queries.ts +165 -0
- package/src/core/resolve.ts +70 -0
- package/src/formatters/endpoint.ts +74 -0
- package/src/formatters/schema.ts +66 -0
- package/src/main.ts +12 -0
- package/src/types.ts +58 -0
- package/test/openapi.json +30965 -0
- package/test/swagger.test.ts +778 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# OpenAPI Explorer CLI
|
|
2
|
+
|
|
3
|
+
CLI tool for progressive exploration of OpenAPI/Swagger API documentation. Load a spec from URL or local file and inspect endpoints, schemas, tags — all in one command, no state, no server.
|
|
4
|
+
|
|
5
|
+
Part of the `admin-skills` monorepo.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Requires Node.js >= 22.18 (TypeScript is run natively via Node's built-in
|
|
10
|
+
type-stripping — no compiler or loader needed).
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# run without installing
|
|
14
|
+
npx @askills/openapi-explorer-cli info https://petstore.swagger.io/v2/swagger.json
|
|
15
|
+
|
|
16
|
+
# or install globally
|
|
17
|
+
npm install -g @askills/openapi-explorer-cli
|
|
18
|
+
openapi-explorer info https://petstore.swagger.io/v2/swagger.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Command | Description |
|
|
24
|
+
| ----------------------------------------------------- | -------------------------------------------------------- |
|
|
25
|
+
| `info <source>` | API overview: title, version, endpoint/schema/tag counts |
|
|
26
|
+
| `tags <source>` | List tag groups with endpoint counts |
|
|
27
|
+
| `paths <source> [--tag <t>] [--limit N] [--offset N]` | List endpoints, filtered by tag, with pagination |
|
|
28
|
+
| `endpoint <source> <path> <method> [--full]` | Endpoint detail. `--full` resolves all `$ref` |
|
|
29
|
+
| `schemas <source> [--limit N] [--offset N]` | List component schemas with pagination |
|
|
30
|
+
| `schema <source> <schema_name>` | Schema detail with properties, types, constraints |
|
|
31
|
+
| `search <source> <query>` | Full-text search across endpoints, schemas, properties |
|
|
32
|
+
|
|
33
|
+
`<source>` is a URL (`https://...`) or local file path — auto-detected.
|
|
34
|
+
|
|
35
|
+
## Progressive Exploration Workflow
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
1. info <source> → overview of the API
|
|
39
|
+
2. tags <source> → see available groups
|
|
40
|
+
3. paths <source> --tag users → browse endpoints in a group
|
|
41
|
+
4. endpoint <source> <path> <method> --full → drill into endpoint
|
|
42
|
+
5. schemas <source> → see available data models
|
|
43
|
+
6. schema <source> <name> → inspect a model
|
|
44
|
+
7. search <source> <query> → find anything across the spec
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node src/main.ts info https://petstore.swagger.io/v2/swagger.json
|
|
51
|
+
node src/main.ts tags ./spec.json
|
|
52
|
+
node src/main.ts paths ./spec.json --tag pets --limit 10
|
|
53
|
+
node src/main.ts schemas ./spec.json --limit 5
|
|
54
|
+
node src/main.ts schema ./spec.json Config
|
|
55
|
+
node src/main.ts endpoint ./spec.json /pets/{petId} get --full
|
|
56
|
+
node src/main.ts search ./spec.json "payment"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
Requires Node.js 22+ (native TypeScript). No build step needed.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm install
|
|
65
|
+
pnpm test
|
|
66
|
+
pnpm start info <source>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The source is TypeScript at `src/`, organized by module:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
src/
|
|
73
|
+
├── main.ts # Entry point
|
|
74
|
+
├── types.ts # Shared types
|
|
75
|
+
├── cli/args.ts # Argument parsing
|
|
76
|
+
├── cli/dispatch.ts # Command routing
|
|
77
|
+
├── commands/ # One file per command
|
|
78
|
+
├── core/loader.ts # Spec loading (URL/file)
|
|
79
|
+
├── core/queries.ts # Query functions
|
|
80
|
+
├── core/resolve.ts # $ref resolution
|
|
81
|
+
└── formatters/ # Schema & endpoint formatting
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Compatibility
|
|
85
|
+
|
|
86
|
+
- OpenAPI v3.x, Swagger v2
|
|
87
|
+
- JSON format only (YAML not supported)
|
|
88
|
+
- Remote URLs and local file paths
|
|
89
|
+
- Node.js 22+ (native TypeScript support required)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@askills/openapi-explorer-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for progressive exploration of OpenAPI/Swagger API documentation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "anuoua",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/anuoua/skills.git",
|
|
11
|
+
"directory": "packages/openapi-explorer-cli"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"openapi-explorer": "src/main.ts"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/main.ts",
|
|
18
|
+
"test": "node --test test/*.test.ts",
|
|
19
|
+
"test:watch": "node --test --watch test/*.test.ts",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.18.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli/args.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function parseArgs(argv: string[]): {
|
|
2
|
+
command: string;
|
|
3
|
+
source: string;
|
|
4
|
+
flags: Record<string, string | boolean | number>;
|
|
5
|
+
positional: string[];
|
|
6
|
+
} {
|
|
7
|
+
const args = argv.slice(2);
|
|
8
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
9
|
+
return { command: "help", source: "", flags: {}, positional: [] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const command = args[0] ?? "";
|
|
13
|
+
const source = args[1] ?? "";
|
|
14
|
+
const positional: string[] = [];
|
|
15
|
+
const flags: Record<string, string | boolean | number> = {};
|
|
16
|
+
|
|
17
|
+
let i = 2;
|
|
18
|
+
while (i < args.length) {
|
|
19
|
+
const arg = args[i]!;
|
|
20
|
+
if (arg.startsWith("--")) {
|
|
21
|
+
const key = arg.slice(2);
|
|
22
|
+
if (key === "full") {
|
|
23
|
+
flags[key] = true;
|
|
24
|
+
i++;
|
|
25
|
+
} else if (key === "tag" || key === "limit" || key === "offset") {
|
|
26
|
+
flags[key] = args[i + 1] ?? "";
|
|
27
|
+
i += 2;
|
|
28
|
+
} else {
|
|
29
|
+
flags[key] = args[i + 1] ?? true;
|
|
30
|
+
i += 2;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
positional.push(arg);
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { command, source, flags, positional };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function printHelp(): string {
|
|
42
|
+
return `Usage: openapi-explorer <command> <source> [args...]
|
|
43
|
+
|
|
44
|
+
source can be a URL (https://...) or a local file path.
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
info <source> API overview
|
|
48
|
+
tags <source> List tag groups
|
|
49
|
+
paths <source> [--tag <t>] [--limit N] [--offset N] List endpoints
|
|
50
|
+
endpoint <source> <path> <method> [--full] Endpoint detail
|
|
51
|
+
schemas <source> [--limit N] [--offset N] List schemas
|
|
52
|
+
schema <source> <schema_name> Schema detail
|
|
53
|
+
search <source> <query> Full-text search
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
openapi-explorer info https://petstore.swagger.io/v2/swagger.json
|
|
57
|
+
openapi-explorer endpoint ./spec.json /pets/{petId} get --full
|
|
58
|
+
openapi-explorer paths ./spec.json --tag pets`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cmdInfo } from "../commands/info.ts";
|
|
2
|
+
import { cmdTags } from "../commands/tags.ts";
|
|
3
|
+
import { cmdPaths } from "../commands/paths.ts";
|
|
4
|
+
import { cmdEndpoint } from "../commands/endpoint.ts";
|
|
5
|
+
import { cmdSchemas } from "../commands/schemas.ts";
|
|
6
|
+
import { cmdSchema } from "../commands/schema.ts";
|
|
7
|
+
import { cmdSearch } from "../commands/search.ts";
|
|
8
|
+
import { parseArgs, printHelp } from "./args.ts";
|
|
9
|
+
|
|
10
|
+
export async function dispatch(argv: string[]): Promise<string> {
|
|
11
|
+
const { command, source, flags, positional } = parseArgs(argv);
|
|
12
|
+
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "help":
|
|
15
|
+
return printHelp();
|
|
16
|
+
|
|
17
|
+
case "info":
|
|
18
|
+
if (!source) throw new Error("Usage: swagger.mjs info <source>");
|
|
19
|
+
return await cmdInfo(source);
|
|
20
|
+
|
|
21
|
+
case "tags":
|
|
22
|
+
if (!source) throw new Error("Usage: swagger.mjs tags <source>");
|
|
23
|
+
return await cmdTags(source);
|
|
24
|
+
|
|
25
|
+
case "paths": {
|
|
26
|
+
if (!source)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Usage: swagger.mjs paths <source> [--tag <t>] [--limit N] [--offset N]",
|
|
29
|
+
);
|
|
30
|
+
const tag = typeof flags.tag === "string" ? flags.tag : undefined;
|
|
31
|
+
const limit =
|
|
32
|
+
typeof flags.limit === "string" ? parseInt(flags.limit, 10) || 50 : 50;
|
|
33
|
+
const offset =
|
|
34
|
+
typeof flags.offset === "string" ? parseInt(flags.offset, 10) || 0 : 0;
|
|
35
|
+
return await cmdPaths(source, tag, limit, offset);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
case "endpoint":
|
|
39
|
+
case "ep": {
|
|
40
|
+
const path = positional[0] || flags.path;
|
|
41
|
+
const method = positional[1] || flags.method;
|
|
42
|
+
if (!source || !path || !method) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Usage: swagger.mjs endpoint <source> <path> <method> [--full]",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const full = flags.full === true;
|
|
48
|
+
return await cmdEndpoint(source, path as string, method as string, full);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "schemas": {
|
|
52
|
+
if (!source)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Usage: swagger.mjs schemas <source> [--limit N] [--offset N]",
|
|
55
|
+
);
|
|
56
|
+
const slimit =
|
|
57
|
+
typeof flags.limit === "string" ? parseInt(flags.limit, 10) || 50 : 50;
|
|
58
|
+
const soffset =
|
|
59
|
+
typeof flags.offset === "string" ? parseInt(flags.offset, 10) || 0 : 0;
|
|
60
|
+
return await cmdSchemas(source, slimit, soffset);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "schema": {
|
|
64
|
+
const schemaName = positional[0] || flags.name;
|
|
65
|
+
if (!source || !schemaName)
|
|
66
|
+
throw new Error("Usage: swagger.mjs schema <source> <schema_name>");
|
|
67
|
+
return await cmdSchema(source, schemaName as string);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "search": {
|
|
71
|
+
const query = positional[0] || flags.query;
|
|
72
|
+
if (!source || !query)
|
|
73
|
+
throw new Error("Usage: swagger.mjs search <source> <query>");
|
|
74
|
+
return await cmdSearch(source, query as string);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unknown command: ${command}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { getEndpoints, getPathParams } from "../core/queries.ts";
|
|
3
|
+
import { deepResolve } from "../core/resolve.ts";
|
|
4
|
+
import { fmtEndpoint } from "../formatters/endpoint.ts";
|
|
5
|
+
import type { FormattedEndpoint } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
export async function cmdEndpoint(
|
|
8
|
+
source: string,
|
|
9
|
+
path: string,
|
|
10
|
+
method: string,
|
|
11
|
+
full = false,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
const spec = await loadSpec(source);
|
|
14
|
+
method = method.toLowerCase();
|
|
15
|
+
const op = spec.paths[path]?.[method];
|
|
16
|
+
if (!op) {
|
|
17
|
+
const similar = getEndpoints(spec)
|
|
18
|
+
.filter((e) => e.path.includes(path) || path.includes(e.path))
|
|
19
|
+
.slice(0, 5);
|
|
20
|
+
const hint = similar.length
|
|
21
|
+
? "\n\nDid you mean?\n" +
|
|
22
|
+
similar.map((e) => ` ${e.method.toUpperCase()} ${e.path}`).join("\n")
|
|
23
|
+
: "";
|
|
24
|
+
throw new Error(`No ${method.toUpperCase()} ${path} found.${hint}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const paramNames = getPathParams(path);
|
|
28
|
+
const mergedParams = [
|
|
29
|
+
...paramNames.map(
|
|
30
|
+
(n) =>
|
|
31
|
+
(op.parameters || []).find((p: any) => p.name === n) || {
|
|
32
|
+
name: n,
|
|
33
|
+
in: "path",
|
|
34
|
+
required: true,
|
|
35
|
+
description: `Path param: ${n}`,
|
|
36
|
+
schema: { type: "string" },
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
...(op.parameters || []).filter((p: any) => !paramNames.includes(p.name)),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const ep: FormattedEndpoint = {
|
|
43
|
+
path,
|
|
44
|
+
method,
|
|
45
|
+
summary: op.summary,
|
|
46
|
+
description: op.description,
|
|
47
|
+
operationId: op.operationId,
|
|
48
|
+
tags: op.tags || [],
|
|
49
|
+
deprecated: op.deprecated || false,
|
|
50
|
+
parameters: full
|
|
51
|
+
? mergedParams.map((p: any) => ({
|
|
52
|
+
...p,
|
|
53
|
+
schema: p.schema ? deepResolve(p.schema, spec) : p.schema,
|
|
54
|
+
}))
|
|
55
|
+
: mergedParams,
|
|
56
|
+
requestBody: op.requestBody
|
|
57
|
+
? {
|
|
58
|
+
description: op.requestBody.description,
|
|
59
|
+
required: op.requestBody.required,
|
|
60
|
+
content: Object.fromEntries(
|
|
61
|
+
Object.entries(op.requestBody.content).map(
|
|
62
|
+
([ct, mt]: [string, any]) => [
|
|
63
|
+
ct,
|
|
64
|
+
{
|
|
65
|
+
schema:
|
|
66
|
+
full && mt.schema
|
|
67
|
+
? deepResolve(mt.schema, spec)
|
|
68
|
+
: mt.schema,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
responses: op.responses
|
|
76
|
+
? Object.fromEntries(
|
|
77
|
+
Object.entries(op.responses).map(([code, resp]: [string, any]) => [
|
|
78
|
+
code,
|
|
79
|
+
{
|
|
80
|
+
description: resp.description,
|
|
81
|
+
content: resp.content
|
|
82
|
+
? Object.fromEntries(
|
|
83
|
+
Object.entries(resp.content).map(
|
|
84
|
+
([ct, mt]: [string, any]) => [
|
|
85
|
+
ct,
|
|
86
|
+
{
|
|
87
|
+
schema:
|
|
88
|
+
full && mt.schema
|
|
89
|
+
? deepResolve(mt.schema, spec)
|
|
90
|
+
: mt.schema,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
: undefined,
|
|
96
|
+
},
|
|
97
|
+
]),
|
|
98
|
+
)
|
|
99
|
+
: undefined,
|
|
100
|
+
security: op.security,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return fmtEndpoint(ep);
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { OpenAPISpec } from "../types.ts";
|
|
2
|
+
import { loadSpec, getServerUrl } from "../core/loader.ts";
|
|
3
|
+
import { getEndpoints, getSchemas, getTags } from "../core/queries.ts";
|
|
4
|
+
|
|
5
|
+
export async function cmdInfo(source: string): Promise<string> {
|
|
6
|
+
const spec = await loadSpec(source);
|
|
7
|
+
const eps = getEndpoints(spec);
|
|
8
|
+
const schemas = getSchemas(spec);
|
|
9
|
+
const tags = getTags(spec);
|
|
10
|
+
const url = getServerUrl(spec);
|
|
11
|
+
const sv = spec.openapi || spec.swagger || "unknown";
|
|
12
|
+
|
|
13
|
+
const lines: string[] = [
|
|
14
|
+
`# ${spec.info.title}`,
|
|
15
|
+
"",
|
|
16
|
+
`Version: ${spec.info.version}`,
|
|
17
|
+
`OpenAPI Version: ${sv}`,
|
|
18
|
+
`Server: \`${url || "Not specified"}\``,
|
|
19
|
+
"",
|
|
20
|
+
spec.info.description || "",
|
|
21
|
+
"",
|
|
22
|
+
"## Summary",
|
|
23
|
+
`- Endpoints: ${eps.length}`,
|
|
24
|
+
`- Schemas: ${schemas.length}`,
|
|
25
|
+
`- Tags: ${tags.length}`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
if (spec.info.contact) {
|
|
29
|
+
lines.push("", "## Contact");
|
|
30
|
+
for (const k of ["name", "email", "url"] as const) {
|
|
31
|
+
const val = spec.info.contact[k];
|
|
32
|
+
if (val)
|
|
33
|
+
lines.push(`- ${k.charAt(0).toUpperCase() + k.slice(1)}: ${val}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (spec.info.license) {
|
|
38
|
+
lines.push("", "## License");
|
|
39
|
+
lines.push(`- Name: ${spec.info.license.name}`);
|
|
40
|
+
if (spec.info.license.url) lines.push(`- URL: ${spec.info.license.url}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { getEndpoints } from "../core/queries.ts";
|
|
3
|
+
|
|
4
|
+
export async function cmdPaths(
|
|
5
|
+
source: string,
|
|
6
|
+
tag?: string,
|
|
7
|
+
limit = 50,
|
|
8
|
+
offset = 0,
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const spec = await loadSpec(source);
|
|
11
|
+
const all = getEndpoints(spec, tag);
|
|
12
|
+
const paginated = all.slice(offset, offset + limit);
|
|
13
|
+
|
|
14
|
+
if (!paginated.length) {
|
|
15
|
+
return all.length === 0
|
|
16
|
+
? "No endpoints found."
|
|
17
|
+
: `No more endpoints (offset ${offset} of ${all.length}).`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hasMore = all.length > offset + paginated.length;
|
|
21
|
+
const lines: string[] = ["# Endpoints"];
|
|
22
|
+
if (tag) lines.push(`\nFiltered by tag: \`${tag}\``);
|
|
23
|
+
lines.push(`\n${all.length} total (showing ${paginated.length})`, "");
|
|
24
|
+
|
|
25
|
+
for (const ep of paginated) {
|
|
26
|
+
lines.push(
|
|
27
|
+
`### \`${ep.method.toUpperCase()}\` ${ep.path}${ep.deprecated ? " ~~(deprecated)~~" : ""}`,
|
|
28
|
+
);
|
|
29
|
+
if (ep.summary) lines.push(ep.summary);
|
|
30
|
+
if (ep.operationId) lines.push(`- Operation ID: \`${ep.operationId}\``);
|
|
31
|
+
if (ep.tags.length)
|
|
32
|
+
lines.push(`- Tags: ${ep.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
33
|
+
lines.push("");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (hasMore) {
|
|
37
|
+
lines.push(
|
|
38
|
+
`> Showing ${paginated.length} of ${all.length}. Use --offset=${offset + paginated.length} to see more.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { getSchemas, getSchemaDetail } from "../core/queries.ts";
|
|
3
|
+
import { fmtSchema } from "../formatters/schema.ts";
|
|
4
|
+
|
|
5
|
+
export async function cmdSchema(
|
|
6
|
+
source: string,
|
|
7
|
+
schemaName: string,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
const spec = await loadSpec(source);
|
|
10
|
+
const schema = getSchemaDetail(spec, schemaName);
|
|
11
|
+
if (!schema) {
|
|
12
|
+
const suggestions = getSchemas(spec)
|
|
13
|
+
.slice(0, 10)
|
|
14
|
+
.map((s) => s.name);
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Schema '${schemaName}' not found.\n\nAvailable:\n${suggestions.map((s) => ` - ${s}`).join("\n")}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return [`# Schema: ${schemaName}`, "", "```", fmtSchema(schema), "```"].join(
|
|
20
|
+
"\n",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { getSchemas } from "../core/queries.ts";
|
|
3
|
+
|
|
4
|
+
export async function cmdSchemas(
|
|
5
|
+
source: string,
|
|
6
|
+
limit = 50,
|
|
7
|
+
offset = 0,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
const spec = await loadSpec(source);
|
|
10
|
+
const all = getSchemas(spec);
|
|
11
|
+
const paginated = all.slice(offset, offset + limit);
|
|
12
|
+
|
|
13
|
+
if (!paginated.length) {
|
|
14
|
+
return all.length === 0
|
|
15
|
+
? "No schemas defined."
|
|
16
|
+
: `No more schemas (offset ${offset} of ${all.length}).`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hasMore = all.length > offset + paginated.length;
|
|
20
|
+
const lines: string[] = [
|
|
21
|
+
"# Component Schemas",
|
|
22
|
+
"",
|
|
23
|
+
`${all.length} total (showing ${paginated.length})`,
|
|
24
|
+
"",
|
|
25
|
+
"| Name | Type | Properties | Description |",
|
|
26
|
+
"|------|------|------------|-------------|",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const s of paginated) {
|
|
30
|
+
lines.push(
|
|
31
|
+
`| \`${s.name}\` | ${s.type} | ${s.properties} | ${(s.description || "").slice(0, 80)} |`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (hasMore) {
|
|
36
|
+
lines.push(
|
|
37
|
+
`\n> Showing ${paginated.length} of ${all.length}. Use --offset=${offset + paginated.length} to see more.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lines.push(
|
|
42
|
+
"\nUse `openapi-explorer schema <source> <schema_name>` for details.",
|
|
43
|
+
);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { searchSpec } from "../core/queries.ts";
|
|
3
|
+
|
|
4
|
+
export async function cmdSearch(
|
|
5
|
+
source: string,
|
|
6
|
+
query: string,
|
|
7
|
+
): Promise<string> {
|
|
8
|
+
const spec = await loadSpec(source);
|
|
9
|
+
const results = searchSpec(spec, query);
|
|
10
|
+
if (!results.length) return `No results for '${query}'.`;
|
|
11
|
+
|
|
12
|
+
const lines: string[] = [
|
|
13
|
+
`# Search Results for '${query}'`,
|
|
14
|
+
"",
|
|
15
|
+
`Found ${results.length} matches`,
|
|
16
|
+
"",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const eps = results.filter((r) => r.type === "endpoint");
|
|
20
|
+
const schemas = results.filter((r) => r.type === "schema");
|
|
21
|
+
const props = results.filter((r) => r.type === "property");
|
|
22
|
+
|
|
23
|
+
if (eps.length) {
|
|
24
|
+
lines.push(`## Endpoints (${eps.length})`);
|
|
25
|
+
for (const e of eps)
|
|
26
|
+
lines.push(
|
|
27
|
+
`- ${e.method?.toUpperCase()} \`${e.path}\` - ${e.summary || ""}`,
|
|
28
|
+
);
|
|
29
|
+
lines.push("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (schemas.length) {
|
|
33
|
+
lines.push(`## Schemas (${schemas.length})`);
|
|
34
|
+
for (const s of schemas)
|
|
35
|
+
lines.push(`- ${s.schemaName} - ${s.summary || ""}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (props.length) {
|
|
40
|
+
lines.push(`## Properties (${props.length})`);
|
|
41
|
+
for (const p of props)
|
|
42
|
+
lines.push(
|
|
43
|
+
`- \`${p.schemaName}.${p.propertyName}\` - ${p.summary || ""}`,
|
|
44
|
+
);
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { loadSpec } from "../core/loader.ts";
|
|
2
|
+
import { getTags } from "../core/queries.ts";
|
|
3
|
+
|
|
4
|
+
export async function cmdTags(source: string): Promise<string> {
|
|
5
|
+
const spec = await loadSpec(source);
|
|
6
|
+
const tags = getTags(spec);
|
|
7
|
+
if (!tags.length) return "No tags found.";
|
|
8
|
+
const lines = ["# Tags", "", "| Tag | Endpoints |", "|-----|-----------|"];
|
|
9
|
+
for (const t of tags) lines.push(`| \`${t.name}\` | ${t.count} |`);
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { OpenAPISpec } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
export async function loadSpec(source: string): Promise<OpenAPISpec> {
|
|
6
|
+
const isUrl = /^https?:\/\//i.test(source);
|
|
7
|
+
let raw: string;
|
|
8
|
+
|
|
9
|
+
if (isUrl) {
|
|
10
|
+
const res = await fetch(source, {
|
|
11
|
+
headers: { Accept: "application/json, text/plain" },
|
|
12
|
+
signal: AbortSignal.timeout(30000),
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${source}`);
|
|
15
|
+
raw = await res.text();
|
|
16
|
+
} else {
|
|
17
|
+
raw = await readFile(resolve(source), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = JSON.parse(raw) as OpenAPISpec;
|
|
21
|
+
if (!data.openapi && !data.swagger)
|
|
22
|
+
throw new Error("Not a valid OpenAPI/Swagger spec.");
|
|
23
|
+
if (!data.info) throw new Error("Invalid spec: missing 'info' field.");
|
|
24
|
+
data.info.title = data.info.title || "Untitled API";
|
|
25
|
+
data.info.version = data.info.version || "0.0.0";
|
|
26
|
+
data.paths = data.paths || {};
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getServerUrl(spec: OpenAPISpec): string {
|
|
31
|
+
if (spec.servers?.length) {
|
|
32
|
+
let url = spec.servers[0]!.url;
|
|
33
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
34
|
+
}
|
|
35
|
+
if (spec.host)
|
|
36
|
+
return `${spec.schemes?.[0] || "https"}://${spec.host}${spec.basePath || ""}`;
|
|
37
|
+
return "";
|
|
38
|
+
}
|