crowgent-openapi 1.0.0 → 1.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/dist/cli.js +170 -33
- package/package.json +19 -5
package/dist/cli.js
CHANGED
|
@@ -1,57 +1,194 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
|
-
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import { join, extname, relative } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
4
|
+
import { join, extname, relative, resolve } 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
|
+
console.log();
|
|
22
|
+
p.intro(chalk.bgCyan.black(' 🐦 Crowgent OpenAPI Generator '));
|
|
23
|
+
// Friendly explanation
|
|
24
|
+
p.note(`I'll help you generate an OpenAPI specification from your backend code.\n\n` +
|
|
25
|
+
`${chalk.dim('What is this for?')}\n` +
|
|
26
|
+
`An OpenAPI spec describes your API endpoints, parameters, and responses.\n` +
|
|
27
|
+
`Upload it to ${chalk.cyan('Crow')} to give your AI agent the ability to call your APIs.`, 'Welcome');
|
|
17
28
|
const apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
|
|
29
|
+
// Check API key
|
|
18
30
|
if (!apiKey) {
|
|
19
|
-
|
|
31
|
+
p.log.error('Missing OpenAI API key');
|
|
32
|
+
p.log.message(`${chalk.dim('To fix this, run:')}\n` +
|
|
33
|
+
`${chalk.cyan('export OPENAI_API_KEY="sk-..."')}\n\n` +
|
|
34
|
+
`${chalk.dim('Get your key at:')} ${chalk.underline('https://platform.openai.com/api-keys')}`);
|
|
35
|
+
p.outro(chalk.red('Setup incomplete'));
|
|
20
36
|
process.exit(1);
|
|
21
37
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
// Ask for consent with explanation
|
|
39
|
+
if (!opts.yes) {
|
|
40
|
+
const consent = await p.confirm({
|
|
41
|
+
message: `I'll scan your code and send it to OpenAI to generate the spec. This typically costs < $0.01. Continue?`,
|
|
42
|
+
initialValue: true,
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(consent) || !consent) {
|
|
45
|
+
p.outro(chalk.yellow('No problem! Run me again when you\'re ready.'));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Get directory with helpful guidance
|
|
50
|
+
let targetDir = directory;
|
|
51
|
+
if (!targetDir && !opts.yes) {
|
|
52
|
+
p.log.message(chalk.dim('Tip: Point me at the folder containing your API routes (e.g., ./src, ./routes, ./api)'));
|
|
53
|
+
const dirInput = await p.text({
|
|
54
|
+
message: 'Where is your backend code?',
|
|
55
|
+
placeholder: './backend or ./src/api',
|
|
56
|
+
defaultValue: '.',
|
|
57
|
+
validate: (value) => {
|
|
58
|
+
if (!existsSync(value))
|
|
59
|
+
return `Can't find "${value}" - check the path and try again`;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (p.isCancel(dirInput)) {
|
|
63
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
targetDir = dirInput;
|
|
67
|
+
}
|
|
68
|
+
targetDir = targetDir || '.';
|
|
69
|
+
// Handle single file vs directory
|
|
70
|
+
const targetPath = resolve(targetDir);
|
|
71
|
+
const stat = statSync(targetPath);
|
|
72
|
+
if (!stat.isDirectory()) {
|
|
73
|
+
// It's a file - use its parent directory but only include this file
|
|
74
|
+
p.log.warn(`"${targetDir}" is a file, not a directory. I'll analyze just this file.`);
|
|
75
|
+
}
|
|
76
|
+
// Validate
|
|
77
|
+
if (!existsSync(targetDir)) {
|
|
78
|
+
p.log.error(`Can't find "${targetDir}"`);
|
|
79
|
+
p.log.message(chalk.dim('Make sure the path exists and try again.'));
|
|
80
|
+
p.outro(chalk.red('Failed'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
// Get output file
|
|
84
|
+
let outputFile = opts.output;
|
|
85
|
+
if (!outputFile && !opts.yes) {
|
|
86
|
+
const outputInput = await p.text({
|
|
87
|
+
message: 'Where should I save the OpenAPI spec?',
|
|
88
|
+
placeholder: 'openapi.yaml',
|
|
89
|
+
defaultValue: 'openapi.yaml',
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(outputInput)) {
|
|
92
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
outputFile = outputInput;
|
|
96
|
+
}
|
|
97
|
+
outputFile = outputFile || 'openapi.yaml';
|
|
98
|
+
const outputPath = resolve(outputFile);
|
|
99
|
+
// Get base URL with explanation
|
|
100
|
+
let baseUrl = opts.baseUrl;
|
|
101
|
+
if (!baseUrl && !opts.yes) {
|
|
102
|
+
p.log.message(chalk.dim('This is the URL where your API is hosted (used in the spec\'s "servers" field)'));
|
|
103
|
+
const urlInput = await p.text({
|
|
104
|
+
message: 'What\'s your API base URL?',
|
|
105
|
+
placeholder: 'https://api.yourapp.com or http://localhost:3000',
|
|
106
|
+
defaultValue: 'http://localhost:3000',
|
|
107
|
+
});
|
|
108
|
+
if (p.isCancel(urlInput)) {
|
|
109
|
+
p.outro(chalk.yellow('Cancelled'));
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
baseUrl = urlInput;
|
|
113
|
+
}
|
|
114
|
+
baseUrl = baseUrl || 'http://localhost:3000';
|
|
115
|
+
// Scan files
|
|
116
|
+
p.log.step('Scanning your code...');
|
|
117
|
+
const files = stat.isDirectory()
|
|
118
|
+
? collectFiles(targetDir)
|
|
119
|
+
: [{
|
|
120
|
+
path: targetDir,
|
|
121
|
+
content: readFileSync(targetDir, 'utf-8'),
|
|
122
|
+
}];
|
|
25
123
|
if (files.length === 0) {
|
|
26
|
-
|
|
124
|
+
p.log.error('No source files found');
|
|
125
|
+
p.log.message(chalk.dim(`I look for these file types: ${EXTENSIONS.join(', ')}\n`) +
|
|
126
|
+
chalk.dim(`Make sure your backend code is in "${targetDir}"`));
|
|
127
|
+
p.outro(chalk.red('Failed'));
|
|
27
128
|
process.exit(1);
|
|
28
129
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
130
|
+
p.log.success(`Found ${files.length} source file${files.length > 1 ? 's' : ''}`);
|
|
131
|
+
// Show which files we found
|
|
132
|
+
if (files.length <= 5) {
|
|
133
|
+
files.forEach(f => p.log.message(chalk.dim(` • ${f.path}`)));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
files.slice(0, 3).forEach(f => p.log.message(chalk.dim(` • ${f.path}`)));
|
|
137
|
+
p.log.message(chalk.dim(` • ... and ${files.length - 3} more`));
|
|
138
|
+
}
|
|
139
|
+
// Generate spec
|
|
140
|
+
const genSpinner = ora('Analyzing code and generating OpenAPI spec...').start();
|
|
141
|
+
try {
|
|
142
|
+
const openai = new OpenAI({ apiKey });
|
|
143
|
+
const codeContext = files
|
|
144
|
+
.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
145
|
+
.join('\n\n');
|
|
146
|
+
const response = await openai.chat.completions.create({
|
|
147
|
+
model: opts.model,
|
|
148
|
+
messages: [{
|
|
149
|
+
role: 'system',
|
|
150
|
+
content: `You are an expert at generating OpenAPI 3.0 specifications.
|
|
39
151
|
Analyze the provided backend code and generate a complete, valid OpenAPI 3.0.3 YAML spec.
|
|
40
152
|
Include: all endpoints, HTTP methods, path/query parameters, request bodies, response schemas with properties.
|
|
41
153
|
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
|
-
|
|
154
|
+
}, {
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: `Generate an OpenAPI spec for this backend code. Base URL: ${baseUrl}\n\n${codeContext}`
|
|
157
|
+
}],
|
|
158
|
+
max_tokens: 16000,
|
|
159
|
+
temperature: 0.2,
|
|
160
|
+
});
|
|
161
|
+
let yaml = response.choices[0].message.content || '';
|
|
162
|
+
yaml = yaml.replace(/^```ya?ml\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
163
|
+
writeFileSync(outputFile, yaml);
|
|
164
|
+
const tokens = response.usage?.total_tokens || 0;
|
|
165
|
+
const cost = (tokens * 0.00015 / 1000).toFixed(4);
|
|
166
|
+
genSpinner.succeed('OpenAPI spec generated!');
|
|
167
|
+
// Success summary
|
|
168
|
+
console.log();
|
|
169
|
+
p.note(`${chalk.green('✓')} Saved to: ${chalk.cyan(outputPath)}\n` +
|
|
170
|
+
`${chalk.green('✓')} Tokens used: ${tokens} (~$${cost})\n\n` +
|
|
171
|
+
`${chalk.dim('Next step:')}\n` +
|
|
172
|
+
`Upload this file to ${chalk.cyan('Crow')} at Integration → OpenAPI\n` +
|
|
173
|
+
`to give your AI agent access to your API tools.`, 'Done!');
|
|
174
|
+
p.outro(chalk.green('Happy building! 🚀'));
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
genSpinner.fail('Generation failed');
|
|
178
|
+
if (error.message?.includes('401')) {
|
|
179
|
+
p.log.error('Invalid API key');
|
|
180
|
+
p.log.message(chalk.dim('Check that your OPENAI_API_KEY is correct and has credits.'));
|
|
181
|
+
}
|
|
182
|
+
else if (error.message?.includes('429')) {
|
|
183
|
+
p.log.error('Rate limited - too many requests');
|
|
184
|
+
p.log.message(chalk.dim('Wait a minute and try again.'));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
p.log.error(error.message || 'Unknown error');
|
|
188
|
+
}
|
|
189
|
+
p.outro(chalk.red('Failed'));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
55
192
|
});
|
|
56
193
|
function collectFiles(dir, root = dir) {
|
|
57
194
|
const files = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crowgent-openapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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
|
-
|