api-spec-cli 0.0.4 → 0.1.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 +86 -130
- package/package.json +1 -1
- package/src/cli.js +45 -21
- package/src/commands/call.js +12 -3
- package/src/commands/list.js +54 -17
- package/src/commands/show.js +106 -46
- package/src/commands/types.js +165 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# spec-cli
|
|
1
|
+
# api-spec-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for AI agents to explore and call OpenAPI and GraphQL APIs. Output is JSON by default — compact, parseable, token-efficient.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,190 +8,146 @@ Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed
|
|
|
8
8
|
npm install -g api-spec-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Works with Node.js 18+ or Bun. No other dependencies.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
# Or run without installing
|
|
15
|
+
npx api-spec-cli <command>
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
## How It Works
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
npx api-spec-cli <command>
|
|
21
|
-
bunx api-spec-cli <command>
|
|
22
|
-
```
|
|
20
|
+
The CLI follows a progressive discovery pattern. You never dump an entire API spec at once — instead you narrow down to what you need.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
### Step 1: Load the spec
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
|
-
|
|
28
|
-
spec load
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
spec load https://gql.hashnode.com
|
|
25
|
+
spec load https://petstore3.swagger.io/api/v3/openapi.json # OpenAPI
|
|
26
|
+
spec load ./openapi.yaml # Local file
|
|
27
|
+
spec load https://gql.hashnode.com # GraphQL (introspection)
|
|
28
|
+
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
Output tells you what was loaded:
|
|
31
|
+
```json
|
|
32
|
+
{ "ok": true, "type": "graphql", "operationCount": 114, "source": "https://gql.hashnode.com" }
|
|
33
|
+
```
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
spec show getPetById
|
|
39
|
-
spec show publishPost
|
|
35
|
+
### Step 2: Find what you need
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
43
|
-
spec call findPetsByStatus --query status=available
|
|
37
|
+
`list` is compact by default — just operation IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
spec
|
|
47
|
-
spec
|
|
39
|
+
```bash
|
|
40
|
+
spec list # All operations (compact IDs only)
|
|
41
|
+
spec list --filter publish # Search by keyword
|
|
42
|
+
spec list --tag pets # OpenAPI: filter by tag
|
|
43
|
+
spec list --tag mutation # GraphQL: filter by kind (query/mutation/subscription)
|
|
44
|
+
spec list --limit 10 # First 10 only
|
|
45
|
+
spec list --limit 10 --offset 10 # Next 10
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
Compact output (token-efficient):
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "graphql",
|
|
52
|
+
"total": 5,
|
|
53
|
+
"showing": 5,
|
|
54
|
+
"operations": [
|
|
55
|
+
{ "id": "publishPost", "kind": "mutation" },
|
|
56
|
+
{ "id": "publishDraft", "kind": "mutation" }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
Use `--compact false` for full details (summary, tags, args).
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
### Step 3: Inspect one operation
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
- **GraphQL**: Runs introspection query on the endpoint
|
|
65
|
+
`show` gives you everything you need to call an operation — params, body schema, response, and related types — in one call.
|
|
58
66
|
|
|
59
67
|
```bash
|
|
60
|
-
spec
|
|
61
|
-
spec
|
|
62
|
-
spec
|
|
68
|
+
spec show publishPost # GraphQL: by operation name
|
|
69
|
+
spec show getPetById # OpenAPI: by operationId
|
|
70
|
+
spec show /pet/{petId} # OpenAPI: by path
|
|
71
|
+
spec show "GET /pet/{petId}" # OpenAPI: by method + path
|
|
63
72
|
```
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
Schemas are compact. Nested `$ref` references show as type names (not exploded), so the output stays small. If you need details on a referenced type, use `spec types <name>`.
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
### Step 4: Drill into types (if needed)
|
|
68
77
|
|
|
69
78
|
```bash
|
|
70
|
-
spec
|
|
71
|
-
spec
|
|
79
|
+
spec types # List all schema/type names
|
|
80
|
+
spec types Pet # Inspect one schema
|
|
81
|
+
spec types PublishPostInput # Inspect a GraphQL input type
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
**GraphQL output**: `id`, `kind` (query/mutation/subscription), `description`, `args`, `returnType`
|
|
76
|
-
|
|
77
|
-
### `spec show <operation>`
|
|
84
|
+
This is optional — `show` already includes related types inline. Use `types` only when you need a type that wasn't included in the `show` output.
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
### Step 5: Call the API
|
|
80
87
|
|
|
81
88
|
```bash
|
|
82
|
-
|
|
83
|
-
spec
|
|
84
|
-
spec
|
|
85
|
-
spec show publishPost # GraphQL operation name
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
For OpenAPI: resolves `$ref` references in parameters, request body, and responses.
|
|
89
|
-
For GraphQL: includes related types with field definitions.
|
|
90
|
-
|
|
91
|
-
### `spec call <operation> [options]`
|
|
92
|
-
|
|
93
|
-
Execute an API request.
|
|
89
|
+
# Set base URL and auth first (persisted across calls)
|
|
90
|
+
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
91
|
+
spec config set auth YOUR_TOKEN
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
# OpenAPI
|
|
97
|
-
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
93
|
+
# OpenAPI calls
|
|
98
94
|
spec call getPetById --var petId=1
|
|
99
95
|
spec call findPetsByStatus --query status=available
|
|
100
|
-
spec call
|
|
96
|
+
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
101
97
|
|
|
102
|
-
# GraphQL
|
|
98
|
+
# GraphQL calls (auto-generates query from schema)
|
|
103
99
|
spec call me
|
|
104
|
-
spec call publication --var host=blog.
|
|
105
|
-
spec call publishPost --data '{"query":"mutation { ... }"}'
|
|
100
|
+
spec call publication --var host=blog.hashnode.dev
|
|
106
101
|
```
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
| Flag | Description |
|
|
110
|
-
|------|-------------|
|
|
111
|
-
| `--data '{"key":"val"}'` | Request body (JSON) |
|
|
112
|
-
| `--query key=val` | Query parameter (repeatable) |
|
|
113
|
-
| `--header key=val` | Per-request header (repeatable) |
|
|
114
|
-
| `--var key=val` | Path variable or GraphQL variable (repeatable) |
|
|
115
|
-
| `--method GET\|POST\|...` | Override HTTP method |
|
|
103
|
+
## Config
|
|
116
104
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### `spec validate <file-or-url>`
|
|
120
|
-
|
|
121
|
-
Validate an OpenAPI spec and report errors and warnings.
|
|
105
|
+
Persistent config stored in `.spec-cli/config.json`. Set once, used for all calls.
|
|
122
106
|
|
|
123
107
|
```bash
|
|
124
|
-
spec
|
|
125
|
-
spec
|
|
108
|
+
spec config set baseUrl https://api.example.com
|
|
109
|
+
spec config set auth my-token # Auto-adds "Bearer " prefix
|
|
110
|
+
spec config set auth "Basic dXNlcjpwYXNz" # Or explicit auth header
|
|
111
|
+
spec config set headers.X-API-Key abc123 # Custom headers (dot notation)
|
|
112
|
+
spec config get # Show all config
|
|
113
|
+
spec config unset auth # Remove a key
|
|
126
114
|
```
|
|
127
115
|
|
|
128
|
-
|
|
129
|
-
- Required fields (`info`, `info.title`, `info.version`, `paths`)
|
|
130
|
-
- Valid HTTP methods and schema types
|
|
131
|
-
- Unique `operationId` values
|
|
132
|
-
- Broken `$ref` references
|
|
133
|
-
- Array schemas have `items`
|
|
134
|
-
- Path parameters are declared
|
|
135
|
-
- Server/host definitions
|
|
136
|
-
- Unusual patterns (request body on GET)
|
|
116
|
+
## Validate
|
|
137
117
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Manage persistent configuration stored in `.spec-cli/config.json`.
|
|
118
|
+
Check an OpenAPI spec for errors before using it:
|
|
141
119
|
|
|
142
120
|
```bash
|
|
143
|
-
spec
|
|
144
|
-
spec config get baseUrl # show single key
|
|
145
|
-
spec config set baseUrl https://api.example.com
|
|
146
|
-
spec config set auth my-api-token # adds Bearer prefix
|
|
147
|
-
spec config set auth "Bearer my-token" # explicit Bearer
|
|
148
|
-
spec config set auth "Basic dXNlcjpwYXNz" # Basic auth
|
|
149
|
-
spec config set headers.X-API-Key abc123 # custom header (dot notation)
|
|
150
|
-
spec config unset headers.X-API-Key # remove a key
|
|
121
|
+
spec validate https://api.example.com/openapi.json
|
|
151
122
|
```
|
|
152
123
|
|
|
153
|
-
|
|
124
|
+
Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
|
|
154
125
|
|
|
155
|
-
|
|
126
|
+
## Output Format
|
|
127
|
+
|
|
128
|
+
JSON by default. Use `--format text` or `--format yaml` for alternatives:
|
|
156
129
|
|
|
157
130
|
```bash
|
|
158
|
-
spec list --format
|
|
159
|
-
spec
|
|
160
|
-
spec list --format yaml # YAML
|
|
131
|
+
spec list --format text
|
|
132
|
+
spec show getPetById --format yaml
|
|
161
133
|
```
|
|
162
134
|
|
|
163
|
-
Errors always
|
|
164
|
-
|
|
165
|
-
## GraphQL Coverage
|
|
135
|
+
Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
|
|
166
136
|
|
|
167
|
-
|
|
168
|
-
- **Queries** — all root query fields
|
|
169
|
-
- **Mutations** — all root mutation fields
|
|
170
|
-
- **Subscriptions** — all root subscription fields
|
|
171
|
-
- **Types** — input objects, enums, scalars, object types with fields
|
|
172
|
-
- **Args** — full argument definitions with types and defaults
|
|
137
|
+
## Token Efficiency
|
|
173
138
|
|
|
174
|
-
|
|
139
|
+
The CLI is designed to minimize context window usage for AI agents:
|
|
175
140
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
4. **Auto-query building** — GraphQL calls auto-generate queries from the schema
|
|
182
|
-
5. **Persistent config** — set auth once, use across calls
|
|
183
|
-
6. **No interactive prompts** — everything is flags and args
|
|
141
|
+
- `list` returns only IDs by default (not full schemas)
|
|
142
|
+
- `show` resolves schemas compactly — nested refs show as names, not deep explosions
|
|
143
|
+
- `types` lets you inspect one type at a time instead of loading all schemas
|
|
144
|
+
- `--limit` and `--offset` paginate large APIs
|
|
145
|
+
- `--filter` and `--tag` narrow results before output
|
|
184
146
|
|
|
185
147
|
## Storage
|
|
186
148
|
|
|
187
|
-
All state
|
|
188
|
-
- `spec.json` —
|
|
189
|
-
- `config.json` — base URL,
|
|
149
|
+
All state lives in `.spec-cli/` in the working directory:
|
|
150
|
+
- `spec.json` — loaded spec cache
|
|
151
|
+
- `config.json` — base URL, auth, headers
|
|
190
152
|
|
|
191
153
|
Add `.spec-cli/` to your `.gitignore`.
|
|
192
|
-
|
|
193
|
-
## Dependencies
|
|
194
|
-
|
|
195
|
-
- [yaml](https://www.npmjs.com/package/yaml) — YAML parsing (for OpenAPI YAML specs and YAML output)
|
|
196
|
-
|
|
197
|
-
No other runtime dependencies. Works with Node.js 18+ or Bun (uses native `fetch`).
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,33 +4,53 @@ import { showOperation } from "./commands/show.js";
|
|
|
4
4
|
import { callOperation } from "./commands/call.js";
|
|
5
5
|
import { configCmd } from "./commands/config.js";
|
|
6
6
|
import { validateSpec } from "./commands/validate.js";
|
|
7
|
+
import { typesCmd } from "./commands/types.js";
|
|
7
8
|
import { out, err, setFormat } from "./output.js";
|
|
8
9
|
|
|
9
|
-
const HELP = `spec-cli —
|
|
10
|
+
const HELP = `spec-cli — Explore and call APIs from the command line.
|
|
11
|
+
All output is JSON. Designed for AI agents but works for humans too.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
spec load <file-or-url>
|
|
13
|
-
spec list
|
|
14
|
-
spec show <operation>
|
|
15
|
-
spec call <operation> [options]
|
|
16
|
-
spec validate <file-or-url> Validate an OpenAPI spec (errors + warnings)
|
|
17
|
-
spec config set <key> <value> Set config (baseUrl, headers.X-Key, auth)
|
|
18
|
-
spec config get [key] Show config
|
|
19
|
-
spec config unset <key> Remove config key
|
|
13
|
+
WORKFLOW (follow this order):
|
|
14
|
+
1. spec load <file-or-url> Load an OpenAPI or GraphQL spec
|
|
15
|
+
2. spec list Browse operations (compact IDs)
|
|
16
|
+
3. spec show <operation> Get params, body, response for one op
|
|
17
|
+
4. spec call <operation> [options] Execute the request
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--query key=val Query parameters (repeatable)
|
|
24
|
-
--header key=val Per-request headers (repeatable)
|
|
25
|
-
--var key=val Path variables (repeatable)
|
|
26
|
-
--method GET|POST|... Override HTTP method
|
|
19
|
+
Use spec types [name] to inspect a schema/type referenced by show.
|
|
20
|
+
Use spec config to set baseUrl, auth, and headers before calling.
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
--
|
|
22
|
+
DISCOVERY (narrowing down):
|
|
23
|
+
spec list All operations (just IDs)
|
|
24
|
+
spec list --filter user Search across all fields
|
|
25
|
+
spec list --tag pets OpenAPI tag or GraphQL kind (query/mutation)
|
|
26
|
+
spec list --limit 10 --offset 20 Paginate large APIs
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
INSPECT:
|
|
29
|
+
spec show getPetById Match by operationId
|
|
30
|
+
spec show /pet/{petId} Match by path
|
|
31
|
+
spec show "GET /pet/{petId}" Match by method + path
|
|
32
|
+
spec show publishPost GraphQL operation name
|
|
33
|
+
spec types List all schema/type names
|
|
34
|
+
spec types Pet Inspect one schema (compact, no $ref explosion)
|
|
35
|
+
|
|
36
|
+
EXECUTE:
|
|
37
|
+
spec call <op> --var petId=1 Path or GraphQL variables
|
|
38
|
+
spec call <op> --query status=available Query string params
|
|
39
|
+
spec call <op> --data '{"name":"Rex"}' JSON body
|
|
40
|
+
spec call <op> --data-file /tmp/query.json JSON body from file (avoids shell escaping)
|
|
41
|
+
spec call <op> --header X-Custom=val Extra headers
|
|
42
|
+
spec call <op> --method PUT Override HTTP method
|
|
43
|
+
|
|
44
|
+
CONFIG (persisted in .spec-cli/config.json):
|
|
45
|
+
spec config set baseUrl https://api.example.com
|
|
46
|
+
spec config set auth <token> Auto-adds Bearer prefix
|
|
47
|
+
spec config set headers.X-API-Key <key> Dot notation for nested keys
|
|
48
|
+
spec config get Show current config
|
|
49
|
+
spec config unset auth Remove a key
|
|
50
|
+
|
|
51
|
+
OTHER:
|
|
52
|
+
spec validate <file-or-url> Check OpenAPI spec for errors
|
|
53
|
+
--format json|text|yaml Output format (default: json)`;
|
|
34
54
|
|
|
35
55
|
export async function run(args) {
|
|
36
56
|
// Extract --format before routing
|
|
@@ -65,6 +85,10 @@ export async function run(args) {
|
|
|
65
85
|
case "validate":
|
|
66
86
|
await validateSpec(args.slice(1));
|
|
67
87
|
break;
|
|
88
|
+
case "types":
|
|
89
|
+
case "type":
|
|
90
|
+
await typesCmd(args.slice(1));
|
|
91
|
+
break;
|
|
68
92
|
case "config":
|
|
69
93
|
case "cfg":
|
|
70
94
|
await configCmd(args.slice(1));
|
package/src/commands/call.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { getSpec, getConfig } from "../store.js";
|
|
2
3
|
import { out } from "../output.js";
|
|
3
4
|
import { parseArgs, parseKV } from "../args.js";
|
|
@@ -5,7 +6,12 @@ import { parseArgs, parseKV } from "../args.js";
|
|
|
5
6
|
export async function callOperation(args) {
|
|
6
7
|
const { flags, positional } = parseArgs(args);
|
|
7
8
|
const target = positional[0];
|
|
8
|
-
if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
|
|
9
|
+
if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--data-file path.json] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
|
|
10
|
+
|
|
11
|
+
// Support --data-file to avoid shell escaping issues
|
|
12
|
+
if (flags["data-file"] && !flags.data) {
|
|
13
|
+
flags.data = readFileSync(flags["data-file"], "utf-8").trim();
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
const spec = getSpec();
|
|
11
17
|
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
@@ -103,11 +109,13 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
103
109
|
|
|
104
110
|
// Build query from operation
|
|
105
111
|
let query;
|
|
112
|
+
let dataVariables;
|
|
106
113
|
if (flags.data) {
|
|
107
114
|
// If --data is provided, treat as raw GraphQL query
|
|
108
115
|
try {
|
|
109
116
|
const parsed = JSON.parse(flags.data);
|
|
110
117
|
query = parsed.query || flags.data;
|
|
118
|
+
dataVariables = parsed.variables;
|
|
111
119
|
} catch {
|
|
112
120
|
query = flags.data;
|
|
113
121
|
}
|
|
@@ -116,8 +124,9 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
116
124
|
query = buildGraphQLQuery(op, spec.types);
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
// Variables
|
|
120
|
-
const
|
|
127
|
+
// Variables: --data variables merged with --var overrides
|
|
128
|
+
const varOverrides = parseKV(flags.var);
|
|
129
|
+
const variables = { ...dataVariables, ...varOverrides };
|
|
121
130
|
|
|
122
131
|
const headers = {
|
|
123
132
|
"Content-Type": "application/json",
|
package/src/commands/list.js
CHANGED
|
@@ -8,28 +8,53 @@ export async function listOperations(args) {
|
|
|
8
8
|
|
|
9
9
|
const opts = parseArgs(args);
|
|
10
10
|
const filter = opts.flags.filter?.toLowerCase();
|
|
11
|
+
const compact = opts.flags.compact !== "false"; // compact by default
|
|
12
|
+
const limit = parseInt(opts.flags.limit) || 0;
|
|
13
|
+
const offset = parseInt(opts.flags.offset) || 0;
|
|
14
|
+
const tag = opts.flags.tag?.toLowerCase();
|
|
11
15
|
|
|
12
16
|
let operations;
|
|
13
17
|
|
|
14
18
|
if (spec.type === "openapi") {
|
|
15
|
-
operations = spec.operations.map((op) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
operations = spec.operations.map((op) =>
|
|
20
|
+
compact
|
|
21
|
+
? { id: op.id, method: op.method, path: op.path }
|
|
22
|
+
: {
|
|
23
|
+
id: op.id,
|
|
24
|
+
method: op.method,
|
|
25
|
+
path: op.path,
|
|
26
|
+
summary: op.summary,
|
|
27
|
+
tags: op.tags,
|
|
28
|
+
deprecated: op.deprecated,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Filter by tag
|
|
33
|
+
if (tag) {
|
|
34
|
+
const fullOps = spec.operations;
|
|
35
|
+
operations = operations.filter((_, i) =>
|
|
36
|
+
fullOps[i].tags?.some((t) => t.toLowerCase().includes(tag))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
23
39
|
} else {
|
|
24
40
|
// graphql
|
|
25
|
-
operations = spec.operations.map((op) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
operations = spec.operations.map((op) =>
|
|
42
|
+
compact
|
|
43
|
+
? { id: op.name, kind: op.kind }
|
|
44
|
+
: {
|
|
45
|
+
id: op.name,
|
|
46
|
+
kind: op.kind,
|
|
47
|
+
description: op.description,
|
|
48
|
+
args: op.args.map((a) => a.name),
|
|
49
|
+
returnType: op.returnType,
|
|
50
|
+
isDeprecated: op.isDeprecated,
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Filter by kind (query/mutation/subscription)
|
|
55
|
+
if (tag) {
|
|
56
|
+
operations = operations.filter((op) => op.kind === tag);
|
|
57
|
+
}
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
if (filter) {
|
|
@@ -39,9 +64,21 @@ export async function listOperations(args) {
|
|
|
39
64
|
});
|
|
40
65
|
}
|
|
41
66
|
|
|
67
|
+
const total = operations.length;
|
|
68
|
+
|
|
69
|
+
// Pagination
|
|
70
|
+
if (offset > 0) {
|
|
71
|
+
operations = operations.slice(offset);
|
|
72
|
+
}
|
|
73
|
+
if (limit > 0) {
|
|
74
|
+
operations = operations.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
out({
|
|
43
78
|
type: spec.type,
|
|
44
|
-
|
|
79
|
+
total,
|
|
80
|
+
showing: operations.length,
|
|
81
|
+
offset: offset || 0,
|
|
45
82
|
operations,
|
|
46
83
|
});
|
|
47
84
|
}
|
package/src/commands/show.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getSpec } from "../store.js";
|
|
2
2
|
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs } from "../args.js";
|
|
3
4
|
|
|
4
5
|
export async function showOperation(args) {
|
|
5
|
-
const
|
|
6
|
+
const { positional } = parseArgs(args);
|
|
7
|
+
const target = positional[0];
|
|
6
8
|
if (!target) throw new Error("Usage: spec show <operationId-or-path>");
|
|
7
9
|
|
|
8
10
|
const spec = getSpec();
|
|
@@ -18,7 +20,6 @@ export async function showOperation(args) {
|
|
|
18
20
|
function showOpenAPI(spec, target) {
|
|
19
21
|
const lower = target.toLowerCase();
|
|
20
22
|
|
|
21
|
-
// Match by operationId, path, or "METHOD path"
|
|
22
23
|
const op = spec.operations.find((o) => {
|
|
23
24
|
return (
|
|
24
25
|
o.id.toLowerCase() === lower ||
|
|
@@ -33,15 +34,29 @@ function showOpenAPI(spec, target) {
|
|
|
33
34
|
|
|
34
35
|
const root = spec.raw || spec.components;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
out({
|
|
38
|
+
id: op.id,
|
|
39
|
+
method: op.method,
|
|
40
|
+
path: op.path,
|
|
41
|
+
summary: op.summary,
|
|
42
|
+
description: op.description,
|
|
43
|
+
tags: op.tags,
|
|
44
|
+
deprecated: op.deprecated,
|
|
45
|
+
parameters: op.parameters.map((p) => {
|
|
46
|
+
const resolved = resolveRef(p, root);
|
|
47
|
+
return {
|
|
48
|
+
name: resolved.name,
|
|
49
|
+
in: resolved.in,
|
|
50
|
+
required: resolved.required || false,
|
|
51
|
+
type: resolved.schema?.type || null,
|
|
52
|
+
format: resolved.schema?.format || undefined,
|
|
53
|
+
description: resolved.description || undefined,
|
|
54
|
+
enum: resolved.schema?.enum || undefined,
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
40
57
|
requestBody: op.requestBody ? resolveRequestBody(op.requestBody, root) : null,
|
|
41
|
-
responses:
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
out(resolved);
|
|
58
|
+
responses: resolveResponsesCompact(op.responses, root),
|
|
59
|
+
});
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
function showGraphQL(spec, target) {
|
|
@@ -53,15 +68,27 @@ function showGraphQL(spec, target) {
|
|
|
53
68
|
throw new Error(`Operation not found: ${target}. Run 'spec list' to see available operations.`);
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
// Also find related types
|
|
57
71
|
const relatedTypes = findRelatedTypes(op, spec.types);
|
|
58
72
|
|
|
59
73
|
out({
|
|
60
|
-
|
|
74
|
+
name: op.name,
|
|
75
|
+
kind: op.kind,
|
|
76
|
+
description: op.description,
|
|
77
|
+
returnType: op.returnType,
|
|
78
|
+
isDeprecated: op.isDeprecated,
|
|
79
|
+
args: op.args?.map((a) => ({
|
|
80
|
+
name: a.name,
|
|
81
|
+
type: flattenType(a.type),
|
|
82
|
+
required: a.type?.kind === "NON_NULL",
|
|
83
|
+
description: a.description || undefined,
|
|
84
|
+
defaultValue: a.defaultValue || undefined,
|
|
85
|
+
})),
|
|
61
86
|
relatedTypes,
|
|
62
87
|
});
|
|
63
88
|
}
|
|
64
89
|
|
|
90
|
+
// --- Helpers ---
|
|
91
|
+
|
|
65
92
|
function resolveRef(obj, root) {
|
|
66
93
|
if (!obj || typeof obj !== "object") return obj;
|
|
67
94
|
if (obj.$ref) {
|
|
@@ -79,55 +106,81 @@ function resolveRequestBody(body, root) {
|
|
|
79
106
|
if (!body) return null;
|
|
80
107
|
const resolved = resolveRef(body, root);
|
|
81
108
|
if (resolved?.content) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
// Only show application/json if available (most useful for agents)
|
|
110
|
+
const jsonContent = resolved.content["application/json"];
|
|
111
|
+
if (jsonContent) {
|
|
112
|
+
return {
|
|
113
|
+
description: resolved.description || undefined,
|
|
114
|
+
required: resolved.required || undefined,
|
|
115
|
+
schema: resolveSchema(jsonContent.schema, root),
|
|
87
116
|
};
|
|
88
117
|
}
|
|
89
|
-
|
|
118
|
+
// Fallback: show first content type
|
|
119
|
+
const [mediaType, value] = Object.entries(resolved.content)[0];
|
|
120
|
+
return {
|
|
121
|
+
description: resolved.description || undefined,
|
|
122
|
+
required: resolved.required || undefined,
|
|
123
|
+
mediaType,
|
|
124
|
+
schema: resolveSchema(value.schema, root),
|
|
125
|
+
};
|
|
90
126
|
}
|
|
91
127
|
return resolved;
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
// Only show success response schema — agent doesn't need error schemas to make a call
|
|
131
|
+
function resolveResponsesCompact(responses, root) {
|
|
132
|
+
if (!responses) return null;
|
|
96
133
|
const result = {};
|
|
97
134
|
for (const [code, resp] of Object.entries(responses)) {
|
|
98
135
|
const resolved = resolveRef(resp, root);
|
|
99
136
|
if (resolved?.content) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
result[code].content[mediaType] = {
|
|
106
|
-
...value,
|
|
107
|
-
schema: resolveSchema(value.schema, root),
|
|
137
|
+
const jsonContent = resolved.content["application/json"];
|
|
138
|
+
if (jsonContent) {
|
|
139
|
+
result[code] = {
|
|
140
|
+
description: resolved.description,
|
|
141
|
+
schema: resolveSchema(jsonContent.schema, root),
|
|
108
142
|
};
|
|
143
|
+
} else {
|
|
144
|
+
result[code] = { description: resolved.description };
|
|
109
145
|
}
|
|
110
146
|
} else {
|
|
111
|
-
result[code] = resolved;
|
|
147
|
+
result[code] = { description: resolved.description };
|
|
112
148
|
}
|
|
113
149
|
}
|
|
114
150
|
return result;
|
|
115
151
|
}
|
|
116
152
|
|
|
117
153
|
function resolveSchema(schema, root, depth = 0) {
|
|
118
|
-
if (!schema || depth >
|
|
154
|
+
if (!schema || depth > 3) return schema;
|
|
119
155
|
if (schema.$ref) {
|
|
120
|
-
|
|
156
|
+
const resolved = resolveRef(schema, root);
|
|
157
|
+
// Resolve one more level for the top-level ref
|
|
158
|
+
return resolveSchema(resolved, root, depth + 1);
|
|
121
159
|
}
|
|
122
160
|
if (schema.properties) {
|
|
123
|
-
const result = {
|
|
161
|
+
const result = { type: schema.type, required: schema.required };
|
|
162
|
+
result.properties = {};
|
|
124
163
|
for (const [key, val] of Object.entries(schema.properties)) {
|
|
125
|
-
|
|
164
|
+
if (val.$ref) {
|
|
165
|
+
const refName = val.$ref.split("/").pop();
|
|
166
|
+
result.properties[key] = { $ref: refName };
|
|
167
|
+
} else if (val.type === "array" && val.items?.$ref) {
|
|
168
|
+
result.properties[key] = { type: "array", items: val.items.$ref.split("/").pop() };
|
|
169
|
+
} else {
|
|
170
|
+
const prop = { type: val.type };
|
|
171
|
+
if (val.format) prop.format = val.format;
|
|
172
|
+
if (val.enum) prop.enum = val.enum;
|
|
173
|
+
if (val.description) prop.description = val.description;
|
|
174
|
+
result.properties[key] = prop;
|
|
175
|
+
}
|
|
126
176
|
}
|
|
127
177
|
return result;
|
|
128
178
|
}
|
|
129
179
|
if (schema.items) {
|
|
130
|
-
|
|
180
|
+
if (schema.items.$ref) {
|
|
181
|
+
return { type: "array", items: schema.items.$ref.split("/").pop() };
|
|
182
|
+
}
|
|
183
|
+
return { type: "array", items: resolveSchema(schema.items, root, depth + 1) };
|
|
131
184
|
}
|
|
132
185
|
return schema;
|
|
133
186
|
}
|
|
@@ -135,7 +188,6 @@ function resolveSchema(schema, root, depth = 0) {
|
|
|
135
188
|
function findRelatedTypes(op, types) {
|
|
136
189
|
const names = new Set();
|
|
137
190
|
|
|
138
|
-
// Collect type names from args and return type
|
|
139
191
|
function extractTypeNames(typeStr) {
|
|
140
192
|
if (!typeStr) return;
|
|
141
193
|
const cleaned = typeStr.replace(/[[\]!]/g, "");
|
|
@@ -147,20 +199,28 @@ function findRelatedTypes(op, types) {
|
|
|
147
199
|
extractTypeNames(flattenType(arg.type));
|
|
148
200
|
}
|
|
149
201
|
|
|
150
|
-
// Filter out built-in scalar types
|
|
151
202
|
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
152
203
|
return types
|
|
153
204
|
.filter((t) => names.has(t.name) && !scalars.has(t.name))
|
|
154
|
-
.map((t) =>
|
|
155
|
-
name: t.name,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
.map((t) => {
|
|
206
|
+
const result = { name: t.name, kind: t.kind };
|
|
207
|
+
if (t.fields) {
|
|
208
|
+
result.fields = t.fields.map((f) => ({
|
|
209
|
+
name: f.name,
|
|
210
|
+
type: flattenType(f.type),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
if (t.inputFields) {
|
|
214
|
+
result.inputFields = t.inputFields.map((f) => ({
|
|
215
|
+
name: f.name,
|
|
216
|
+
type: flattenType(f.type),
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
if (t.enumValues) {
|
|
220
|
+
result.enumValues = t.enumValues.map((e) => e.name);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
});
|
|
164
224
|
}
|
|
165
225
|
|
|
166
226
|
function flattenType(t) {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { getSpec } from "../store.js";
|
|
2
|
+
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs } from "../args.js";
|
|
4
|
+
|
|
5
|
+
export async function typesCmd(args) {
|
|
6
|
+
const spec = getSpec();
|
|
7
|
+
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
8
|
+
|
|
9
|
+
const { positional, flags } = parseArgs(args);
|
|
10
|
+
const target = positional[0];
|
|
11
|
+
|
|
12
|
+
if (spec.type === "openapi") {
|
|
13
|
+
showOpenAPISchema(spec, target, flags);
|
|
14
|
+
} else {
|
|
15
|
+
showGraphQLType(spec, target, flags);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function showOpenAPISchema(spec, target, flags) {
|
|
20
|
+
const schemas = spec.raw?.components?.schemas || spec.raw?.definitions || {};
|
|
21
|
+
|
|
22
|
+
if (!target) {
|
|
23
|
+
// List all schema names — just names, very compact
|
|
24
|
+
const names = Object.keys(schemas);
|
|
25
|
+
out({
|
|
26
|
+
type: "openapi",
|
|
27
|
+
count: names.length,
|
|
28
|
+
schemas: names,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Find schema (case-insensitive)
|
|
34
|
+
const lower = target.toLowerCase();
|
|
35
|
+
const key = Object.keys(schemas).find((k) => k.toLowerCase() === lower);
|
|
36
|
+
|
|
37
|
+
if (!key) {
|
|
38
|
+
throw new Error(`Schema not found: ${target}. Run 'spec types' to list available schemas.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const schema = schemas[key];
|
|
42
|
+
const root = spec.raw;
|
|
43
|
+
|
|
44
|
+
// Resolve one level deep — don't recursively explode nested schemas
|
|
45
|
+
const resolved = resolveSchemaCompact(schema, root);
|
|
46
|
+
|
|
47
|
+
out({
|
|
48
|
+
name: key,
|
|
49
|
+
...resolved,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function showGraphQLType(spec, target, flags) {
|
|
54
|
+
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
55
|
+
const userTypes = spec.types?.filter((t) => !t.name.startsWith("__") && !scalars.has(t.name)) || [];
|
|
56
|
+
|
|
57
|
+
if (!target) {
|
|
58
|
+
// List type names grouped by kind — compact
|
|
59
|
+
const grouped = {};
|
|
60
|
+
for (const t of userTypes) {
|
|
61
|
+
if (!grouped[t.kind]) grouped[t.kind] = [];
|
|
62
|
+
grouped[t.kind].push(t.name);
|
|
63
|
+
}
|
|
64
|
+
out({
|
|
65
|
+
type: "graphql",
|
|
66
|
+
count: userTypes.length,
|
|
67
|
+
types: grouped,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Find specific type
|
|
73
|
+
const lower = target.toLowerCase();
|
|
74
|
+
const type = userTypes.find((t) => t.name.toLowerCase() === lower);
|
|
75
|
+
|
|
76
|
+
if (!type) {
|
|
77
|
+
throw new Error(`Type not found: ${target}. Run 'spec types' to list available types.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = {
|
|
81
|
+
name: type.name,
|
|
82
|
+
kind: type.kind,
|
|
83
|
+
description: type.description || null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (type.fields) {
|
|
87
|
+
result.fields = type.fields.map((f) => ({
|
|
88
|
+
name: f.name,
|
|
89
|
+
type: flattenType(f.type),
|
|
90
|
+
args: f.args?.length > 0 ? f.args.map((a) => ({ name: a.name, type: flattenType(a.type) })) : undefined,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type.inputFields) {
|
|
95
|
+
result.inputFields = type.inputFields.map((f) => ({
|
|
96
|
+
name: f.name,
|
|
97
|
+
type: flattenType(f.type),
|
|
98
|
+
defaultValue: f.defaultValue || undefined,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (type.enumValues) {
|
|
103
|
+
result.enumValues = type.enumValues.map((e) => e.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
out(result);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveSchemaCompact(schema, root) {
|
|
110
|
+
if (!schema) return schema;
|
|
111
|
+
|
|
112
|
+
if (schema.$ref) {
|
|
113
|
+
const path = schema.$ref.replace("#/", "").split("/");
|
|
114
|
+
let resolved = root;
|
|
115
|
+
for (const p of path) resolved = resolved?.[p];
|
|
116
|
+
return resolveSchemaCompact(resolved, root);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {};
|
|
120
|
+
if (schema.type) result.type = schema.type;
|
|
121
|
+
if (schema.description) result.description = schema.description;
|
|
122
|
+
if (schema.required) result.required = schema.required;
|
|
123
|
+
if (schema.enum) result.enum = schema.enum;
|
|
124
|
+
|
|
125
|
+
if (schema.properties) {
|
|
126
|
+
result.properties = {};
|
|
127
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
128
|
+
if (val.$ref) {
|
|
129
|
+
// Just show the type name, don't resolve
|
|
130
|
+
const refName = val.$ref.split("/").pop();
|
|
131
|
+
result.properties[key] = { $ref: refName };
|
|
132
|
+
} else if (val.type === "array" && val.items?.$ref) {
|
|
133
|
+
const refName = val.items.$ref.split("/").pop();
|
|
134
|
+
result.properties[key] = { type: "array", items: refName };
|
|
135
|
+
} else {
|
|
136
|
+
result.properties[key] = { type: val.type || null };
|
|
137
|
+
if (val.enum) result.properties[key].enum = val.enum;
|
|
138
|
+
if (val.format) result.properties[key].format = val.format;
|
|
139
|
+
if (val.description) result.properties[key].description = val.description;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schema.items) {
|
|
145
|
+
if (schema.items.$ref) {
|
|
146
|
+
result.items = schema.items.$ref.split("/").pop();
|
|
147
|
+
} else {
|
|
148
|
+
result.items = { type: schema.items.type };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function flattenType(t) {
|
|
156
|
+
if (!t) return null;
|
|
157
|
+
if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
|
|
158
|
+
if (t.ofType) {
|
|
159
|
+
const inner = flattenType(t.ofType);
|
|
160
|
+
if (t.kind === "LIST") return `[${inner}]`;
|
|
161
|
+
if (t.kind === "NON_NULL") return `${inner}!`;
|
|
162
|
+
return inner;
|
|
163
|
+
}
|
|
164
|
+
return t.kind;
|
|
165
|
+
}
|