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.
Files changed (2) hide show
  1. package/dist/cli.js +72 -25
  2. 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.info('Set OPENAI_API_KEY environment variable or use --api-key flag');
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: 'This will send your code to OpenAI to generate an API spec. Continue?',
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('Cancelled'));
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: 'Which directory contains your backend code?',
48
- placeholder: './src or ./routes',
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 'Directory not found';
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
- // Validate directory
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(`Directory not found: ${targetDir}`);
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 we save the OpenAPI spec?',
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
- // Get base URL
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 is your API base URL?',
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
- const scanSpinner = ora('Scanning files...').start();
100
- const files = collectFiles(targetDir);
101
- scanSpinner.succeed(`Found ${files.length} source 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
+ }];
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('Generating OpenAPI spec with AI...').start();
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
- p.log.success(`Saved to ${chalk.cyan(outputFile)}`);
136
- p.log.info(`${tokens} tokens used (~$${cost})`);
137
- p.outro(chalk.green('✨ Done!'));
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
- p.log.error(error.message || 'Unknown error');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crowgent-openapi",
3
- "version": "1.1.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": {