api-spec-cli 0.1.2 → 0.2.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 +129 -73
- package/package.json +1 -1
- package/src/args.js +1 -1
- package/src/cli.js +97 -55
- package/src/commands/add.js +67 -0
- package/src/commands/call.js +22 -71
- package/src/commands/list.js +20 -30
- package/src/commands/load.js +57 -70
- package/src/commands/show.js +33 -68
- package/src/commands/specs.js +69 -0
- package/src/commands/types.js +2 -4
- package/src/mcp-client.js +9 -2
- package/src/registry.js +53 -0
- package/src/resolve.js +65 -0
package/src/commands/call.js
CHANGED
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import { getSpec, getConfig } from "../store.js";
|
|
3
2
|
import { out } from "../output.js";
|
|
4
3
|
import { parseArgs, parseKV } from "../args.js";
|
|
5
4
|
import { createMcpClient } from "../mcp-client.js";
|
|
5
|
+
import { resolveActiveSpec, resolveConfig } from "../resolve.js";
|
|
6
6
|
|
|
7
7
|
export async function callOperation(args) {
|
|
8
8
|
const { flags, positional } = parseArgs(args);
|
|
9
9
|
const target = positional[0];
|
|
10
|
-
if (!target) throw new Error(
|
|
10
|
+
if (!target) throw new Error(
|
|
11
|
+
"Usage: spec call <operation> [--spec <name> | --openapi <url> | ...] [--data '{}'] [--var k=v] [--header k=v]"
|
|
12
|
+
);
|
|
11
13
|
|
|
12
|
-
// Support --data-file to avoid shell escaping issues
|
|
13
14
|
if (flags["data-file"] && !flags.data) {
|
|
14
15
|
flags.data = readFileSync(flags["data-file"], "utf-8").trim();
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const spec =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const config = getConfig();
|
|
18
|
+
const { spec, entry } = await resolveActiveSpec(flags);
|
|
19
|
+
const config = resolveConfig(flags, entry);
|
|
21
20
|
|
|
22
21
|
if (spec.type === "openapi") {
|
|
23
22
|
await callOpenAPI(spec, config, target, flags);
|
|
24
23
|
} else if (spec.type === "mcp") {
|
|
25
|
-
await callMCP(spec, target, flags);
|
|
24
|
+
await callMCP(spec, entry, target, flags);
|
|
26
25
|
} else {
|
|
27
26
|
await callGraphQL(spec, config, target, flags);
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
async function callMCP(spec, target, flags) {
|
|
30
|
+
async function callMCP(spec, entry, target, flags) {
|
|
32
31
|
const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
|
|
33
32
|
if (!tool) throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
|
|
34
33
|
|
|
35
|
-
// Build arguments: --data for full JSON object, --var for individual keys
|
|
36
34
|
let toolArgs = {};
|
|
37
35
|
if (flags.data) {
|
|
38
36
|
try {
|
|
@@ -41,11 +39,11 @@ async function callMCP(spec, target, flags) {
|
|
|
41
39
|
throw new Error("--data must be valid JSON when calling an MCP tool");
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
|
-
// --var key=value overrides / extends --data args
|
|
45
42
|
const varOverrides = parseKV(flags.var);
|
|
46
43
|
toolArgs = { ...toolArgs, ...varOverrides };
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
// Re-connect using the original entry (which holds transport config + headers/env)
|
|
46
|
+
const client = await createMcpClient(entry);
|
|
49
47
|
try {
|
|
50
48
|
const result = await client.callTool({ name: tool.name, arguments: toolArgs });
|
|
51
49
|
out({ tool: tool.name, arguments: toolArgs, result });
|
|
@@ -57,67 +55,38 @@ async function callMCP(spec, target, flags) {
|
|
|
57
55
|
async function callOpenAPI(spec, config, target, flags) {
|
|
58
56
|
const lower = target.toLowerCase();
|
|
59
57
|
|
|
60
|
-
const op = spec.operations.find((o) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
});
|
|
58
|
+
const op = spec.operations.find((o) =>
|
|
59
|
+
o.id.toLowerCase() === lower ||
|
|
60
|
+
o.path.toLowerCase() === lower ||
|
|
61
|
+
`${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
|
|
62
|
+
);
|
|
67
63
|
|
|
68
64
|
if (!op) throw new Error(`Operation not found: ${target}`);
|
|
69
65
|
|
|
70
|
-
// Build URL
|
|
71
66
|
const baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
|
|
72
67
|
let path = op.path;
|
|
73
68
|
|
|
74
|
-
// Substitute path variables
|
|
75
69
|
const vars = parseKV(flags.var);
|
|
76
70
|
for (const [key, val] of Object.entries(vars)) {
|
|
77
71
|
path = path.replace(`{${key}}`, encodeURIComponent(val));
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
// Query params
|
|
81
74
|
const queryParams = parseKV(flags.query);
|
|
82
75
|
const qs = new URLSearchParams(queryParams).toString();
|
|
83
76
|
const url = `${baseUrl}${path}${qs ? "?" + qs : ""}`;
|
|
84
77
|
|
|
85
|
-
// Method
|
|
86
78
|
const method = (flags.method || op.method).toUpperCase();
|
|
79
|
+
const headers = { ...config.headers };
|
|
87
80
|
|
|
88
|
-
// Headers
|
|
89
|
-
const headers = {
|
|
90
|
-
...config.headers,
|
|
91
|
-
...parseKV(flags.header),
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Auth
|
|
95
|
-
if (config.auth) {
|
|
96
|
-
if (config.auth.startsWith("Bearer ") || config.auth.startsWith("Basic ")) {
|
|
97
|
-
headers["Authorization"] = config.auth;
|
|
98
|
-
} else {
|
|
99
|
-
headers["Authorization"] = `Bearer ${config.auth}`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Body
|
|
104
81
|
let body = undefined;
|
|
105
82
|
if (flags.data) {
|
|
106
83
|
body = flags.data;
|
|
107
|
-
if (!headers["Content-Type"])
|
|
108
|
-
headers["Content-Type"] = "application/json";
|
|
109
|
-
}
|
|
84
|
+
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
110
85
|
}
|
|
111
86
|
|
|
112
87
|
const res = await fetch(url, { method, headers, body });
|
|
113
88
|
const contentType = res.headers.get("content-type") || "";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (contentType.includes("json")) {
|
|
117
|
-
responseBody = await res.json();
|
|
118
|
-
} else {
|
|
119
|
-
responseBody = await res.text();
|
|
120
|
-
}
|
|
89
|
+
const responseBody = contentType.includes("json") ? await res.json() : await res.text();
|
|
121
90
|
|
|
122
91
|
out({
|
|
123
92
|
status: res.status,
|
|
@@ -133,14 +102,12 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
133
102
|
const op = spec.operations.find((o) => o.name.toLowerCase() === lower);
|
|
134
103
|
if (!op) throw new Error(`Operation not found: ${target}`);
|
|
135
104
|
|
|
136
|
-
const endpoint = spec.endpoint;
|
|
137
|
-
if (!endpoint) throw new Error("No GraphQL endpoint
|
|
105
|
+
const endpoint = config.baseUrl || spec.endpoint;
|
|
106
|
+
if (!endpoint) throw new Error("No GraphQL endpoint. Set --base-url or register with --graphql <url>.");
|
|
138
107
|
|
|
139
|
-
// Build query from operation
|
|
140
108
|
let query;
|
|
141
109
|
let dataVariables;
|
|
142
110
|
if (flags.data) {
|
|
143
|
-
// If --data is provided, treat as raw GraphQL query
|
|
144
111
|
try {
|
|
145
112
|
const parsed = JSON.parse(flags.data);
|
|
146
113
|
query = parsed.query || flags.data;
|
|
@@ -149,34 +116,23 @@ async function callGraphQL(spec, config, target, flags) {
|
|
|
149
116
|
query = flags.data;
|
|
150
117
|
}
|
|
151
118
|
} else {
|
|
152
|
-
// Auto-build a simple query/mutation
|
|
153
119
|
query = buildGraphQLQuery(op, spec.types);
|
|
154
120
|
}
|
|
155
121
|
|
|
156
|
-
// Variables: --data variables merged with --var overrides
|
|
157
122
|
const varOverrides = parseKV(flags.var);
|
|
158
123
|
const variables = { ...dataVariables, ...varOverrides };
|
|
159
124
|
|
|
160
125
|
const headers = {
|
|
161
126
|
"Content-Type": "application/json",
|
|
162
127
|
...config.headers,
|
|
163
|
-
...parseKV(flags.header),
|
|
164
128
|
};
|
|
165
129
|
|
|
166
|
-
if (config.auth) {
|
|
167
|
-
if (config.auth.startsWith("Bearer ") || config.auth.startsWith("Basic ")) {
|
|
168
|
-
headers["Authorization"] = config.auth;
|
|
169
|
-
} else {
|
|
170
|
-
headers["Authorization"] = `Bearer ${config.auth}`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
130
|
const body = JSON.stringify({
|
|
175
131
|
query,
|
|
176
132
|
variables: Object.keys(variables).length > 0 ? variables : undefined,
|
|
177
133
|
});
|
|
178
134
|
|
|
179
|
-
const res = await fetch(
|
|
135
|
+
const res = await fetch(endpoint, { method: "POST", headers, body });
|
|
180
136
|
const responseBody = await res.json();
|
|
181
137
|
|
|
182
138
|
out({
|
|
@@ -193,18 +149,15 @@ function buildGraphQLQuery(op, types) {
|
|
|
193
149
|
const argsStr = args.length > 0
|
|
194
150
|
? `(${args.map((a) => `$${a.name}: ${flattenType(a.type)}`).join(", ")})`
|
|
195
151
|
: "";
|
|
196
|
-
|
|
197
152
|
const passArgs = args.length > 0
|
|
198
153
|
? `(${args.map((a) => `${a.name}: $${a.name}`).join(", ")})`
|
|
199
154
|
: "";
|
|
200
155
|
|
|
201
|
-
// Try to build a field selection from the return type
|
|
202
156
|
const returnTypeName = op.returnType?.replace(/[[\]!]/g, "");
|
|
203
157
|
const returnType = types?.find((t) => t.name === returnTypeName);
|
|
204
158
|
let fields = "";
|
|
205
159
|
|
|
206
160
|
if (returnType?.fields) {
|
|
207
|
-
// Select scalar fields only (1 level deep)
|
|
208
161
|
const scalarFields = returnType.fields
|
|
209
162
|
.filter((f) => {
|
|
210
163
|
const typeName = flattenType(f.type)?.replace(/[[\]!]/g, "");
|
|
@@ -213,9 +166,7 @@ function buildGraphQLQuery(op, types) {
|
|
|
213
166
|
})
|
|
214
167
|
.map((f) => f.name);
|
|
215
168
|
|
|
216
|
-
if (scalarFields.length > 0) {
|
|
217
|
-
fields = ` { ${scalarFields.join(" ")} }`;
|
|
218
|
-
}
|
|
169
|
+
if (scalarFields.length > 0) fields = ` { ${scalarFields.join(" ")} }`;
|
|
219
170
|
}
|
|
220
171
|
|
|
221
172
|
const keyword = op.kind === "mutation" ? "mutation" : "query";
|
package/src/commands/list.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import { getSpec } from "../store.js";
|
|
2
1
|
import { out } from "../output.js";
|
|
3
2
|
import { parseArgs } from "../args.js";
|
|
3
|
+
import { resolveActiveSpec } from "../resolve.js";
|
|
4
4
|
|
|
5
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
6
|
const opts = parseArgs(args);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
7
|
+
const { flags } = opts;
|
|
8
|
+
|
|
9
|
+
const { spec } = await resolveActiveSpec(flags);
|
|
10
|
+
|
|
11
|
+
const filter = flags.filter?.toLowerCase();
|
|
12
|
+
const compact = flags.compact !== "false";
|
|
13
|
+
const limit = parseInt(flags.limit) || 0;
|
|
14
|
+
const offset = parseInt(flags.offset) || 0;
|
|
15
|
+
const tag = flags.tag?.toLowerCase();
|
|
15
16
|
|
|
16
17
|
let operations;
|
|
17
18
|
|
|
18
19
|
if (spec.type === "openapi") {
|
|
19
|
-
|
|
20
|
+
let source = spec.operations;
|
|
21
|
+
if (tag) {
|
|
22
|
+
source = source.filter((op) => op.tags?.some((t) => t.toLowerCase().includes(tag)));
|
|
23
|
+
}
|
|
24
|
+
operations = source.map((op) =>
|
|
20
25
|
compact
|
|
21
26
|
? { id: op.id, method: op.method, path: op.path }
|
|
22
27
|
: {
|
|
@@ -28,14 +33,6 @@ export async function listOperations(args) {
|
|
|
28
33
|
deprecated: op.deprecated,
|
|
29
34
|
}
|
|
30
35
|
);
|
|
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
|
-
}
|
|
39
36
|
} else if (spec.type === "mcp") {
|
|
40
37
|
operations = spec.tools.map((t) =>
|
|
41
38
|
compact
|
|
@@ -57,28 +54,21 @@ export async function listOperations(args) {
|
|
|
57
54
|
}
|
|
58
55
|
);
|
|
59
56
|
|
|
60
|
-
// Filter by kind (query/mutation/subscription)
|
|
61
57
|
if (tag) {
|
|
62
58
|
operations = operations.filter((op) => op.kind === tag);
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
if (filter) {
|
|
67
|
-
operations = operations.filter((op) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
63
|
+
operations = operations.filter((op) =>
|
|
64
|
+
JSON.stringify(op).toLowerCase().includes(filter)
|
|
65
|
+
);
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
const total = operations.length;
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
operations = operations.slice(offset);
|
|
78
|
-
}
|
|
79
|
-
if (limit > 0) {
|
|
80
|
-
operations = operations.slice(0, limit);
|
|
81
|
-
}
|
|
70
|
+
if (offset > 0) operations = operations.slice(offset);
|
|
71
|
+
if (limit > 0) operations = operations.slice(0, limit);
|
|
82
72
|
|
|
83
73
|
out({
|
|
84
74
|
type: spec.type,
|
package/src/commands/load.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
|
-
import {
|
|
5
|
-
import { out, err } from "../output.js";
|
|
6
|
-
import { parseArgs } from "../args.js";
|
|
4
|
+
import { parseKV } from "../args.js";
|
|
7
5
|
import { createMcpClient } from "../mcp-client.js";
|
|
8
6
|
|
|
9
7
|
const INTROSPECTION_QUERY = `{
|
|
@@ -61,78 +59,77 @@ fragment TypeRef on __Type {
|
|
|
61
59
|
}
|
|
62
60
|
}`;
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const source = positional[0];
|
|
82
|
-
if (!source) throw new Error("Usage: spec load <file-or-url> | spec load --mcp-stdio <cmd> | spec load --mcp-sse <url> | spec load --mcp-http <url>");
|
|
83
|
-
|
|
84
|
-
// Detect if it's a URL or file
|
|
85
|
-
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
86
|
-
|
|
87
|
-
let spec;
|
|
88
|
-
|
|
89
|
-
if (isUrl) {
|
|
90
|
-
spec = await loadFromUrl(source);
|
|
91
|
-
} else {
|
|
92
|
-
spec = loadFromFile(source);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
saveSpec(spec);
|
|
96
|
-
out({
|
|
97
|
-
ok: true,
|
|
98
|
-
type: spec.type,
|
|
99
|
-
title: spec.title || null,
|
|
100
|
-
operationCount: countOperations(spec),
|
|
101
|
-
source: source,
|
|
102
|
-
});
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a spec from a registry entry or inline flags entry.
|
|
64
|
+
* Entry shape:
|
|
65
|
+
* { type: "openapi", source: "<url-or-file>", config: { headers, auth } }
|
|
66
|
+
* { type: "graphql", source: "<url>", config: { headers, auth } }
|
|
67
|
+
* { type: "mcp", transport: "stdio|sse|streamable-http", url?, command?, args?, config: { headers, env } }
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveSpec(entry) {
|
|
70
|
+
if (entry.type === "mcp") return await loadMCPFromEntry(entry);
|
|
71
|
+
if (entry.type === "graphql") return await loadGraphQL(entry.source, entry.config?.headers);
|
|
72
|
+
// openapi — url or file; skip GraphQL probe since type is explicitly declared
|
|
73
|
+
const isUrl = entry.source?.startsWith("http://") || entry.source?.startsWith("https://");
|
|
74
|
+
return isUrl ? await loadFromUrl(entry.source, true) : loadFromFile(entry.source);
|
|
103
75
|
}
|
|
104
76
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Build an inline entry from flags (for ad-hoc commands like --mcp-http <url>).
|
|
79
|
+
* Returns null if no inline source flags present.
|
|
80
|
+
*/
|
|
81
|
+
export function inlineEntryFromFlags(flags) {
|
|
108
82
|
if (flags["mcp-stdio"]) {
|
|
109
83
|
const raw = flags["mcp-stdio"];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
84
|
+
const parts = (raw.trim() ? raw.match(/(?:[^\s"]+|"[^"]*")+/g) : null)?.map((p) => p.replace(/^"|"$/g, ""));
|
|
85
|
+
if (!parts?.length) throw new Error("--mcp-stdio requires a non-empty command string");
|
|
86
|
+
return {
|
|
87
|
+
type: "mcp",
|
|
113
88
|
transport: "stdio",
|
|
114
89
|
command: parts[0],
|
|
115
90
|
args: parts.slice(1),
|
|
91
|
+
config: { env: parseKV(flags.env) },
|
|
116
92
|
};
|
|
117
|
-
}
|
|
118
|
-
|
|
93
|
+
}
|
|
94
|
+
if (flags["mcp-sse"]) {
|
|
95
|
+
return {
|
|
96
|
+
type: "mcp",
|
|
119
97
|
transport: "sse",
|
|
120
98
|
url: flags["mcp-sse"],
|
|
99
|
+
config: { headers: parseKV(flags.header) },
|
|
121
100
|
};
|
|
122
|
-
}
|
|
123
|
-
|
|
101
|
+
}
|
|
102
|
+
if (flags["mcp-http"]) {
|
|
103
|
+
return {
|
|
104
|
+
type: "mcp",
|
|
124
105
|
transport: "streamable-http",
|
|
125
106
|
url: flags["mcp-http"],
|
|
107
|
+
config: { headers: parseKV(flags.header) },
|
|
126
108
|
};
|
|
127
109
|
}
|
|
110
|
+
if (flags.graphql) {
|
|
111
|
+
return { type: "graphql", source: flags.graphql, config: { headers: parseKV(flags.header) } };
|
|
112
|
+
}
|
|
113
|
+
if (flags.openapi) {
|
|
114
|
+
return { type: "openapi", source: flags.openapi, config: { headers: parseKV(flags.header), baseUrl: flags["base-url"] || null } };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Internal loaders ---
|
|
128
120
|
|
|
129
|
-
|
|
121
|
+
async function loadMCPFromEntry(entry) {
|
|
122
|
+
const client = await createMcpClient(entry);
|
|
130
123
|
try {
|
|
131
124
|
const { tools } = await client.listTools();
|
|
132
125
|
return {
|
|
133
126
|
type: "mcp",
|
|
134
|
-
title: "MCP Server",
|
|
135
|
-
|
|
127
|
+
title: entry.name || "MCP Server",
|
|
128
|
+
transport: entry.transport,
|
|
129
|
+
url: entry.url,
|
|
130
|
+
command: entry.command,
|
|
131
|
+
args: entry.args,
|
|
132
|
+
config: entry.config,
|
|
136
133
|
tools: tools.map((t) => ({
|
|
137
134
|
name: t.name,
|
|
138
135
|
description: t.description || null,
|
|
@@ -144,24 +141,21 @@ async function loadMCP(flags) {
|
|
|
144
141
|
}
|
|
145
142
|
}
|
|
146
143
|
|
|
147
|
-
async function loadFromUrl(url) {
|
|
148
|
-
// Try GraphQL introspection first if URL doesn't end with known extensions
|
|
144
|
+
async function loadFromUrl(url, skipGraphQLProbe = false) {
|
|
149
145
|
const lowerUrl = url.toLowerCase();
|
|
150
146
|
const isLikelyFile =
|
|
151
147
|
lowerUrl.endsWith(".json") ||
|
|
152
148
|
lowerUrl.endsWith(".yaml") ||
|
|
153
149
|
lowerUrl.endsWith(".yml");
|
|
154
150
|
|
|
155
|
-
if (!isLikelyFile) {
|
|
156
|
-
// Try GraphQL introspection
|
|
151
|
+
if (!isLikelyFile && !skipGraphQLProbe) {
|
|
157
152
|
try {
|
|
158
153
|
return await loadGraphQL(url);
|
|
159
|
-
} catch
|
|
154
|
+
} catch {
|
|
160
155
|
// Fall through to OpenAPI
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
158
|
|
|
164
|
-
// Fetch as OpenAPI
|
|
165
159
|
const res = await fetch(url);
|
|
166
160
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
167
161
|
const text = await res.text();
|
|
@@ -175,10 +169,10 @@ function loadFromFile(path) {
|
|
|
175
169
|
return parseOpenAPI(text, path);
|
|
176
170
|
}
|
|
177
171
|
|
|
178
|
-
async function loadGraphQL(url) {
|
|
172
|
+
async function loadGraphQL(url, extraHeaders = {}) {
|
|
179
173
|
const res = await fetch(url, {
|
|
180
174
|
method: "POST",
|
|
181
|
-
headers: { "Content-Type": "application/json" },
|
|
175
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
182
176
|
body: JSON.stringify({ query: INTROSPECTION_QUERY }),
|
|
183
177
|
});
|
|
184
178
|
|
|
@@ -190,8 +184,6 @@ async function loadGraphQL(url) {
|
|
|
190
184
|
}
|
|
191
185
|
|
|
192
186
|
const schema = json.data.__schema;
|
|
193
|
-
|
|
194
|
-
// Extract operations from query/mutation/subscription types
|
|
195
187
|
const operations = [];
|
|
196
188
|
const typeMap = {};
|
|
197
189
|
for (const t of schema.types) {
|
|
@@ -237,7 +229,6 @@ function parseOpenAPI(text, source) {
|
|
|
237
229
|
doc = YAML.parse(text);
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
// Detect OpenAPI vs Swagger
|
|
241
232
|
const version = doc.openapi || doc.swagger;
|
|
242
233
|
if (!version) throw new Error("Not a valid OpenAPI/Swagger spec");
|
|
243
234
|
|
|
@@ -284,7 +275,3 @@ function flattenType(t) {
|
|
|
284
275
|
}
|
|
285
276
|
return t.kind;
|
|
286
277
|
}
|
|
287
|
-
|
|
288
|
-
function countOperations(spec) {
|
|
289
|
-
return spec.operations?.length || 0;
|
|
290
|
-
}
|