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