crowgent-openapi 1.0.0 → 1.1.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/dist/cli.js +122 -32
- package/package.json +19 -5
package/dist/cli.js
CHANGED
|
@@ -1,57 +1,147 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
|
-
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
4
4
|
import { join, extname, relative } from 'path';
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
6
9
|
const EXTENSIONS = ['.ts', '.js', '.tsx', '.jsx', '.py', '.rb', '.go', '.java', '.kt', '.rs'];
|
|
7
10
|
const IGNORE = ['node_modules', '.git', 'dist', 'build', '__pycache__', '.next', 'coverage'];
|
|
8
11
|
program
|
|
9
12
|
.name('crowgent-openapi')
|
|
10
13
|
.description('Generate OpenAPI specs from your backend code using AI')
|
|
11
|
-
.argument('
|
|
12
|
-
.option('-o, --output <file>', 'Output file'
|
|
14
|
+
.argument('[directory]', 'Backend directory to scan')
|
|
15
|
+
.option('-o, --output <file>', 'Output file')
|
|
13
16
|
.option('-k, --api-key <key>', 'OpenAI API key (or set OPENAI_API_KEY)')
|
|
14
17
|
.option('-m, --model <model>', 'Model to use', 'gpt-4o-mini')
|
|
15
|
-
.option('--base-url <url>', 'API base URL'
|
|
18
|
+
.option('--base-url <url>', 'API base URL')
|
|
19
|
+
.option('--yes', 'Skip prompts and use defaults', false)
|
|
16
20
|
.action(async (directory, opts) => {
|
|
21
|
+
// Welcome
|
|
22
|
+
console.log();
|
|
23
|
+
p.intro(chalk.bgCyan.black(' 🐦 Crowgent OpenAPI Generator '));
|
|
17
24
|
const apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
|
|
25
|
+
// Check API key
|
|
18
26
|
if (!apiKey) {
|
|
19
|
-
|
|
27
|
+
p.log.error('Missing OpenAI API key');
|
|
28
|
+
p.log.info('Set OPENAI_API_KEY environment variable or use --api-key flag');
|
|
29
|
+
p.outro(chalk.red('Setup incomplete'));
|
|
20
30
|
process.exit(1);
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
// Ask for consent
|
|
33
|
+
if (!opts.yes) {
|
|
34
|
+
const consent = await p.confirm({
|
|
35
|
+
message: 'This will send your code to OpenAI to generate an API spec. Continue?',
|
|
36
|
+
initialValue: true,
|
|
37
|
+
});
|
|
38
|
+
if (p.isCancel(consent) || !consent) {
|
|
39
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Get directory
|
|
44
|
+
let targetDir = directory;
|
|
45
|
+
if (!targetDir && !opts.yes) {
|
|
46
|
+
const dirInput = await p.text({
|
|
47
|
+
message: 'Which directory contains your backend code?',
|
|
48
|
+
placeholder: './src or ./routes',
|
|
49
|
+
defaultValue: '.',
|
|
50
|
+
validate: (value) => {
|
|
51
|
+
if (!existsSync(value))
|
|
52
|
+
return 'Directory not found';
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (p.isCancel(dirInput)) {
|
|
56
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
targetDir = dirInput;
|
|
60
|
+
}
|
|
61
|
+
targetDir = targetDir || '.';
|
|
62
|
+
// Validate directory
|
|
63
|
+
if (!existsSync(targetDir)) {
|
|
64
|
+
p.log.error(`Directory not found: ${targetDir}`);
|
|
65
|
+
p.outro(chalk.red('Failed'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Get output file
|
|
69
|
+
let outputFile = opts.output;
|
|
70
|
+
if (!outputFile && !opts.yes) {
|
|
71
|
+
const outputInput = await p.text({
|
|
72
|
+
message: 'Where should we save the OpenAPI spec?',
|
|
73
|
+
placeholder: 'openapi.yaml',
|
|
74
|
+
defaultValue: 'openapi.yaml',
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(outputInput)) {
|
|
77
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
outputFile = outputInput;
|
|
81
|
+
}
|
|
82
|
+
outputFile = outputFile || 'openapi.yaml';
|
|
83
|
+
// Get base URL
|
|
84
|
+
let baseUrl = opts.baseUrl;
|
|
85
|
+
if (!baseUrl && !opts.yes) {
|
|
86
|
+
const urlInput = await p.text({
|
|
87
|
+
message: 'What is your API base URL?',
|
|
88
|
+
placeholder: 'http://localhost:3000',
|
|
89
|
+
defaultValue: 'http://localhost:3000',
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(urlInput)) {
|
|
92
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
baseUrl = urlInput;
|
|
96
|
+
}
|
|
97
|
+
baseUrl = baseUrl || 'http://localhost:3000';
|
|
98
|
+
// Scan files
|
|
99
|
+
const scanSpinner = ora('Scanning files...').start();
|
|
100
|
+
const files = collectFiles(targetDir);
|
|
101
|
+
scanSpinner.succeed(`Found ${files.length} source files`);
|
|
25
102
|
if (files.length === 0) {
|
|
26
|
-
|
|
103
|
+
p.log.error('No source files found');
|
|
104
|
+
p.outro(chalk.red('Failed'));
|
|
27
105
|
process.exit(1);
|
|
28
106
|
}
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
107
|
+
// Generate spec
|
|
108
|
+
const genSpinner = ora('Generating OpenAPI spec with AI...').start();
|
|
109
|
+
try {
|
|
110
|
+
const openai = new OpenAI({ apiKey });
|
|
111
|
+
const codeContext = files
|
|
112
|
+
.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
113
|
+
.join('\n\n');
|
|
114
|
+
const response = await openai.chat.completions.create({
|
|
115
|
+
model: opts.model,
|
|
116
|
+
messages: [{
|
|
117
|
+
role: 'system',
|
|
118
|
+
content: `You are an expert at generating OpenAPI 3.0 specifications.
|
|
39
119
|
Analyze the provided backend code and generate a complete, valid OpenAPI 3.0.3 YAML spec.
|
|
40
120
|
Include: all endpoints, HTTP methods, path/query parameters, request bodies, response schemas with properties.
|
|
41
121
|
Use descriptive summaries. Infer types from the code. Return ONLY valid YAML, no markdown or explanation.`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
}, {
|
|
123
|
+
role: 'user',
|
|
124
|
+
content: `Generate an OpenAPI spec for this backend code. Base URL: ${baseUrl}\n\n${codeContext}`
|
|
125
|
+
}],
|
|
126
|
+
max_tokens: 16000,
|
|
127
|
+
temperature: 0.2,
|
|
128
|
+
});
|
|
129
|
+
let yaml = response.choices[0].message.content || '';
|
|
130
|
+
yaml = yaml.replace(/^```ya?ml\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
131
|
+
writeFileSync(outputFile, yaml);
|
|
132
|
+
const tokens = response.usage?.total_tokens || 0;
|
|
133
|
+
const cost = (tokens * 0.00015 / 1000).toFixed(4);
|
|
134
|
+
genSpinner.succeed('OpenAPI spec generated');
|
|
135
|
+
p.log.success(`Saved to ${chalk.cyan(outputFile)}`);
|
|
136
|
+
p.log.info(`${tokens} tokens used (~$${cost})`);
|
|
137
|
+
p.outro(chalk.green('✨ Done!'));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
genSpinner.fail('Generation failed');
|
|
141
|
+
p.log.error(error.message || 'Unknown error');
|
|
142
|
+
p.outro(chalk.red('Failed'));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
55
145
|
});
|
|
56
146
|
function collectFiles(dir, root = dir) {
|
|
57
147
|
const files = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crowgent-openapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Generate OpenAPI specs from your backend code using AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
"dev": "tsx src/cli.ts",
|
|
13
13
|
"prepublishOnly": "npm run build"
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
16
19
|
"dependencies": {
|
|
20
|
+
"@clack/prompts": "^0.7.0",
|
|
21
|
+
"chalk": "^5.3.0",
|
|
17
22
|
"commander": "^12.0.0",
|
|
18
|
-
"openai": "^4.0.0"
|
|
23
|
+
"openai": "^4.0.0",
|
|
24
|
+
"ora": "^8.0.0"
|
|
19
25
|
},
|
|
20
26
|
"devDependencies": {
|
|
21
27
|
"@types/node": "^20.0.0",
|
|
@@ -25,7 +31,16 @@
|
|
|
25
31
|
"engines": {
|
|
26
32
|
"node": ">=18"
|
|
27
33
|
},
|
|
28
|
-
"keywords": [
|
|
34
|
+
"keywords": [
|
|
35
|
+
"openapi",
|
|
36
|
+
"swagger",
|
|
37
|
+
"api",
|
|
38
|
+
"generator",
|
|
39
|
+
"ai",
|
|
40
|
+
"llm",
|
|
41
|
+
"gpt",
|
|
42
|
+
"documentation"
|
|
43
|
+
],
|
|
29
44
|
"repository": {
|
|
30
45
|
"type": "git",
|
|
31
46
|
"url": "https://github.com/usecrow/crowgent-openapi"
|
|
@@ -33,4 +48,3 @@
|
|
|
33
48
|
"author": "Crow",
|
|
34
49
|
"license": "MIT"
|
|
35
50
|
}
|
|
36
|
-
|