api-spec-cli 0.0.1 → 0.0.2
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 +191 -0
- package/bin/spec.js +5 -0
- package/package.json +32 -8
- package/src/args.js +46 -0
- package/src/cli.js +79 -0
- package/src/commands/call.js +198 -0
- package/src/commands/config.js +75 -0
- package/src/commands/list.js +47 -0
- package/src/commands/load.js +230 -0
- package/src/commands/show.js +176 -0
- package/src/commands/validate.js +269 -0
- package/src/output.js +61 -0
- package/src/store.js +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# spec-cli
|
|
2
|
+
|
|
3
|
+
Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed for AI coding agents — all output is structured JSON by default, with optional text and YAML formats.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun link
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or run directly:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun bin/spec.js <command>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Load an OpenAPI spec
|
|
22
|
+
spec load https://petstore3.swagger.io/api/v3/openapi.json
|
|
23
|
+
|
|
24
|
+
# Load a GraphQL endpoint (introspection)
|
|
25
|
+
spec load https://gql.hashnode.com
|
|
26
|
+
|
|
27
|
+
# List operations
|
|
28
|
+
spec list
|
|
29
|
+
spec list --filter pets
|
|
30
|
+
|
|
31
|
+
# Show operation details
|
|
32
|
+
spec show getPetById
|
|
33
|
+
spec show publishPost
|
|
34
|
+
|
|
35
|
+
# Call an endpoint
|
|
36
|
+
spec config set baseUrl https://petstore3.swagger.io/api/v3
|
|
37
|
+
spec call findPetsByStatus --query status=available
|
|
38
|
+
|
|
39
|
+
# GraphQL with auth
|
|
40
|
+
spec config set auth YOUR_TOKEN
|
|
41
|
+
spec call me
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### `spec load <file-or-url>`
|
|
47
|
+
|
|
48
|
+
Load an API spec from a local file (JSON/YAML) or URL.
|
|
49
|
+
|
|
50
|
+
- **OpenAPI/Swagger**: Detects JSON or YAML, supports v2 and v3
|
|
51
|
+
- **GraphQL**: Runs introspection query on the endpoint
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
spec load ./openapi.yaml
|
|
55
|
+
spec load https://api.example.com/openapi.json
|
|
56
|
+
spec load https://gql.example.com/graphql
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `spec list [--filter <text>]`
|
|
60
|
+
|
|
61
|
+
List all operations in the loaded spec.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
spec list # all operations
|
|
65
|
+
spec list --filter user # filter by keyword
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**OpenAPI output**: `id`, `method`, `path`, `summary`, `tags`, `deprecated`
|
|
69
|
+
**GraphQL output**: `id`, `kind` (query/mutation/subscription), `description`, `args`, `returnType`
|
|
70
|
+
|
|
71
|
+
### `spec show <operation>`
|
|
72
|
+
|
|
73
|
+
Show full details of an operation.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
spec show getPetById # by operationId
|
|
77
|
+
spec show /pet/{petId} # by path
|
|
78
|
+
spec show "GET /pet/{petId}" # by method + path
|
|
79
|
+
spec show publishPost # GraphQL operation name
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For OpenAPI: resolves `$ref` references in parameters, request body, and responses.
|
|
83
|
+
For GraphQL: includes related types with field definitions.
|
|
84
|
+
|
|
85
|
+
### `spec call <operation> [options]`
|
|
86
|
+
|
|
87
|
+
Execute an API request.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# OpenAPI
|
|
91
|
+
spec call addPet --data '{"name":"Rex","photoUrls":[]}'
|
|
92
|
+
spec call getPetById --var petId=1
|
|
93
|
+
spec call findPetsByStatus --query status=available
|
|
94
|
+
spec call updatePet --method PUT --data '{"id":1,"name":"Rex"}'
|
|
95
|
+
|
|
96
|
+
# GraphQL
|
|
97
|
+
spec call me
|
|
98
|
+
spec call publication --var host=blog.example.com
|
|
99
|
+
spec call publishPost --data '{"query":"mutation { ... }"}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Options:**
|
|
103
|
+
| Flag | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `--data '{"key":"val"}'` | Request body (JSON) |
|
|
106
|
+
| `--query key=val` | Query parameter (repeatable) |
|
|
107
|
+
| `--header key=val` | Per-request header (repeatable) |
|
|
108
|
+
| `--var key=val` | Path variable or GraphQL variable (repeatable) |
|
|
109
|
+
| `--method GET\|POST\|...` | Override HTTP method |
|
|
110
|
+
|
|
111
|
+
For GraphQL calls without `--data`, the CLI auto-generates a query from the operation's schema, selecting scalar fields from the return type.
|
|
112
|
+
|
|
113
|
+
### `spec validate <file-or-url>`
|
|
114
|
+
|
|
115
|
+
Validate an OpenAPI spec and report errors and warnings.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
spec validate ./openapi.yaml
|
|
119
|
+
spec validate https://api.example.com/openapi.json
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Checks:
|
|
123
|
+
- Required fields (`info`, `info.title`, `info.version`, `paths`)
|
|
124
|
+
- Valid HTTP methods and schema types
|
|
125
|
+
- Unique `operationId` values
|
|
126
|
+
- Broken `$ref` references
|
|
127
|
+
- Array schemas have `items`
|
|
128
|
+
- Path parameters are declared
|
|
129
|
+
- Server/host definitions
|
|
130
|
+
- Unusual patterns (request body on GET)
|
|
131
|
+
|
|
132
|
+
### `spec config`
|
|
133
|
+
|
|
134
|
+
Manage persistent configuration stored in `.spec-cli/config.json`.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
spec config get # show all config
|
|
138
|
+
spec config get baseUrl # show single key
|
|
139
|
+
spec config set baseUrl https://api.example.com
|
|
140
|
+
spec config set auth my-api-token # adds Bearer prefix
|
|
141
|
+
spec config set auth "Bearer my-token" # explicit Bearer
|
|
142
|
+
spec config set auth "Basic dXNlcjpwYXNz" # Basic auth
|
|
143
|
+
spec config set headers.X-API-Key abc123 # custom header (dot notation)
|
|
144
|
+
spec config unset headers.X-API-Key # remove a key
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Output Formats
|
|
148
|
+
|
|
149
|
+
All commands support `--format`:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
spec list --format json # JSON (default)
|
|
153
|
+
spec list --format text # human-readable
|
|
154
|
+
spec list --format yaml # YAML
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Errors always output as JSON to stderr for reliable agent parsing.
|
|
158
|
+
|
|
159
|
+
## GraphQL Coverage
|
|
160
|
+
|
|
161
|
+
Full GraphQL schema support via introspection:
|
|
162
|
+
- **Queries** — all root query fields
|
|
163
|
+
- **Mutations** — all root mutation fields
|
|
164
|
+
- **Subscriptions** — all root subscription fields
|
|
165
|
+
- **Types** — input objects, enums, scalars, object types with fields
|
|
166
|
+
- **Args** — full argument definitions with types and defaults
|
|
167
|
+
|
|
168
|
+
## For AI Agents
|
|
169
|
+
|
|
170
|
+
This CLI is designed to be used by AI coding agents (Claude, GPT, etc.) as an MCP tool or shell command:
|
|
171
|
+
|
|
172
|
+
1. **Structured output** — JSON by default, every field is predictable
|
|
173
|
+
2. **Error format** — `{"error": "message"}` on stderr, non-zero exit code
|
|
174
|
+
3. **Discoverable** — `list` and `show` let agents explore APIs without docs
|
|
175
|
+
4. **Auto-query building** — GraphQL calls auto-generate queries from the schema
|
|
176
|
+
5. **Persistent config** — set auth once, use across calls
|
|
177
|
+
6. **No interactive prompts** — everything is flags and args
|
|
178
|
+
|
|
179
|
+
## Storage
|
|
180
|
+
|
|
181
|
+
All state is stored in `.spec-cli/` in the current directory:
|
|
182
|
+
- `spec.json` — cached loaded spec
|
|
183
|
+
- `config.json` — base URL, headers, auth
|
|
184
|
+
|
|
185
|
+
Add `.spec-cli/` to your `.gitignore`.
|
|
186
|
+
|
|
187
|
+
## Dependencies
|
|
188
|
+
|
|
189
|
+
- [yaml](https://www.npmjs.com/package/yaml) — YAML parsing (for OpenAPI YAML specs and YAML output)
|
|
190
|
+
|
|
191
|
+
No other runtime dependencies. Uses Bun's built-in `fetch` for HTTP.
|
package/bin/spec.js
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-spec-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spec": "./bin/spec.js"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
|
-
"test": "
|
|
10
|
+
"test": "bun test"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"openapi",
|
|
19
|
+
"graphql",
|
|
20
|
+
"cli",
|
|
21
|
+
"agent",
|
|
22
|
+
"api",
|
|
23
|
+
"swagger",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"mcp",
|
|
26
|
+
"introspection"
|
|
27
|
+
],
|
|
28
|
+
"author": "niradler55",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/niradler/api-spec-cli.git"
|
|
8
33
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"type": "commonjs"
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"yaml": "^2.7.0"
|
|
36
|
+
}
|
|
13
37
|
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Simple arg parser — no deps needed.
|
|
2
|
+
// Supports: --flag value, --flag=value, and positional args
|
|
3
|
+
// Repeatable flags (--query, --header, --var) are collected into arrays.
|
|
4
|
+
|
|
5
|
+
const REPEATABLE = new Set(["query", "header", "var"]);
|
|
6
|
+
|
|
7
|
+
export function parseArgs(args) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const positional = [];
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg.startsWith("--")) {
|
|
14
|
+
let key, value;
|
|
15
|
+
if (arg.includes("=")) {
|
|
16
|
+
[key, ...value] = arg.slice(2).split("=");
|
|
17
|
+
value = value.join("=");
|
|
18
|
+
} else {
|
|
19
|
+
key = arg.slice(2);
|
|
20
|
+
value = args[++i];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (REPEATABLE.has(key)) {
|
|
24
|
+
if (!flags[key]) flags[key] = [];
|
|
25
|
+
flags[key].push(value);
|
|
26
|
+
} else {
|
|
27
|
+
flags[key] = value;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(arg);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { flags, positional };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse key=value pairs from an array of strings
|
|
38
|
+
export function parseKV(pairs) {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const pair of pairs || []) {
|
|
41
|
+
const idx = pair.indexOf("=");
|
|
42
|
+
if (idx === -1) throw new Error(`Invalid key=value: ${pair}`);
|
|
43
|
+
result[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { loadSpec } from "./commands/load.js";
|
|
2
|
+
import { listOperations } from "./commands/list.js";
|
|
3
|
+
import { showOperation } from "./commands/show.js";
|
|
4
|
+
import { callOperation } from "./commands/call.js";
|
|
5
|
+
import { configCmd } from "./commands/config.js";
|
|
6
|
+
import { validateSpec } from "./commands/validate.js";
|
|
7
|
+
import { out, err, setFormat } from "./output.js";
|
|
8
|
+
|
|
9
|
+
const HELP = `spec-cli — Agent-friendly API spec explorer
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
spec load <file-or-url> Load an OpenAPI (JSON/YAML) or GraphQL endpoint
|
|
13
|
+
spec list [--filter <text>] List all operations
|
|
14
|
+
spec show <operation> Show operation details (params, body, response)
|
|
15
|
+
spec call <operation> [options] Execute a request
|
|
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
|
|
20
|
+
|
|
21
|
+
Call options:
|
|
22
|
+
--data '{"key":"val"}' Request body (JSON)
|
|
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
|
|
27
|
+
|
|
28
|
+
Output format (all commands):
|
|
29
|
+
--format json JSON (default, best for agents)
|
|
30
|
+
--format text Human-readable text
|
|
31
|
+
--format yaml YAML output
|
|
32
|
+
|
|
33
|
+
All output defaults to JSON for agent consumption.`;
|
|
34
|
+
|
|
35
|
+
export async function run(args) {
|
|
36
|
+
// Extract --format before routing
|
|
37
|
+
const formatIdx = args.indexOf("--format");
|
|
38
|
+
if (formatIdx !== -1) {
|
|
39
|
+
setFormat(args[formatIdx + 1]);
|
|
40
|
+
args = [...args.slice(0, formatIdx), ...args.slice(formatIdx + 2)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cmd = args[0];
|
|
44
|
+
|
|
45
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
46
|
+
out({ help: HELP });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
switch (cmd) {
|
|
52
|
+
case "load":
|
|
53
|
+
await loadSpec(args.slice(1));
|
|
54
|
+
break;
|
|
55
|
+
case "list":
|
|
56
|
+
case "ls":
|
|
57
|
+
await listOperations(args.slice(1));
|
|
58
|
+
break;
|
|
59
|
+
case "show":
|
|
60
|
+
await showOperation(args.slice(1));
|
|
61
|
+
break;
|
|
62
|
+
case "call":
|
|
63
|
+
await callOperation(args.slice(1));
|
|
64
|
+
break;
|
|
65
|
+
case "validate":
|
|
66
|
+
await validateSpec(args.slice(1));
|
|
67
|
+
break;
|
|
68
|
+
case "config":
|
|
69
|
+
case "cfg":
|
|
70
|
+
await configCmd(args.slice(1));
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
err(`Unknown command: ${cmd}. Run 'spec help' for usage.`);
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
err(e.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { getSpec, getConfig } from "../store.js";
|
|
2
|
+
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs, parseKV } from "../args.js";
|
|
4
|
+
|
|
5
|
+
export async function callOperation(args) {
|
|
6
|
+
const { flags, positional } = parseArgs(args);
|
|
7
|
+
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
|
+
|
|
10
|
+
const spec = getSpec();
|
|
11
|
+
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
12
|
+
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
|
|
15
|
+
if (spec.type === "openapi") {
|
|
16
|
+
await callOpenAPI(spec, config, target, flags);
|
|
17
|
+
} else {
|
|
18
|
+
await callGraphQL(spec, config, target, flags);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function callOpenAPI(spec, config, target, flags) {
|
|
23
|
+
const lower = target.toLowerCase();
|
|
24
|
+
|
|
25
|
+
const op = spec.operations.find((o) => {
|
|
26
|
+
return (
|
|
27
|
+
o.id.toLowerCase() === lower ||
|
|
28
|
+
o.path.toLowerCase() === lower ||
|
|
29
|
+
`${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!op) throw new Error(`Operation not found: ${target}`);
|
|
34
|
+
|
|
35
|
+
// Build URL
|
|
36
|
+
const baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
|
|
37
|
+
let path = op.path;
|
|
38
|
+
|
|
39
|
+
// Substitute path variables
|
|
40
|
+
const vars = parseKV(flags.var);
|
|
41
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
42
|
+
path = path.replace(`{${key}}`, encodeURIComponent(val));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Query params
|
|
46
|
+
const queryParams = parseKV(flags.query);
|
|
47
|
+
const qs = new URLSearchParams(queryParams).toString();
|
|
48
|
+
const url = `${baseUrl}${path}${qs ? "?" + qs : ""}`;
|
|
49
|
+
|
|
50
|
+
// Method
|
|
51
|
+
const method = (flags.method || op.method).toUpperCase();
|
|
52
|
+
|
|
53
|
+
// Headers
|
|
54
|
+
const headers = {
|
|
55
|
+
...config.headers,
|
|
56
|
+
...parseKV(flags.header),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Auth
|
|
60
|
+
if (config.auth) {
|
|
61
|
+
if (config.auth.startsWith("Bearer ") || config.auth.startsWith("Basic ")) {
|
|
62
|
+
headers["Authorization"] = config.auth;
|
|
63
|
+
} else {
|
|
64
|
+
headers["Authorization"] = `Bearer ${config.auth}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Body
|
|
69
|
+
let body = undefined;
|
|
70
|
+
if (flags.data) {
|
|
71
|
+
body = flags.data;
|
|
72
|
+
if (!headers["Content-Type"]) {
|
|
73
|
+
headers["Content-Type"] = "application/json";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const res = await fetch(url, { method, headers, body });
|
|
78
|
+
const contentType = res.headers.get("content-type") || "";
|
|
79
|
+
let responseBody;
|
|
80
|
+
|
|
81
|
+
if (contentType.includes("json")) {
|
|
82
|
+
responseBody = await res.json();
|
|
83
|
+
} else {
|
|
84
|
+
responseBody = await res.text();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
out({
|
|
88
|
+
status: res.status,
|
|
89
|
+
statusText: res.statusText,
|
|
90
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
91
|
+
body: responseBody,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function callGraphQL(spec, config, target, flags) {
|
|
96
|
+
const lower = target.toLowerCase();
|
|
97
|
+
|
|
98
|
+
const op = spec.operations.find((o) => o.name.toLowerCase() === lower);
|
|
99
|
+
if (!op) throw new Error(`Operation not found: ${target}`);
|
|
100
|
+
|
|
101
|
+
const endpoint = spec.endpoint;
|
|
102
|
+
if (!endpoint) throw new Error("No GraphQL endpoint set");
|
|
103
|
+
|
|
104
|
+
// Build query from operation
|
|
105
|
+
let query;
|
|
106
|
+
if (flags.data) {
|
|
107
|
+
// If --data is provided, treat as raw GraphQL query
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(flags.data);
|
|
110
|
+
query = parsed.query || flags.data;
|
|
111
|
+
} catch {
|
|
112
|
+
query = flags.data;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Auto-build a simple query/mutation
|
|
116
|
+
query = buildGraphQLQuery(op, spec.types);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Variables from --var flags
|
|
120
|
+
const variables = parseKV(flags.var);
|
|
121
|
+
|
|
122
|
+
const headers = {
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
...config.headers,
|
|
125
|
+
...parseKV(flags.header),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (config.auth) {
|
|
129
|
+
if (config.auth.startsWith("Bearer ") || config.auth.startsWith("Basic ")) {
|
|
130
|
+
headers["Authorization"] = config.auth;
|
|
131
|
+
} else {
|
|
132
|
+
headers["Authorization"] = `Bearer ${config.auth}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const body = JSON.stringify({
|
|
137
|
+
query,
|
|
138
|
+
variables: Object.keys(variables).length > 0 ? variables : undefined,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const res = await fetch(config.baseUrl || endpoint, { method: "POST", headers, body });
|
|
142
|
+
const responseBody = await res.json();
|
|
143
|
+
|
|
144
|
+
out({
|
|
145
|
+
status: res.status,
|
|
146
|
+
query,
|
|
147
|
+
variables: Object.keys(variables).length > 0 ? variables : undefined,
|
|
148
|
+
data: responseBody.data || null,
|
|
149
|
+
errors: responseBody.errors || null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildGraphQLQuery(op, types) {
|
|
154
|
+
const args = op.args || [];
|
|
155
|
+
const argsStr = args.length > 0
|
|
156
|
+
? `(${args.map((a) => `$${a.name}: ${flattenType(a.type)}`).join(", ")})`
|
|
157
|
+
: "";
|
|
158
|
+
|
|
159
|
+
const passArgs = args.length > 0
|
|
160
|
+
? `(${args.map((a) => `${a.name}: $${a.name}`).join(", ")})`
|
|
161
|
+
: "";
|
|
162
|
+
|
|
163
|
+
// Try to build a field selection from the return type
|
|
164
|
+
const returnTypeName = op.returnType?.replace(/[[\]!]/g, "");
|
|
165
|
+
const returnType = types?.find((t) => t.name === returnTypeName);
|
|
166
|
+
let fields = "";
|
|
167
|
+
|
|
168
|
+
if (returnType?.fields) {
|
|
169
|
+
// Select scalar fields only (1 level deep)
|
|
170
|
+
const scalarFields = returnType.fields
|
|
171
|
+
.filter((f) => {
|
|
172
|
+
const typeName = flattenType(f.type)?.replace(/[[\]!]/g, "");
|
|
173
|
+
const t = types?.find((tt) => tt.name === typeName);
|
|
174
|
+
return !t || t.kind === "SCALAR" || t.kind === "ENUM";
|
|
175
|
+
})
|
|
176
|
+
.map((f) => f.name);
|
|
177
|
+
|
|
178
|
+
if (scalarFields.length > 0) {
|
|
179
|
+
fields = ` { ${scalarFields.join(" ")} }`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const keyword = op.kind === "mutation" ? "mutation" : "query";
|
|
184
|
+
return `${keyword}${argsStr} { ${op.name}${passArgs}${fields} }`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function flattenType(t) {
|
|
188
|
+
if (!t) return null;
|
|
189
|
+
if (typeof t === "string") return t;
|
|
190
|
+
if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
|
|
191
|
+
if (t.ofType) {
|
|
192
|
+
const inner = flattenType(t.ofType);
|
|
193
|
+
if (t.kind === "LIST") return `[${inner}]`;
|
|
194
|
+
if (t.kind === "NON_NULL") return `${inner}!`;
|
|
195
|
+
return inner;
|
|
196
|
+
}
|
|
197
|
+
return t.kind;
|
|
198
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getConfig, setConfig } from "../store.js";
|
|
2
|
+
import { out } from "../output.js";
|
|
3
|
+
|
|
4
|
+
export async function configCmd(args) {
|
|
5
|
+
const sub = args[0];
|
|
6
|
+
|
|
7
|
+
if (!sub || sub === "get" || sub === "show") {
|
|
8
|
+
const key = args[1];
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
if (key) {
|
|
11
|
+
out({ [key]: getNestedValue(config, key) });
|
|
12
|
+
} else {
|
|
13
|
+
out(config);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (sub === "set") {
|
|
19
|
+
const key = args[1];
|
|
20
|
+
const value = args[2];
|
|
21
|
+
if (!key || value === undefined) throw new Error("Usage: spec config set <key> <value>");
|
|
22
|
+
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
setNestedValue(config, key, value);
|
|
25
|
+
setConfig(config);
|
|
26
|
+
out({ ok: true, [key]: value });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (sub === "unset") {
|
|
31
|
+
const key = args[1];
|
|
32
|
+
if (!key) throw new Error("Usage: spec config unset <key>");
|
|
33
|
+
|
|
34
|
+
const config = getConfig();
|
|
35
|
+
deleteNestedValue(config, key);
|
|
36
|
+
setConfig(config);
|
|
37
|
+
out({ ok: true, deleted: key });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Unknown config subcommand: ${sub}. Use: get, set, unset`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Support dotted keys: "headers.Authorization", "headers.X-API-Key"
|
|
45
|
+
function setNestedValue(obj, key, value) {
|
|
46
|
+
const parts = key.split(".");
|
|
47
|
+
let current = obj;
|
|
48
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
49
|
+
if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
|
|
50
|
+
current[parts[i]] = {};
|
|
51
|
+
}
|
|
52
|
+
current = current[parts[i]];
|
|
53
|
+
}
|
|
54
|
+
current[parts[parts.length - 1]] = value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getNestedValue(obj, key) {
|
|
58
|
+
const parts = key.split(".");
|
|
59
|
+
let current = obj;
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
if (current == null) return undefined;
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deleteNestedValue(obj, key) {
|
|
68
|
+
const parts = key.split(".");
|
|
69
|
+
let current = obj;
|
|
70
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
71
|
+
if (current == null) return;
|
|
72
|
+
current = current[parts[i]];
|
|
73
|
+
}
|
|
74
|
+
if (current != null) delete current[parts[parts.length - 1]];
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getSpec } from "../store.js";
|
|
2
|
+
import { out } from "../output.js";
|
|
3
|
+
import { parseArgs } from "../args.js";
|
|
4
|
+
|
|
5
|
+
export async function listOperations(args) {
|
|
6
|
+
const spec = getSpec();
|
|
7
|
+
if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
|
|
8
|
+
|
|
9
|
+
const opts = parseArgs(args);
|
|
10
|
+
const filter = opts.flags.filter?.toLowerCase();
|
|
11
|
+
|
|
12
|
+
let operations;
|
|
13
|
+
|
|
14
|
+
if (spec.type === "openapi") {
|
|
15
|
+
operations = spec.operations.map((op) => ({
|
|
16
|
+
id: op.id,
|
|
17
|
+
method: op.method,
|
|
18
|
+
path: op.path,
|
|
19
|
+
summary: op.summary,
|
|
20
|
+
tags: op.tags,
|
|
21
|
+
deprecated: op.deprecated,
|
|
22
|
+
}));
|
|
23
|
+
} else {
|
|
24
|
+
// graphql
|
|
25
|
+
operations = spec.operations.map((op) => ({
|
|
26
|
+
id: op.name,
|
|
27
|
+
kind: op.kind,
|
|
28
|
+
description: op.description,
|
|
29
|
+
args: op.args.map((a) => a.name),
|
|
30
|
+
returnType: op.returnType,
|
|
31
|
+
isDeprecated: op.isDeprecated,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (filter) {
|
|
36
|
+
operations = operations.filter((op) => {
|
|
37
|
+
const text = JSON.stringify(op).toLowerCase();
|
|
38
|
+
return text.includes(filter);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
out({
|
|
43
|
+
type: spec.type,
|
|
44
|
+
count: operations.length,
|
|
45
|
+
operations,
|
|
46
|
+
});
|
|
47
|
+
}
|