post-api-sync 0.1.1

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.
@@ -0,0 +1,35 @@
1
+ const { extractNestJsEndpoints } = require('./nestjs');
2
+ const { extractExpressEndpoints } = require('./express');
3
+ const { extractHonoEndpoints } = require('./hono');
4
+
5
+ async function extractEndpoints(filePath, framework) {
6
+ if (framework === 'nestjs') return extractNestJsEndpoints(filePath);
7
+ if (framework === 'express') return extractExpressEndpoints(filePath);
8
+ if (framework === 'hono') return [];
9
+ // auto: try NestJS then Express
10
+ const nest = await extractNestJsEndpoints(filePath);
11
+ const exp = await extractExpressEndpoints(filePath);
12
+ const map = new Map();
13
+ for (const e of [...nest, ...exp]) map.set(e.key, e);
14
+ return Array.from(map.values());
15
+ }
16
+
17
+ async function extractAllEndpoints(files, framework) {
18
+ if (framework === 'hono') {
19
+ return extractHonoEndpoints(files);
20
+ }
21
+
22
+ if (framework === 'auto') {
23
+ const hono = await extractHonoEndpoints(files);
24
+ if (hono.length) return hono;
25
+ }
26
+
27
+ const endpoints = [];
28
+ for (const file of files) {
29
+ const extracted = await extractEndpoints(file, framework);
30
+ endpoints.push(...extracted);
31
+ }
32
+ return endpoints;
33
+ }
34
+
35
+ module.exports = { extractEndpoints, extractAllEndpoints };
@@ -0,0 +1,410 @@
1
+ const traverse = require('@babel/traverse').default;
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { parseFile } = require('./ast');
5
+ const { joinPaths, normalizePath, toKey } = require('../utils');
6
+
7
+ const HTTP_DECORATORS = {
8
+ Get: 'GET',
9
+ Post: 'POST',
10
+ Put: 'PUT',
11
+ Patch: 'PATCH',
12
+ Delete: 'DELETE',
13
+ Options: 'OPTIONS',
14
+ Head: 'HEAD'
15
+ };
16
+
17
+ const OPTIONAL_DECORATORS = new Set(['IsOptional', 'ApiPropertyOptional']);
18
+ const TYPE_DECORATORS = new Map([
19
+ ['IsString', 'string'],
20
+ ['IsEmail', 'string'],
21
+ ['IsUUID', 'string'],
22
+ ['IsInt', 'number'],
23
+ ['IsNumber', 'number'],
24
+ ['Min', 'number'],
25
+ ['Max', 'number'],
26
+ ['IsBoolean', 'boolean']
27
+ ]);
28
+
29
+ function getDecoratorName(dec) {
30
+ const expr = dec.expression;
31
+ if (!expr) return null;
32
+ if (expr.type === 'CallExpression') {
33
+ if (expr.callee.type === 'Identifier') return expr.callee.name;
34
+ }
35
+ if (expr.type === 'Identifier') return expr.name;
36
+ return null;
37
+ }
38
+
39
+ function getDecoratorArgs(dec) {
40
+ const expr = dec.expression;
41
+ if (expr && expr.type === 'CallExpression') return expr.arguments || [];
42
+ return [];
43
+ }
44
+
45
+ function getStringArg(dec) {
46
+ const args = getDecoratorArgs(dec);
47
+ const first = args[0];
48
+ if (!first) return '';
49
+ if (first.type === 'StringLiteral') return first.value;
50
+ if (first.type === 'TemplateLiteral' && first.quasis.length === 1) {
51
+ return first.quasis[0].value.cooked || '';
52
+ }
53
+ return '';
54
+ }
55
+
56
+ function getTagsFromDecorator(dec) {
57
+ const args = getDecoratorArgs(dec);
58
+ const tags = [];
59
+ for (const arg of args) {
60
+ if (arg.type === 'StringLiteral') tags.push(arg.value);
61
+ }
62
+ return tags;
63
+ }
64
+
65
+ function getSummaryFromDecorator(dec) {
66
+ const args = getDecoratorArgs(dec);
67
+ const first = args[0];
68
+ if (!first || first.type !== 'ObjectExpression') return null;
69
+ for (const prop of first.properties) {
70
+ if (prop.type !== 'ObjectProperty') continue;
71
+ if (prop.key.type === 'Identifier' && prop.key.name === 'summary') {
72
+ if (prop.value.type === 'StringLiteral') return prop.value.value;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function getPropertyName(node) {
79
+ if (!node) return null;
80
+ if (node.type === 'Identifier') return node.name;
81
+ if (node.type === 'StringLiteral') return node.value;
82
+ return null;
83
+ }
84
+
85
+ function getDecoratorObjectArg(dec) {
86
+ const args = getDecoratorArgs(dec);
87
+ const first = args[0];
88
+ if (first && first.type === 'ObjectExpression') return first;
89
+ return null;
90
+ }
91
+
92
+ function getPropMetaFromDecorators(decorators) {
93
+ let optional = false;
94
+ let example;
95
+ let overrideType;
96
+
97
+ for (const dec of decorators || []) {
98
+ const name = getDecoratorName(dec);
99
+ if (!name) continue;
100
+ if (OPTIONAL_DECORATORS.has(name)) optional = true;
101
+ if (TYPE_DECORATORS.has(name)) overrideType = TYPE_DECORATORS.get(name);
102
+
103
+ if (name === 'ApiProperty' || name === 'ApiPropertyOptional') {
104
+ const obj = getDecoratorObjectArg(dec);
105
+ if (name === 'ApiPropertyOptional') optional = true;
106
+ if (obj) {
107
+ for (const prop of obj.properties) {
108
+ if (prop.type !== 'ObjectProperty') continue;
109
+ if (prop.key.type !== 'Identifier') continue;
110
+ if (prop.key.name === 'required' && prop.value.type === 'BooleanLiteral') {
111
+ if (prop.value.value === false) optional = true;
112
+ }
113
+ if (prop.key.name === 'example') {
114
+ if (prop.value.type === 'StringLiteral' || prop.value.type === 'NumericLiteral' || prop.value.type === 'BooleanLiteral') {
115
+ example = prop.value.value;
116
+ }
117
+ }
118
+ if (prop.key.name === 'type' && prop.value.type === 'Identifier') {
119
+ overrideType = prop.value.name.toLowerCase();
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ return { optional, example, overrideType };
127
+ }
128
+
129
+ function collectDtoSchemasFromAst(ast) {
130
+ const dtoMap = new Map();
131
+
132
+ traverse(ast, {
133
+ ClassDeclaration(path) {
134
+ const classNode = path.node;
135
+ if (!classNode.id || !classNode.id.name) return;
136
+ const className = classNode.id.name;
137
+ const props = [];
138
+
139
+ for (const member of classNode.body.body || []) {
140
+ if (member.type !== 'ClassProperty') continue;
141
+ const name = getPropertyName(member.key);
142
+ if (!name) continue;
143
+ const meta = getPropMetaFromDecorators(member.decorators || []);
144
+ const optional = member.optional === true || meta.optional;
145
+ const typeNode = member.typeAnnotation ? member.typeAnnotation.typeAnnotation : null;
146
+ props.push({ name, optional, typeNode, example: meta.example, overrideType: meta.overrideType });
147
+ }
148
+
149
+ if (props.length) dtoMap.set(className, { name: className, props });
150
+ }
151
+ });
152
+
153
+ return dtoMap;
154
+ }
155
+
156
+ function collectImportMap(ast, filePath) {
157
+ const map = new Map();
158
+ const baseDir = path.dirname(filePath);
159
+
160
+ traverse(ast, {
161
+ ImportDeclaration(path) {
162
+ const node = path.node;
163
+ const source = node.source.value;
164
+ if (!source || !source.startsWith('.')) return;
165
+ const abs = resolveImportFile(baseDir, source);
166
+ if (!abs) return;
167
+ for (const spec of node.specifiers || []) {
168
+ if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
169
+ map.set(spec.local.name, abs);
170
+ }
171
+ }
172
+ }
173
+ });
174
+
175
+ return map;
176
+ }
177
+
178
+ function resolveImportFile(baseDir, source) {
179
+ const candidates = [
180
+ path.resolve(baseDir, `${source}.ts`),
181
+ path.resolve(baseDir, `${source}.tsx`),
182
+ path.resolve(baseDir, `${source}.js`),
183
+ path.resolve(baseDir, `${source}.jsx`),
184
+ path.resolve(baseDir, source, 'index.ts'),
185
+ path.resolve(baseDir, source, 'index.js')
186
+ ];
187
+ for (const candidate of candidates) {
188
+ if (fs.existsSync(candidate)) return candidate;
189
+ }
190
+ return null;
191
+ }
192
+
193
+ async function collectDtoSchemas(ast, filePath) {
194
+ const dtoMap = collectDtoSchemasFromAst(ast);
195
+ const importMap = collectImportMap(ast, filePath);
196
+ const visited = new Set([filePath]);
197
+
198
+ for (const [, importFile] of importMap.entries()) {
199
+ if (visited.has(importFile)) continue;
200
+ visited.add(importFile);
201
+ try {
202
+ const importedAst = await parseFile(importFile);
203
+ const importedDtos = collectDtoSchemasFromAst(importedAst);
204
+ for (const [name, dto] of importedDtos.entries()) {
205
+ if (!dtoMap.has(name)) dtoMap.set(name, dto);
206
+ }
207
+ } catch (err) {
208
+ // Ignore missing or unparsable imports
209
+ }
210
+ }
211
+
212
+ return dtoMap;
213
+ }
214
+
215
+ function schemaFromTypeNode(typeNode, dtoMap, depth = 0, memo = new Map(), meta = {}) {
216
+ if (!typeNode) {
217
+ if (meta.overrideType) return { type: meta.overrideType, example: meta.example };
218
+ return { type: 'object', example: meta.example };
219
+ }
220
+
221
+ if (typeNode.type === 'TSStringKeyword') return { type: 'string', example: meta.example };
222
+ if (typeNode.type === 'TSNumberKeyword') return { type: 'number', example: meta.example };
223
+ if (typeNode.type === 'TSBooleanKeyword') return { type: 'boolean', example: meta.example };
224
+
225
+ if (typeNode.type === 'TSArrayType') {
226
+ return {
227
+ type: 'array',
228
+ items: schemaFromTypeNode(typeNode.elementType, dtoMap, depth + 1, memo)
229
+ };
230
+ }
231
+
232
+ if (typeNode.type === 'TSTypeReference') {
233
+ if (typeNode.typeName.type === 'Identifier') {
234
+ const name = typeNode.typeName.name;
235
+ if (name === 'Array' && typeNode.typeParameters && typeNode.typeParameters.params.length === 1) {
236
+ return {
237
+ type: 'array',
238
+ items: schemaFromTypeNode(typeNode.typeParameters.params[0], dtoMap, depth + 1, memo)
239
+ };
240
+ }
241
+ if (dtoMap.has(name) && depth < 2) {
242
+ return buildSchemaForDto(name, dtoMap, depth + 1, memo);
243
+ }
244
+ }
245
+ }
246
+
247
+ if (typeNode.type === 'TSTypeLiteral') {
248
+ const properties = {};
249
+ for (const member of typeNode.members || []) {
250
+ if (member.type !== 'TSPropertySignature') continue;
251
+ const name = getPropertyName(member.key);
252
+ if (!name) continue;
253
+ const propSchema = schemaFromTypeNode(member.typeAnnotation?.typeAnnotation, dtoMap, depth + 1, memo);
254
+ properties[name] = propSchema;
255
+ }
256
+ return { type: 'object', properties };
257
+ }
258
+
259
+ if (meta.overrideType) return { type: meta.overrideType, example: meta.example };
260
+ return { type: 'object', example: meta.example };
261
+ }
262
+
263
+ function buildSchemaForDto(dtoName, dtoMap, depth = 0, memo = new Map()) {
264
+ if (memo.has(dtoName)) return memo.get(dtoName);
265
+ const dto = dtoMap.get(dtoName);
266
+ if (!dto) return { type: 'object' };
267
+
268
+ const schema = { type: 'object', properties: {} };
269
+ memo.set(dtoName, schema);
270
+
271
+ const required = [];
272
+ for (const prop of dto.props) {
273
+ const propSchema = schemaFromTypeNode(prop.typeNode, dtoMap, depth + 1, memo, prop);
274
+ schema.properties[prop.name] = propSchema;
275
+ if (!prop.optional) required.push(prop.name);
276
+ }
277
+ if (required.length) schema.required = required;
278
+
279
+ return schema;
280
+ }
281
+
282
+ function getParamDecorators(paramNode, dtoSchemas) {
283
+ const decoratorTarget = paramNode.decorators ? paramNode : paramNode.left || paramNode;
284
+ const decorators = decoratorTarget.decorators || [];
285
+ const params = [];
286
+ const query = [];
287
+ let body = null;
288
+
289
+ for (const dec of decorators) {
290
+ const name = getDecoratorName(dec);
291
+ if (!name) continue;
292
+ const arg = getStringArg(dec);
293
+ if (name === 'Param') {
294
+ if (arg) params.push({ name: arg, required: true });
295
+ }
296
+ if (name === 'Query') {
297
+ if (arg) {
298
+ query.push({ name: arg, required: false });
299
+ } else {
300
+ // @Query() query: SearchDto
301
+ // Check if there is a type annotation that maps to a DTO
302
+ const typeNode = decoratorTarget.typeAnnotation ? decoratorTarget.typeAnnotation.typeAnnotation : null;
303
+ if (typeNode && typeNode.type === 'TSTypeReference' && typeNode.typeName.type === 'Identifier') {
304
+ const dtoName = typeNode.typeName.name;
305
+ // Build schema to extract properties
306
+ // Use a temporary memo to avoid polluting global state if we were caching heavily, but here it's fine
307
+ const schema = buildSchemaForDto(dtoName, dtoSchemas);
308
+ if (schema && schema.properties) {
309
+ for (const [key, val] of Object.entries(schema.properties)) {
310
+ // Determine if required based on schema.required array
311
+ const isRequired = schema.required && schema.required.includes(key);
312
+ query.push({ name: key, required: !!isRequired });
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ if (name === 'Body') {
319
+ const typeNode = decoratorTarget.typeAnnotation ? decoratorTarget.typeAnnotation.typeAnnotation : null;
320
+ body = schemaFromTypeNode(typeNode, dtoSchemas);
321
+ }
322
+ }
323
+
324
+ return { params, query, body };
325
+ }
326
+
327
+ async function extractNestJsEndpoints(filePath) {
328
+ const ast = await parseFile(filePath);
329
+ const endpoints = [];
330
+ const dtoSchemas = await collectDtoSchemas(ast, filePath);
331
+
332
+ traverse(ast, {
333
+ ClassDeclaration(path) {
334
+ const classNode = path.node;
335
+ const decorators = classNode.decorators || [];
336
+ let basePath = '';
337
+ let tags = [];
338
+ for (const dec of decorators) {
339
+ const name = getDecoratorName(dec);
340
+ if (name === 'Controller') {
341
+ basePath = getStringArg(dec);
342
+ }
343
+ if (name === 'ApiTags') {
344
+ tags = tags.concat(getTagsFromDecorator(dec));
345
+ }
346
+ }
347
+
348
+ const body = classNode.body.body || [];
349
+ for (const memberNode of body) {
350
+ const isMethod = memberNode.type === 'ClassMethod';
351
+ const isPropertyWithFn =
352
+ memberNode.type === 'ClassProperty' &&
353
+ memberNode.value &&
354
+ (memberNode.value.type === 'ArrowFunctionExpression' || memberNode.value.type === 'FunctionExpression');
355
+
356
+ if (!isMethod && !isPropertyWithFn) continue;
357
+
358
+ const methodDecorators = memberNode.decorators || [];
359
+ let httpMethod = null;
360
+ let methodPath = '';
361
+ let description = null;
362
+
363
+ for (const dec of methodDecorators) {
364
+ const name = getDecoratorName(dec);
365
+ if (HTTP_DECORATORS[name]) {
366
+ httpMethod = HTTP_DECORATORS[name];
367
+ methodPath = getStringArg(dec);
368
+ }
369
+ if (name === 'ApiOperation') {
370
+ description = getSummaryFromDecorator(dec) || description;
371
+ }
372
+ }
373
+
374
+ if (!httpMethod) continue;
375
+
376
+ const fullPath = normalizePath(joinPaths(basePath, methodPath));
377
+ const params = [];
378
+ const query = [];
379
+ let bodySchema = null;
380
+
381
+ const paramList = isMethod ? memberNode.params || [] : memberNode.value.params || [];
382
+ for (const paramNode of paramList) {
383
+ const res = getParamDecorators(paramNode, dtoSchemas);
384
+ params.push(...res.params);
385
+ query.push(...res.query);
386
+ if (res.body) bodySchema = res.body;
387
+ }
388
+
389
+ const endpoint = {
390
+ method: httpMethod,
391
+ path: fullPath,
392
+ description: description || `${httpMethod} ${fullPath}`,
393
+ tags: tags.length ? tags : undefined,
394
+ parameters: {
395
+ path: params.length ? params : undefined,
396
+ query: query.length ? query : undefined,
397
+ body: bodySchema || undefined
398
+ },
399
+ filePath,
400
+ key: toKey(httpMethod, fullPath)
401
+ };
402
+ endpoints.push(endpoint);
403
+ }
404
+ }
405
+ });
406
+
407
+ return endpoints;
408
+ }
409
+
410
+ module.exports = { extractNestJsEndpoints };
@@ -0,0 +1,119 @@
1
+ const traverse = require('@babel/traverse').default;
2
+
3
+ function getPropertyName(node) {
4
+ if (!node) return null;
5
+ if (node.type === 'Identifier') return node.name;
6
+ if (node.type === 'StringLiteral') return node.value;
7
+ return null;
8
+ }
9
+
10
+ function getStringLiteral(node) {
11
+ if (!node) return '';
12
+ if (node.type === 'StringLiteral') return node.value;
13
+ return '';
14
+ }
15
+
16
+ function getObjectProperty(node, keyName) {
17
+ if (!node || node.type !== 'ObjectExpression') return null;
18
+ for (const prop of node.properties) {
19
+ if (prop.type !== 'ObjectProperty') continue;
20
+ const name = getPropertyName(prop.key);
21
+ if (name === keyName) return prop.value;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function resolveZodSchema(node, schemaDefs, depth = 0) {
27
+ if (!node || depth > 10) return { type: 'string' }; // Prevent infinite recursion
28
+
29
+ // Handle identifier references (e.g. userSchema)
30
+ if (node.type === 'Identifier') {
31
+ if (schemaDefs.has(node.name)) {
32
+ // Recursively resolve the stored definition
33
+ return resolveZodSchema(schemaDefs.get(node.name), schemaDefs, depth + 1);
34
+ }
35
+ return { type: 'object' }; // Fallback
36
+ }
37
+
38
+ // Handle call chains: z.string().min(3).openapi({...})
39
+ if (node.type === 'CallExpression') {
40
+ const { callee } = node;
41
+
42
+ if (callee.type === 'MemberExpression') {
43
+ const methodName = callee.property.name;
44
+
45
+ // Primitives call usage: z.string(), z.number(), z.boolean()
46
+ if (methodName === 'string') return { type: 'string' };
47
+ if (methodName === 'number') return { type: 'number' };
48
+ if (methodName === 'boolean') return { type: 'boolean' };
49
+
50
+ // z.object({...})
51
+ if (methodName === 'object') {
52
+ const arg = node.arguments[0];
53
+ if (arg && arg.type === 'ObjectExpression') {
54
+ const properties = {};
55
+ const required = [];
56
+ for (const prop of arg.properties) {
57
+ if (prop.type !== 'ObjectProperty') continue;
58
+ const name = getPropertyName(prop.key);
59
+ if (!name) continue;
60
+ const propSchema = resolveZodSchema(prop.value, schemaDefs, depth + 1);
61
+ properties[name] = propSchema;
62
+ if (!propSchema.optional) required.push(name);
63
+ }
64
+ return { type: 'object', properties, required: required.length ? required : undefined };
65
+ }
66
+ return { type: 'object' };
67
+ }
68
+
69
+ // z.array(schema)
70
+ if (methodName === 'array') {
71
+ const arg = node.arguments[0];
72
+ return {
73
+ type: 'array',
74
+ items: resolveZodSchema(arg, schemaDefs, depth + 1)
75
+ };
76
+ }
77
+
78
+ // Check for .openapi({ example: ... })
79
+ if (methodName === 'openapi') {
80
+ const baseSchema = resolveZodSchema(callee.object, schemaDefs, depth + 1);
81
+ const arg = node.arguments[0];
82
+ const example = getObjectProperty(arg, 'example');
83
+
84
+ if (example) {
85
+ if (example.type === 'StringLiteral') baseSchema.example = example.value;
86
+ if (example.type === 'NumericLiteral') baseSchema.example = example.value;
87
+ if (example.type === 'BooleanLiteral') baseSchema.example = example.value;
88
+ }
89
+ return baseSchema;
90
+ }
91
+
92
+ // Check for .optional()
93
+ if (methodName === 'optional') {
94
+ const base = resolveZodSchema(callee.object, schemaDefs, depth + 1);
95
+ base.optional = true;
96
+ return base;
97
+ }
98
+
99
+ // Generic fallback for other chaining methods (.min, .max, .email, etc.)
100
+ // We recurse on the object being chained on.
101
+ return resolveZodSchema(callee.object, schemaDefs, depth + 1);
102
+ }
103
+ }
104
+
105
+ // Handle z.string, z.number access (MemberExpression not called yet, or part of chain root)
106
+ if (node.type === 'MemberExpression') {
107
+ const name = node.property.name;
108
+ if (name === 'string') return { type: 'string' };
109
+ if (name === 'number') return { type: 'number' };
110
+ if (name === 'boolean') return { type: 'boolean' };
111
+
112
+ // Recurse up if it's something like z.string
113
+ if (node.object) return resolveZodSchema(node.object, schemaDefs, depth + 1);
114
+ }
115
+
116
+ return { type: 'string' };
117
+ }
118
+
119
+ module.exports = { resolveZodSchema };
package/src/init.js ADDED
@@ -0,0 +1,126 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+ const { DEFAULT_CONFIG, resolveConfigPath } = require('./config');
5
+ const { info, warn, success } = require('./log');
6
+
7
+ function getPrompt() {
8
+ return inquirer.prompt || (inquirer.default && inquirer.default.prompt);
9
+ }
10
+
11
+ async function initConfig({ baseDir } = {}) {
12
+ const prompt = getPrompt();
13
+ if (!prompt) {
14
+ throw new Error('Inquirer prompt not available. Please use Node 18+ and reinstall dependencies.');
15
+ }
16
+
17
+ const resolved = await resolveConfigPath(undefined, baseDir);
18
+ const targetPath = resolved.path;
19
+ if (await fs.pathExists(targetPath)) {
20
+ warn(`Config already exists at ${targetPath}`);
21
+ const { overwrite } = await prompt([
22
+ {
23
+ type: 'confirm',
24
+ name: 'overwrite',
25
+ message: 'Overwrite existing config?',
26
+ default: false
27
+ }
28
+ ]);
29
+ if (!overwrite) return;
30
+ }
31
+
32
+ const answers = await prompt([
33
+ {
34
+ type: 'list',
35
+ name: 'framework',
36
+ message: 'Which framework do you use?',
37
+ choices: [
38
+ { name: 'Auto-detect', value: 'auto' },
39
+ { name: 'NestJS', value: 'nestjs' },
40
+ { name: 'Express', value: 'express' },
41
+ { name: 'Hono', value: 'hono' }
42
+ ],
43
+ default: 'auto'
44
+ },
45
+ {
46
+ type: 'input',
47
+ name: 'include',
48
+ message: 'Glob(s) for route/controller files (comma-separated):',
49
+ default: DEFAULT_CONFIG.sources.include.join(',')
50
+ },
51
+ {
52
+ type: 'input',
53
+ name: 'exclude',
54
+ message: 'Glob(s) to exclude (comma-separated):',
55
+ default: DEFAULT_CONFIG.sources.exclude.join(',')
56
+ },
57
+ {
58
+ type: 'input',
59
+ name: 'baseUrl',
60
+ message: 'Base URL for collections:',
61
+ default: DEFAULT_CONFIG.sources.baseUrl
62
+ },
63
+ {
64
+ type: 'confirm',
65
+ name: 'postmanEnabled',
66
+ message: 'Generate Postman collection?',
67
+ default: true
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'postmanPath',
72
+ message: 'Postman output path:',
73
+ default: DEFAULT_CONFIG.output.postman.outputPath,
74
+ when: (a) => a.postmanEnabled
75
+ },
76
+ {
77
+ type: 'confirm',
78
+ name: 'insomniaEnabled',
79
+ message: 'Generate Insomnia collection?',
80
+ default: true
81
+ },
82
+ {
83
+ type: 'input',
84
+ name: 'insomniaPath',
85
+ message: 'Insomnia output path:',
86
+ default: DEFAULT_CONFIG.output.insomnia.outputPath,
87
+ when: (a) => a.insomniaEnabled
88
+ }
89
+ ]);
90
+
91
+ const config = {
92
+ framework: answers.framework,
93
+ sources: {
94
+ include: answers.include.split(',').map((s) => s.trim()).filter(Boolean),
95
+ exclude: answers.exclude.split(',').map((s) => s.trim()).filter(Boolean),
96
+ baseUrl: answers.baseUrl
97
+ },
98
+ output: {
99
+ postman: {
100
+ enabled: !!answers.postmanEnabled,
101
+ outputPath: answers.postmanPath
102
+ },
103
+ insomnia: {
104
+ enabled: !!answers.insomniaEnabled,
105
+ outputPath: answers.insomniaPath
106
+ }
107
+ },
108
+ watch: {
109
+ enabled: true,
110
+ debounce: DEFAULT_CONFIG.watch.debounce
111
+ },
112
+ merge: {
113
+ markDeprecated: true
114
+ },
115
+ organization: {
116
+ groupBy: 'tags'
117
+ }
118
+ };
119
+
120
+ const contents = `module.exports = ${JSON.stringify(config, null, 2)};\n`;
121
+ await fs.outputFile(targetPath, contents);
122
+ info(`Saved config to ${targetPath}`);
123
+ success('Done');
124
+ }
125
+
126
+ module.exports = { initConfig };