@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 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
+ }
@@ -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
+ }