api-spec-cli 0.2.3 → 0.2.4
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 +343 -282
- package/package.json +53 -41
- package/src/cli.js +183 -172
- package/src/commands/add.js +161 -78
- package/src/commands/auth.js +32 -0
- package/src/commands/call.js +220 -207
- package/src/commands/fetch.js +344 -308
- package/src/commands/grep.js +67 -64
- package/src/commands/list.js +78 -80
- package/src/commands/show.js +224 -215
- package/src/commands/specs.js +82 -69
- package/src/commands/types.js +167 -163
- package/src/commands/validate.js +295 -269
- package/src/glob.js +34 -29
- package/src/mcp-client.js +88 -63
- package/src/oauth/auth-flow.js +59 -0
- package/src/oauth/provider.js +192 -0
- package/src/oauth/tokens.js +53 -0
- package/src/registry.js +79 -53
- package/src/resolve.js +64 -65
package/src/commands/show.js
CHANGED
|
@@ -1,215 +1,224 @@
|
|
|
1
|
-
import { out } from "../output.js";
|
|
2
|
-
import { parseArgs } from "../args.js";
|
|
3
|
-
import { resolveSpec } from "../resolve.js";
|
|
4
|
-
|
|
5
|
-
export async function showOperation(args) {
|
|
6
|
-
const { flags, positional } = parseArgs(args);
|
|
7
|
-
const target = positional[0];
|
|
8
|
-
if (!target)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
result[code] =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1
|
+
import { out } from "../output.js";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { resolveSpec } from "../resolve.js";
|
|
4
|
+
|
|
5
|
+
export async function showOperation(args) {
|
|
6
|
+
const { flags, positional } = parseArgs(args);
|
|
7
|
+
const target = positional[0];
|
|
8
|
+
if (!target)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Usage: spec show <operationId-or-path> [--spec <name> | --openapi <url> | ...]"
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const { spec } = await resolveSpec(flags);
|
|
14
|
+
|
|
15
|
+
if (spec.type === "openapi") {
|
|
16
|
+
showOpenAPI(spec, target);
|
|
17
|
+
} else if (spec.type === "mcp") {
|
|
18
|
+
showMCP(spec, target);
|
|
19
|
+
} else {
|
|
20
|
+
showGraphQL(spec, target);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function showOpenAPI(spec, target) {
|
|
25
|
+
const lower = target.toLowerCase();
|
|
26
|
+
|
|
27
|
+
const op = spec.operations.find(
|
|
28
|
+
(o) =>
|
|
29
|
+
o.id.toLowerCase() === lower ||
|
|
30
|
+
o.path.toLowerCase() === lower ||
|
|
31
|
+
`${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!op) {
|
|
35
|
+
throw new Error(`Operation not found: ${target}. Run 'spec list' to see available operations.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const root = spec.raw || spec.components;
|
|
39
|
+
|
|
40
|
+
out({
|
|
41
|
+
id: op.id,
|
|
42
|
+
method: op.method,
|
|
43
|
+
path: op.path,
|
|
44
|
+
summary: op.summary,
|
|
45
|
+
description: op.description,
|
|
46
|
+
tags: op.tags,
|
|
47
|
+
deprecated: op.deprecated,
|
|
48
|
+
parameters: op.parameters.map((p) => {
|
|
49
|
+
const resolved = resolveRef(p, root);
|
|
50
|
+
return {
|
|
51
|
+
name: resolved.name,
|
|
52
|
+
in: resolved.in,
|
|
53
|
+
required: resolved.required || false,
|
|
54
|
+
type: resolved.schema?.type || null,
|
|
55
|
+
format: resolved.schema?.format || undefined,
|
|
56
|
+
description: resolved.description || undefined,
|
|
57
|
+
enum: resolved.schema?.enum || undefined,
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
requestBody: op.requestBody ? resolveRequestBody(op.requestBody, root) : null,
|
|
61
|
+
responses: resolveResponsesCompact(op.responses, root),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function showMCP(spec, target) {
|
|
66
|
+
const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
|
|
67
|
+
if (!tool) {
|
|
68
|
+
throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
|
|
69
|
+
}
|
|
70
|
+
out({
|
|
71
|
+
name: tool.name,
|
|
72
|
+
description: tool.description,
|
|
73
|
+
inputSchema: tool.inputSchema,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function showGraphQL(spec, target) {
|
|
78
|
+
const lower = target.toLowerCase();
|
|
79
|
+
|
|
80
|
+
const op = spec.operations.find((o) => o.name.toLowerCase() === lower);
|
|
81
|
+
|
|
82
|
+
if (!op) {
|
|
83
|
+
throw new Error(`Operation not found: ${target}. Run 'spec list' to see available operations.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const relatedTypes = findRelatedTypes(op, spec.types);
|
|
87
|
+
|
|
88
|
+
out({
|
|
89
|
+
name: op.name,
|
|
90
|
+
kind: op.kind,
|
|
91
|
+
description: op.description,
|
|
92
|
+
returnType: op.returnType,
|
|
93
|
+
isDeprecated: op.isDeprecated,
|
|
94
|
+
args: op.args?.map((a) => ({
|
|
95
|
+
name: a.name,
|
|
96
|
+
type: flattenType(a.type),
|
|
97
|
+
required: a.type?.kind === "NON_NULL",
|
|
98
|
+
description: a.description || undefined,
|
|
99
|
+
defaultValue: a.defaultValue || undefined,
|
|
100
|
+
})),
|
|
101
|
+
relatedTypes,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Helpers ---
|
|
106
|
+
|
|
107
|
+
function resolveRef(obj, root) {
|
|
108
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
109
|
+
if (obj.$ref) {
|
|
110
|
+
const path = obj.$ref.replace("#/", "").split("/");
|
|
111
|
+
let resolved = root;
|
|
112
|
+
for (const p of path) resolved = resolved?.[p];
|
|
113
|
+
return resolved || obj;
|
|
114
|
+
}
|
|
115
|
+
return obj;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveRequestBody(body, root) {
|
|
119
|
+
if (!body) return null;
|
|
120
|
+
const resolved = resolveRef(body, root);
|
|
121
|
+
if (resolved?.content) {
|
|
122
|
+
const jsonContent = resolved.content["application/json"];
|
|
123
|
+
if (jsonContent) {
|
|
124
|
+
return {
|
|
125
|
+
description: resolved.description || undefined,
|
|
126
|
+
required: resolved.required || undefined,
|
|
127
|
+
schema: resolveSchema(jsonContent.schema, root),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const [mediaType, value] = Object.entries(resolved.content)[0];
|
|
131
|
+
return {
|
|
132
|
+
description: resolved.description || undefined,
|
|
133
|
+
required: resolved.required || undefined,
|
|
134
|
+
mediaType,
|
|
135
|
+
schema: resolveSchema(value.schema, root),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveResponsesCompact(responses, root) {
|
|
142
|
+
if (!responses) return null;
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [code, resp] of Object.entries(responses)) {
|
|
145
|
+
const resolved = resolveRef(resp, root);
|
|
146
|
+
if (resolved?.content) {
|
|
147
|
+
const jsonContent = resolved.content["application/json"];
|
|
148
|
+
result[code] = jsonContent
|
|
149
|
+
? { description: resolved.description, schema: resolveSchema(jsonContent.schema, root) }
|
|
150
|
+
: { description: resolved.description };
|
|
151
|
+
} else {
|
|
152
|
+
result[code] = { description: resolved.description };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveSchema(schema, root, depth = 0) {
|
|
159
|
+
if (!schema || depth > 3) return schema;
|
|
160
|
+
if (schema.$ref) return resolveSchema(resolveRef(schema, root), root, depth + 1);
|
|
161
|
+
if (schema.properties) {
|
|
162
|
+
const result = { type: schema.type, required: schema.required, properties: {} };
|
|
163
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
164
|
+
if (val.$ref) {
|
|
165
|
+
result.properties[key] = { $ref: val.$ref.split("/").pop() };
|
|
166
|
+
} else if (val.type === "array" && val.items?.$ref) {
|
|
167
|
+
result.properties[key] = { type: "array", items: val.items.$ref.split("/").pop() };
|
|
168
|
+
} else {
|
|
169
|
+
const prop = { type: val.type };
|
|
170
|
+
if (val.format) prop.format = val.format;
|
|
171
|
+
if (val.enum) prop.enum = val.enum;
|
|
172
|
+
if (val.description) prop.description = val.description;
|
|
173
|
+
result.properties[key] = prop;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
if (schema.items) {
|
|
179
|
+
if (schema.items.$ref) return { type: "array", items: schema.items.$ref.split("/").pop() };
|
|
180
|
+
return { type: "array", items: resolveSchema(schema.items, root, depth + 1) };
|
|
181
|
+
}
|
|
182
|
+
return schema;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findRelatedTypes(op, types) {
|
|
186
|
+
const names = new Set();
|
|
187
|
+
|
|
188
|
+
function extractTypeNames(typeStr) {
|
|
189
|
+
if (!typeStr) return;
|
|
190
|
+
const cleaned = typeStr.replace(/[[\]!]/g, "");
|
|
191
|
+
if (cleaned) names.add(cleaned);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
extractTypeNames(op.returnType);
|
|
195
|
+
for (const arg of op.args || []) extractTypeNames(flattenType(arg.type));
|
|
196
|
+
|
|
197
|
+
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
198
|
+
return types
|
|
199
|
+
.filter((t) => names.has(t.name) && !scalars.has(t.name))
|
|
200
|
+
.map((t) => {
|
|
201
|
+
const result = { name: t.name, kind: t.kind };
|
|
202
|
+
if (t.fields)
|
|
203
|
+
result.fields = t.fields.map((f) => ({ name: f.name, type: flattenType(f.type) }));
|
|
204
|
+
if (t.inputFields)
|
|
205
|
+
result.inputFields = t.inputFields.map((f) => ({
|
|
206
|
+
name: f.name,
|
|
207
|
+
type: flattenType(f.type),
|
|
208
|
+
}));
|
|
209
|
+
if (t.enumValues) result.enumValues = t.enumValues.map((e) => e.name);
|
|
210
|
+
return result;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function flattenType(t) {
|
|
215
|
+
if (!t) return null;
|
|
216
|
+
if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
|
|
217
|
+
if (t.ofType) {
|
|
218
|
+
const inner = flattenType(t.ofType);
|
|
219
|
+
if (t.kind === "LIST") return `[${inner}]`;
|
|
220
|
+
if (t.kind === "NON_NULL") return `${inner}!`;
|
|
221
|
+
return inner;
|
|
222
|
+
}
|
|
223
|
+
return t.kind;
|
|
224
|
+
}
|
package/src/commands/specs.js
CHANGED
|
@@ -1,69 +1,82 @@
|
|
|
1
|
-
import { parseArgs } from "../args.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
1
|
+
import { parseArgs } from "../args.js";
|
|
2
|
+
import {
|
|
3
|
+
getRegistry,
|
|
4
|
+
saveRegistry,
|
|
5
|
+
getEntry,
|
|
6
|
+
removeCachedSpec,
|
|
7
|
+
saveCachedSpec,
|
|
8
|
+
allEntries,
|
|
9
|
+
} from "../registry.js";
|
|
10
|
+
import { fetchSpec } from "./fetch.js";
|
|
11
|
+
import { out } from "../output.js";
|
|
12
|
+
|
|
13
|
+
function findSection(registry, name) {
|
|
14
|
+
for (const section of ["mcp", "openapi", "graphql"]) {
|
|
15
|
+
if (registry[section]?.[name]) return section;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function specsCmd(args) {
|
|
21
|
+
const { flags } = parseArgs(args);
|
|
22
|
+
const compact = flags.compact !== "false";
|
|
23
|
+
const registry = getRegistry();
|
|
24
|
+
|
|
25
|
+
const specs = allEntries(registry).map((e) => {
|
|
26
|
+
if (compact) {
|
|
27
|
+
return {
|
|
28
|
+
name: e.name,
|
|
29
|
+
type: e.type,
|
|
30
|
+
description: e.description || null,
|
|
31
|
+
enabled: e.enabled,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return e;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
out({ specs });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function registryMutate(action, args) {
|
|
41
|
+
const { positional } = parseArgs(args);
|
|
42
|
+
const name = positional[0];
|
|
43
|
+
if (!name) throw new Error(`Usage: spec ${action} <name>`);
|
|
44
|
+
|
|
45
|
+
const registry = getRegistry();
|
|
46
|
+
const section = findSection(registry, name);
|
|
47
|
+
if (!section) throw new Error(`No spec named '${name}'. Run 'spec specs' to see available.`);
|
|
48
|
+
|
|
49
|
+
if (action === "remove") {
|
|
50
|
+
delete registry[section][name];
|
|
51
|
+
saveRegistry(registry);
|
|
52
|
+
removeCachedSpec(name);
|
|
53
|
+
out({ ok: true, removed: name });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action === "enable") {
|
|
58
|
+
registry[section][name].enabled = true;
|
|
59
|
+
saveRegistry(registry);
|
|
60
|
+
out({ ok: true, enabled: name });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (action === "disable") {
|
|
65
|
+
registry[section][name].enabled = false;
|
|
66
|
+
saveRegistry(registry);
|
|
67
|
+
out({ ok: true, disabled: name });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (action === "refresh") {
|
|
72
|
+
const entry = { ...registry[section][name], name, _section: section };
|
|
73
|
+
if (!entry.enabled) throw new Error(`Spec '${name}' is disabled. Enable it first.`);
|
|
74
|
+
const spec = await fetchSpec(entry);
|
|
75
|
+
saveCachedSpec(name, spec);
|
|
76
|
+
const count = spec.tools?.length ?? spec.operations?.length ?? 0;
|
|
77
|
+
out({ ok: true, refreshed: name, type: spec.type, count });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(`Unknown registry action: ${action}`);
|
|
82
|
+
}
|