crowgent-openapi 1.1.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 +72 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
4
|
-
import { join, extname, relative } from 'path';
|
|
4
|
+
import { join, extname, relative, resolve } from 'path';
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import chalk from 'chalk';
|
|
@@ -18,38 +18,45 @@ program
|
|
|
18
18
|
.option('--base-url <url>', 'API base URL')
|
|
19
19
|
.option('--yes', 'Skip prompts and use defaults', false)
|
|
20
20
|
.action(async (directory, opts) => {
|
|
21
|
-
// Welcome
|
|
22
21
|
console.log();
|
|
23
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');
|
|
24
28
|
const apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
|
|
25
29
|
// Check API key
|
|
26
30
|
if (!apiKey) {
|
|
27
31
|
p.log.error('Missing OpenAI API key');
|
|
28
|
-
p.log.
|
|
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')}`);
|
|
29
35
|
p.outro(chalk.red('Setup incomplete'));
|
|
30
36
|
process.exit(1);
|
|
31
37
|
}
|
|
32
|
-
// Ask for consent
|
|
38
|
+
// Ask for consent with explanation
|
|
33
39
|
if (!opts.yes) {
|
|
34
40
|
const consent = await p.confirm({
|
|
35
|
-
message: '
|
|
41
|
+
message: `I'll scan your code and send it to OpenAI to generate the spec. This typically costs < $0.01. Continue?`,
|
|
36
42
|
initialValue: true,
|
|
37
43
|
});
|
|
38
44
|
if (p.isCancel(consent) || !consent) {
|
|
39
|
-
p.outro(chalk.yellow('
|
|
45
|
+
p.outro(chalk.yellow('No problem! Run me again when you\'re ready.'));
|
|
40
46
|
process.exit(0);
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
|
-
// Get directory
|
|
49
|
+
// Get directory with helpful guidance
|
|
44
50
|
let targetDir = directory;
|
|
45
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)'));
|
|
46
53
|
const dirInput = await p.text({
|
|
47
|
-
message: '
|
|
48
|
-
placeholder: './
|
|
54
|
+
message: 'Where is your backend code?',
|
|
55
|
+
placeholder: './backend or ./src/api',
|
|
49
56
|
defaultValue: '.',
|
|
50
57
|
validate: (value) => {
|
|
51
58
|
if (!existsSync(value))
|
|
52
|
-
return '
|
|
59
|
+
return `Can't find "${value}" - check the path and try again`;
|
|
53
60
|
},
|
|
54
61
|
});
|
|
55
62
|
if (p.isCancel(dirInput)) {
|
|
@@ -59,9 +66,17 @@ program
|
|
|
59
66
|
targetDir = dirInput;
|
|
60
67
|
}
|
|
61
68
|
targetDir = targetDir || '.';
|
|
62
|
-
//
|
|
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
|
|
63
77
|
if (!existsSync(targetDir)) {
|
|
64
|
-
p.log.error(`
|
|
78
|
+
p.log.error(`Can't find "${targetDir}"`);
|
|
79
|
+
p.log.message(chalk.dim('Make sure the path exists and try again.'));
|
|
65
80
|
p.outro(chalk.red('Failed'));
|
|
66
81
|
process.exit(1);
|
|
67
82
|
}
|
|
@@ -69,7 +84,7 @@ program
|
|
|
69
84
|
let outputFile = opts.output;
|
|
70
85
|
if (!outputFile && !opts.yes) {
|
|
71
86
|
const outputInput = await p.text({
|
|
72
|
-
message: 'Where should
|
|
87
|
+
message: 'Where should I save the OpenAPI spec?',
|
|
73
88
|
placeholder: 'openapi.yaml',
|
|
74
89
|
defaultValue: 'openapi.yaml',
|
|
75
90
|
});
|
|
@@ -80,12 +95,14 @@ program
|
|
|
80
95
|
outputFile = outputInput;
|
|
81
96
|
}
|
|
82
97
|
outputFile = outputFile || 'openapi.yaml';
|
|
83
|
-
|
|
98
|
+
const outputPath = resolve(outputFile);
|
|
99
|
+
// Get base URL with explanation
|
|
84
100
|
let baseUrl = opts.baseUrl;
|
|
85
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)'));
|
|
86
103
|
const urlInput = await p.text({
|
|
87
|
-
message: 'What
|
|
88
|
-
placeholder: 'http://localhost:3000',
|
|
104
|
+
message: 'What\'s your API base URL?',
|
|
105
|
+
placeholder: 'https://api.yourapp.com or http://localhost:3000',
|
|
89
106
|
defaultValue: 'http://localhost:3000',
|
|
90
107
|
});
|
|
91
108
|
if (p.isCancel(urlInput)) {
|
|
@@ -96,16 +113,31 @@ program
|
|
|
96
113
|
}
|
|
97
114
|
baseUrl = baseUrl || 'http://localhost:3000';
|
|
98
115
|
// Scan files
|
|
99
|
-
|
|
100
|
-
const files =
|
|
101
|
-
|
|
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
|
+
}];
|
|
102
123
|
if (files.length === 0) {
|
|
103
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}"`));
|
|
104
127
|
p.outro(chalk.red('Failed'));
|
|
105
128
|
process.exit(1);
|
|
106
129
|
}
|
|
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
|
+
}
|
|
107
139
|
// Generate spec
|
|
108
|
-
const genSpinner = ora('
|
|
140
|
+
const genSpinner = ora('Analyzing code and generating OpenAPI spec...').start();
|
|
109
141
|
try {
|
|
110
142
|
const openai = new OpenAI({ apiKey });
|
|
111
143
|
const codeContext = files
|
|
@@ -131,14 +163,29 @@ Use descriptive summaries. Infer types from the code. Return ONLY valid YAML, no
|
|
|
131
163
|
writeFileSync(outputFile, yaml);
|
|
132
164
|
const tokens = response.usage?.total_tokens || 0;
|
|
133
165
|
const cost = (tokens * 0.00015 / 1000).toFixed(4);
|
|
134
|
-
genSpinner.succeed('OpenAPI spec generated');
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
p.
|
|
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! 🚀'));
|
|
138
175
|
}
|
|
139
176
|
catch (error) {
|
|
140
177
|
genSpinner.fail('Generation failed');
|
|
141
|
-
|
|
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
|
+
}
|
|
142
189
|
p.outro(chalk.red('Failed'));
|
|
143
190
|
process.exit(1);
|
|
144
191
|
}
|