@stackkedjohn/mcp-factory-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 +5 -0
- package/dist/commands/create.js +4 -0
- package/dist/parsers/apib-parser.d.ts +2 -0
- package/dist/parsers/apib-parser.js +354 -0
- package/dist/parsers/apib-parser.test.d.ts +1 -0
- package/dist/parsers/apib-parser.test.js +59 -0
- package/dist/parsers/detector.d.ts +1 -1
- package/dist/parsers/detector.js +8 -0
- package/dist/parsers/detector.test.d.ts +1 -0
- package/dist/parsers/detector.test.js +20 -0
- package/docs/plans/2026-02-02-api-blueprint-support.md +781 -0
- package/package.json +2 -1
- package/src/commands/create.ts +3 -0
- package/src/parsers/apib-parser.test.ts +64 -0
- package/src/parsers/apib-parser.ts +421 -0
- package/src/parsers/detector.test.ts +21 -0
- package/src/parsers/detector.ts +11 -1
- package/test-fixtures/sample.apib +31 -0
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ npm install -g @stackkedjohn/mcp-factory-cli
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# Generate MCP server from API documentation
|
|
28
|
+
# Supports: OpenAPI, Swagger, API Blueprint (.apib)
|
|
28
29
|
mcp-factory create ./api-docs.yaml
|
|
29
30
|
|
|
30
31
|
# Build the generated server
|
|
@@ -103,6 +104,7 @@ Options:
|
|
|
103
104
|
**Supported Formats:**
|
|
104
105
|
- OpenAPI 3.x (JSON/YAML)
|
|
105
106
|
- Swagger 2.0 (JSON/YAML)
|
|
107
|
+
- API Blueprint (.apib)
|
|
106
108
|
- Postman Collections (coming soon)
|
|
107
109
|
- Unstructured docs with `--ai-parse` (coming soon)
|
|
108
110
|
|
|
@@ -111,6 +113,9 @@ Options:
|
|
|
111
113
|
# Local file
|
|
112
114
|
mcp-factory create ./openapi.yaml
|
|
113
115
|
|
|
116
|
+
# API Blueprint file
|
|
117
|
+
mcp-factory create ./api-documentation.apib
|
|
118
|
+
|
|
114
119
|
# URL (coming soon)
|
|
115
120
|
mcp-factory create https://api.example.com/openapi.json
|
|
116
121
|
|
package/dist/commands/create.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as path from 'path';
|
|
|
2
2
|
import { detectFormat } from '../parsers/detector.js';
|
|
3
3
|
import { parseOpenAPI } from '../parsers/openapi.js';
|
|
4
4
|
import { parsePostman } from '../parsers/postman.js';
|
|
5
|
+
import { parseAPIBlueprint } from '../parsers/apib-parser.js';
|
|
5
6
|
import { parseWithAI } from '../parsers/ai-parser.js';
|
|
6
7
|
import { analyzePatterns } from '../generator/analyzer.js';
|
|
7
8
|
import { generateServer } from '../generator/engine.js';
|
|
@@ -22,6 +23,9 @@ export async function createCommand(input, options) {
|
|
|
22
23
|
else if (detection.format === 'postman') {
|
|
23
24
|
schema = parsePostman(detection.content);
|
|
24
25
|
}
|
|
26
|
+
else if (detection.format === 'apib') {
|
|
27
|
+
schema = await parseAPIBlueprint(detection.content);
|
|
28
|
+
}
|
|
25
29
|
else if (options.aiParse) {
|
|
26
30
|
logger.info('Using AI parser for unstructured docs...');
|
|
27
31
|
schema = await parseWithAI(JSON.stringify(detection.content));
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import drafterModule from 'drafter.js';
|
|
2
|
+
import { ParseError } from '../utils/errors.js';
|
|
3
|
+
// Type assertion for drafter.js API (type definitions are incomplete)
|
|
4
|
+
const drafter = drafterModule;
|
|
5
|
+
// Helper to check if element has a class
|
|
6
|
+
function hasClass(element, className) {
|
|
7
|
+
if (!element?.meta?.classes)
|
|
8
|
+
return false;
|
|
9
|
+
if (element.meta.classes.element === 'array') {
|
|
10
|
+
return element.meta.classes.content.some((c) => c.content === className);
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
export async function parseAPIBlueprint(content) {
|
|
15
|
+
try {
|
|
16
|
+
// Parse the API Blueprint document using drafter.js (synchronous)
|
|
17
|
+
const result = drafter.parseSync(content, {});
|
|
18
|
+
// Check for parsing errors
|
|
19
|
+
if (result.error) {
|
|
20
|
+
throw new ParseError(`API Blueprint parsing failed: ${result.error.message}`);
|
|
21
|
+
}
|
|
22
|
+
const ast = result;
|
|
23
|
+
// Extract API metadata
|
|
24
|
+
const apiMetadata = ast.content[0];
|
|
25
|
+
const name = apiMetadata.meta?.title?.content || 'Untitled API';
|
|
26
|
+
const baseUrl = extractBaseUrl(ast);
|
|
27
|
+
// Extract all endpoints
|
|
28
|
+
const endpoints = [];
|
|
29
|
+
for (const resourceGroup of ast.content) {
|
|
30
|
+
if (resourceGroup.element === 'category' && hasClass(resourceGroup, 'resourceGroup')) {
|
|
31
|
+
for (const resource of resourceGroup.content) {
|
|
32
|
+
if (resource.element === 'resource') {
|
|
33
|
+
const resourcePath = resource.attributes?.href?.content || '';
|
|
34
|
+
for (const transition of resource.content) {
|
|
35
|
+
if (transition.element === 'transition') {
|
|
36
|
+
const transaction = transition.content.find((c) => c.element === 'httpTransaction');
|
|
37
|
+
if (transaction) {
|
|
38
|
+
const endpoint = parseAction(transaction, resourcePath, transition);
|
|
39
|
+
endpoints.push(endpoint);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Also check for resources at the top level (like in api category)
|
|
47
|
+
if (resourceGroup.element === 'category' && hasClass(resourceGroup, 'api')) {
|
|
48
|
+
for (const item of resourceGroup.content) {
|
|
49
|
+
if (item.element === 'resource') {
|
|
50
|
+
const resourcePath = item.attributes?.href?.content || '';
|
|
51
|
+
for (const transition of item.content) {
|
|
52
|
+
if (transition.element === 'transition') {
|
|
53
|
+
const transaction = transition.content.find((c) => c.element === 'httpTransaction');
|
|
54
|
+
if (transaction) {
|
|
55
|
+
const endpoint = parseAction(transaction, resourcePath, transition);
|
|
56
|
+
endpoints.push(endpoint);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Also check for resources at the very top level
|
|
64
|
+
if (resourceGroup.element === 'resource') {
|
|
65
|
+
const resourcePath = resourceGroup.attributes?.href?.content || '';
|
|
66
|
+
for (const transition of resourceGroup.content) {
|
|
67
|
+
if (transition.element === 'transition') {
|
|
68
|
+
const transaction = transition.content.find((c) => c.element === 'httpTransaction');
|
|
69
|
+
if (transaction) {
|
|
70
|
+
const endpoint = parseAction(transaction, resourcePath, transition);
|
|
71
|
+
endpoints.push(endpoint);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Detect authentication
|
|
78
|
+
const auth = detectAuth(ast);
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
baseUrl,
|
|
82
|
+
auth,
|
|
83
|
+
endpoints,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof ParseError) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
throw new ParseError(`Failed to parse API Blueprint: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function extractBaseUrl(ast) {
|
|
94
|
+
// Look for HOST in metadata within category
|
|
95
|
+
for (const element of ast.content) {
|
|
96
|
+
if (element.element === 'category' && hasClass(element, 'api')) {
|
|
97
|
+
// Check attributes.metadata array
|
|
98
|
+
if (element.attributes?.metadata?.element === 'array') {
|
|
99
|
+
for (const metaItem of element.attributes.metadata.content) {
|
|
100
|
+
if (metaItem.element === 'member') {
|
|
101
|
+
const key = metaItem.content?.key?.content;
|
|
102
|
+
if (key === 'HOST') {
|
|
103
|
+
return metaItem.content.value.content;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return 'https://api.example.com';
|
|
111
|
+
}
|
|
112
|
+
function parseAction(transaction, resourcePath, transition) {
|
|
113
|
+
const request = transaction.content.find((c) => c.element === 'httpRequest');
|
|
114
|
+
const response = transaction.content.find((c) => c.element === 'httpResponse');
|
|
115
|
+
const method = (request?.attributes?.method?.content || 'GET').toUpperCase();
|
|
116
|
+
const path = request?.attributes?.href?.content || resourcePath;
|
|
117
|
+
// Extract description - check transition's copy element first, then meta.title
|
|
118
|
+
let description = '';
|
|
119
|
+
const copyElement = transition.content.find((c) => c.element === 'copy');
|
|
120
|
+
if (copyElement?.content) {
|
|
121
|
+
description = copyElement.content;
|
|
122
|
+
}
|
|
123
|
+
else if (transition.meta?.title?.content) {
|
|
124
|
+
description = transition.meta.title.content;
|
|
125
|
+
}
|
|
126
|
+
else if (transaction.meta?.title?.content) {
|
|
127
|
+
description = transaction.meta.title.content;
|
|
128
|
+
}
|
|
129
|
+
else if (request?.meta?.title?.content) {
|
|
130
|
+
description = request.meta.title.content;
|
|
131
|
+
}
|
|
132
|
+
// Parse parameters
|
|
133
|
+
const parameters = parseParameters(request);
|
|
134
|
+
// Parse request body
|
|
135
|
+
const requestBody = parseRequestBody(request);
|
|
136
|
+
// Parse response
|
|
137
|
+
const responseSchema = parseResponse(response);
|
|
138
|
+
// Parse errors (look for non-2xx responses)
|
|
139
|
+
const errors = parseErrors(transaction);
|
|
140
|
+
const id = `${method.toLowerCase()}-${path.replace(/\//g, '-').replace(/[{}]/g, '')}`;
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
method,
|
|
144
|
+
path,
|
|
145
|
+
description,
|
|
146
|
+
parameters,
|
|
147
|
+
requestBody,
|
|
148
|
+
response: responseSchema,
|
|
149
|
+
errors,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseParameters(request) {
|
|
153
|
+
const parameters = [];
|
|
154
|
+
if (!request?.attributes?.hrefVariables) {
|
|
155
|
+
return parameters;
|
|
156
|
+
}
|
|
157
|
+
const hrefVariables = request.attributes.hrefVariables.content;
|
|
158
|
+
for (const variable of hrefVariables) {
|
|
159
|
+
if (variable.element === 'member') {
|
|
160
|
+
const name = variable.content.key.content;
|
|
161
|
+
const description = variable.meta?.description?.content || '';
|
|
162
|
+
const required = variable.attributes?.typeAttributes?.includes('required') || false;
|
|
163
|
+
// Determine parameter location
|
|
164
|
+
let location = 'path';
|
|
165
|
+
if (variable.attributes?.typeAttributes?.includes('query')) {
|
|
166
|
+
location = 'query';
|
|
167
|
+
}
|
|
168
|
+
else if (variable.attributes?.typeAttributes?.includes('header')) {
|
|
169
|
+
location = 'header';
|
|
170
|
+
}
|
|
171
|
+
const schema = mapTypeToSchemaType(variable.content.value.element);
|
|
172
|
+
parameters.push({
|
|
173
|
+
name,
|
|
174
|
+
in: location,
|
|
175
|
+
description,
|
|
176
|
+
required,
|
|
177
|
+
schema,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return parameters;
|
|
182
|
+
}
|
|
183
|
+
function parseRequestBody(request) {
|
|
184
|
+
if (!request?.content) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const asset = request.content.find((c) => c.element === 'asset');
|
|
188
|
+
if (!asset) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const contentType = asset.attributes?.contentType?.content || 'application/json';
|
|
192
|
+
const bodyContent = asset.content;
|
|
193
|
+
// Try to parse body as JSON to infer schema
|
|
194
|
+
let schema = { type: 'object' };
|
|
195
|
+
if (bodyContent) {
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(bodyContent);
|
|
198
|
+
schema = inferSchemaFromValue(parsed);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// If not JSON, keep as generic object
|
|
202
|
+
schema = { type: 'object' };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
required: true,
|
|
207
|
+
contentType,
|
|
208
|
+
schema,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function parseResponse(response) {
|
|
212
|
+
const statusCode = parseInt(response?.attributes?.statusCode?.content || '200');
|
|
213
|
+
const description = response?.meta?.title?.content || '';
|
|
214
|
+
let contentType = 'application/json';
|
|
215
|
+
let schema = { type: 'object' };
|
|
216
|
+
if (response?.content) {
|
|
217
|
+
const asset = response.content.find((c) => c.element === 'asset');
|
|
218
|
+
if (asset) {
|
|
219
|
+
contentType = asset.attributes?.contentType?.content || 'application/json';
|
|
220
|
+
const bodyContent = asset.content;
|
|
221
|
+
if (bodyContent) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(bodyContent);
|
|
224
|
+
schema = inferSchemaFromValue(parsed);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
schema = { type: 'object' };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
statusCode,
|
|
234
|
+
description,
|
|
235
|
+
contentType,
|
|
236
|
+
schema,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parseErrors(transaction) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
if (!transaction?.content) {
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
244
|
+
for (const item of transaction.content) {
|
|
245
|
+
if (item.element === 'httpResponse') {
|
|
246
|
+
const statusCode = parseInt(item.attributes?.statusCode?.content || '200');
|
|
247
|
+
// Only include error responses (4xx and 5xx)
|
|
248
|
+
if (statusCode >= 400) {
|
|
249
|
+
const description = item.meta?.title?.content || `Error ${statusCode}`;
|
|
250
|
+
let schema;
|
|
251
|
+
const asset = item.content?.find((c) => c.element === 'asset');
|
|
252
|
+
if (asset?.content) {
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(asset.content);
|
|
255
|
+
schema = inferSchemaFromValue(parsed);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
schema = { type: 'object' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
errors.push({
|
|
262
|
+
statusCode,
|
|
263
|
+
description,
|
|
264
|
+
schema,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return errors;
|
|
270
|
+
}
|
|
271
|
+
function detectAuth(ast) {
|
|
272
|
+
// Convert AST to string for pattern matching
|
|
273
|
+
const astString = JSON.stringify(ast);
|
|
274
|
+
// Look for common authentication patterns
|
|
275
|
+
if (astString.includes('Authorization') || astString.includes('bearer')) {
|
|
276
|
+
return {
|
|
277
|
+
type: 'bearer',
|
|
278
|
+
location: 'header',
|
|
279
|
+
name: 'Authorization',
|
|
280
|
+
description: 'Bearer token authentication',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (astString.includes('api-key') || astString.includes('apiKey') || astString.includes('X-API-Key')) {
|
|
284
|
+
return {
|
|
285
|
+
type: 'api-key',
|
|
286
|
+
location: 'header',
|
|
287
|
+
name: 'X-API-Key',
|
|
288
|
+
description: 'API key authentication',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (astString.includes('oauth') || astString.includes('OAuth')) {
|
|
292
|
+
return {
|
|
293
|
+
type: 'oauth',
|
|
294
|
+
description: 'OAuth 2.0 authentication',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (astString.includes('Basic')) {
|
|
298
|
+
return {
|
|
299
|
+
type: 'basic',
|
|
300
|
+
description: 'Basic authentication',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
type: 'none',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function mapTypeToSchemaType(element) {
|
|
308
|
+
switch (element) {
|
|
309
|
+
case 'string':
|
|
310
|
+
return { type: 'string' };
|
|
311
|
+
case 'number':
|
|
312
|
+
return { type: 'number' };
|
|
313
|
+
case 'boolean':
|
|
314
|
+
return { type: 'boolean' };
|
|
315
|
+
case 'array':
|
|
316
|
+
return { type: 'array', items: { type: 'object' } };
|
|
317
|
+
case 'object':
|
|
318
|
+
return { type: 'object' };
|
|
319
|
+
default:
|
|
320
|
+
return { type: 'string' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function inferSchemaFromValue(value) {
|
|
324
|
+
if (Array.isArray(value)) {
|
|
325
|
+
const itemSchema = value.length > 0 ? inferSchemaFromValue(value[0]) : { type: 'object' };
|
|
326
|
+
return {
|
|
327
|
+
type: 'array',
|
|
328
|
+
items: itemSchema,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (typeof value === 'object' && value !== null) {
|
|
332
|
+
const properties = {};
|
|
333
|
+
const required = [];
|
|
334
|
+
for (const [key, val] of Object.entries(value)) {
|
|
335
|
+
properties[key] = inferSchemaFromValue(val);
|
|
336
|
+
required.push(key);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties,
|
|
341
|
+
required,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (typeof value === 'string') {
|
|
345
|
+
return { type: 'string' };
|
|
346
|
+
}
|
|
347
|
+
if (typeof value === 'number') {
|
|
348
|
+
return { type: 'number' };
|
|
349
|
+
}
|
|
350
|
+
if (typeof value === 'boolean') {
|
|
351
|
+
return { type: 'boolean' };
|
|
352
|
+
}
|
|
353
|
+
return { type: 'object' };
|
|
354
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { parseAPIBlueprint } from './apib-parser.js';
|
|
4
|
+
describe('API Blueprint Parser', () => {
|
|
5
|
+
it('should parse a basic API Blueprint document', async () => {
|
|
6
|
+
const apibContent = `
|
|
7
|
+
FORMAT: 1A
|
|
8
|
+
HOST: https://api.example.com
|
|
9
|
+
|
|
10
|
+
# My API
|
|
11
|
+
This is my API description.
|
|
12
|
+
|
|
13
|
+
## GET /users
|
|
14
|
+
Get a list of users
|
|
15
|
+
|
|
16
|
+
+ Response 200 (application/json)
|
|
17
|
+
+ Body
|
|
18
|
+
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"id": 1,
|
|
22
|
+
"name": "John Doe"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
## POST /users
|
|
27
|
+
Create a new user
|
|
28
|
+
|
|
29
|
+
+ Request (application/json)
|
|
30
|
+
+ Body
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
"name": "John Doe"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
+ Response 201 (application/json)
|
|
37
|
+
+ Body
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
"id": 1,
|
|
41
|
+
"name": "John Doe"
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
const schema = await parseAPIBlueprint(apibContent);
|
|
45
|
+
assert.strictEqual(schema.name, 'My API');
|
|
46
|
+
assert.strictEqual(schema.baseUrl, 'https://api.example.com');
|
|
47
|
+
assert.strictEqual(schema.endpoints.length, 2);
|
|
48
|
+
// Check GET endpoint
|
|
49
|
+
const getEndpoint = schema.endpoints.find((e) => e.method === 'GET');
|
|
50
|
+
assert.ok(getEndpoint);
|
|
51
|
+
assert.strictEqual(getEndpoint.path, '/users');
|
|
52
|
+
assert.strictEqual(getEndpoint.description, 'Get a list of users');
|
|
53
|
+
// Check POST endpoint
|
|
54
|
+
const postEndpoint = schema.endpoints.find((e) => e.method === 'POST');
|
|
55
|
+
assert.ok(postEndpoint);
|
|
56
|
+
assert.strictEqual(postEndpoint.path, '/users');
|
|
57
|
+
assert.strictEqual(postEndpoint.description, 'Create a new user');
|
|
58
|
+
});
|
|
59
|
+
});
|
package/dist/parsers/detector.js
CHANGED
|
@@ -10,6 +10,14 @@ export async function detectFormat(input) {
|
|
|
10
10
|
catch {
|
|
11
11
|
throw new ParseError(`Could not read file: ${input}`);
|
|
12
12
|
}
|
|
13
|
+
// Check file extension for API Blueprint
|
|
14
|
+
if (input.endsWith('.apib') || input.endsWith('.apiblueprint')) {
|
|
15
|
+
return { format: 'apib', content };
|
|
16
|
+
}
|
|
17
|
+
// Alternatively, check content for API Blueprint markers
|
|
18
|
+
if (content.trim().startsWith('FORMAT: 1A')) {
|
|
19
|
+
return { format: 'apib', content };
|
|
20
|
+
}
|
|
13
21
|
// Try parsing as JSON
|
|
14
22
|
let parsed;
|
|
15
23
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { detectFormat } from './detector.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
describe('Format Detector', () => {
|
|
7
|
+
it('should detect API Blueprint format from .apib file', async () => {
|
|
8
|
+
// Create temp .apib file
|
|
9
|
+
const tempFile = path.join(process.cwd(), 'test.apib');
|
|
10
|
+
await fs.writeFile(tempFile, 'FORMAT: 1A\n# My API\n## GET /users', 'utf-8');
|
|
11
|
+
try {
|
|
12
|
+
const result = await detectFormat(tempFile);
|
|
13
|
+
assert.strictEqual(result.format, 'apib');
|
|
14
|
+
assert.ok(result.content.includes('FORMAT: 1A'));
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await fs.unlink(tempFile);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|