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.
Files changed (2) hide show
  1. package/dist/cli.js +170 -33
  2. 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('<directory>', 'Backend directory to scan')
12
- .option('-o, --output <file>', 'Output file', 'openapi.yaml')
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', 'http://localhost:3000')
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
- console.error('Missing API key. Set OPENAI_API_KEY or use --api-key');
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
- console.log('📂 Scanning files...');
23
- const files = collectFiles(directory);
24
- console.log(` Found ${files.length} source files`);
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
- console.error('No source files found');
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
- console.log('🤖 Generating OpenAPI spec...');
30
- const openai = new OpenAI({ apiKey });
31
- const codeContext = files
32
- .map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
33
- .join('\n\n');
34
- const response = await openai.chat.completions.create({
35
- model: opts.model,
36
- messages: [{
37
- role: 'system',
38
- content: `You are an expert at generating OpenAPI 3.0 specifications.
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
- role: 'user',
44
- content: `Generate an OpenAPI spec for this backend code. Base URL: ${opts.baseUrl}\n\n${codeContext}`
45
- }],
46
- max_tokens: 16000,
47
- temperature: 0.2,
48
- });
49
- let yaml = response.choices[0].message.content || '';
50
- // Strip markdown fences if present
51
- yaml = yaml.replace(/^```ya?ml\n?/i, '').replace(/\n?```$/i, '').trim();
52
- writeFileSync(opts.output, yaml);
53
- console.log(`✅ Saved to ${opts.output}`);
54
- console.log(` ${response.usage?.total_tokens || '?'} tokens used`);
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.0.0",
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": ["dist", "README.md"],
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": ["openapi", "swagger", "api", "generator", "ai", "llm", "gpt", "documentation"],
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
-