codeflow-hook 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 (3) hide show
  1. package/README.md +251 -248
  2. package/bin/codeflow-hook.js +594 -352
  3. package/package.json +37 -25
@@ -1,352 +1,594 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import axios from 'axios';
7
- import fs from 'fs';
8
- import path from 'path';
9
- import { execSync } from 'child_process';
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name('codeflow-hook')
15
- .description('Interactive CI/CD simulator and AI-powered code reviewer')
16
- .version('1.0.0');
17
-
18
- // Configure AI provider settings
19
- program
20
- .command('config')
21
- .description('Configure AI provider settings')
22
- .option('-p, --provider <provider>', 'AI provider (gemini, openai, claude)', 'gemini')
23
- .option('-k, --key <key>', 'API key for the chosen provider')
24
- .option('-u, --url <url>', 'Custom API URL (optional)')
25
- .option('-m, --model <model>', 'AI model name (optional - uses provider default)')
26
- .action((options) => {
27
- const configDir = path.join(process.env.HOME, '.codeflow-hook');
28
- if (!fs.existsSync(configDir)) {
29
- fs.mkdirSync(configDir, { recursive: true });
30
- }
31
-
32
- const configPath = path.join(configDir, 'config.json');
33
- const existingConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
34
-
35
- const config = {
36
- ...existingConfig,
37
- provider: options.provider || existingConfig.provider || 'gemini',
38
- apiKey: options.key || existingConfig.apiKey,
39
- apiUrl: options.url || existingConfig.apiUrl,
40
- model: options.model || existingConfig.model
41
- };
42
-
43
- // Set defaults based on provider
44
- if (!config.apiUrl) {
45
- switch (config.provider) {
46
- case 'openai':
47
- config.apiUrl = 'https://api.openai.com/v1/chat/completions';
48
- config.model = config.model || 'gpt-4';
49
- break;
50
- case 'claude':
51
- config.apiUrl = 'https://api.anthropic.com/v1/messages';
52
- config.model = config.model || 'claude-3-sonnet-20240229';
53
- break;
54
- case 'gemini':
55
- default:
56
- config.apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
57
- config.model = config.model || 'gemini-pro';
58
- break;
59
- }
60
- }
61
-
62
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
63
- console.log(chalk.green(`โœ… Configuration saved for ${config.provider} provider`));
64
- });
65
-
66
- // Install git hooks
67
- program
68
- .command('install')
69
- .description('Install git hooks (pre-commit and pre-push)')
70
- .option('--hooks-dir <dir>', 'Custom hooks directory', '.git/hooks')
71
- .action(async (options) => {
72
- const spinner = ora('Installing git hooks...').start();
73
-
74
- try {
75
- const hooksDir = path.resolve(options.hooksDir);
76
- if (!fs.existsSync(hooksDir)) {
77
- fs.mkdirSync(hooksDir, { recursive: true });
78
- }
79
-
80
- // Create pre-commit hook
81
- const preCommitHook = `#!/usr/bin/env bash
82
- # Codeflow pre-commit hook
83
- # Auto-generated by codeflow-hook CLI
84
-
85
- set -e
86
-
87
- echo "๐Ÿ”ฌ Running Codeflow AI Code Analysis..."
88
-
89
- # Get staged changes
90
- STAGED_DIFF=$(git diff --cached --no-color)
91
-
92
- if [ -z "$STAGED_DIFF" ]; then
93
- echo "โ„น๏ธ No staged changes to analyze"
94
- exit 0
95
- fi
96
-
97
- # Run AI analysis
98
- npx codeflow-hook analyze-diff "$STAGED_DIFF"
99
- `;
100
-
101
- fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
102
-
103
- // Create pre-push hook (enhanced version)
104
- const prePushHook = `#!/usr/bin/env bash
105
- # Codeflow pre-push hook
106
- # Auto-generated by codeflow-hook CLI
107
-
108
- set -e
109
-
110
- echo "๐Ÿš€ Running Codeflow CI/CD simulation..."
111
-
112
- # Run tests if available
113
- if [ -f "package.json" ]; then
114
- echo "๐Ÿงช Running tests..."
115
- npm test || (echo "โŒ Tests failed" && exit 1)
116
- fi
117
-
118
- # Get staged changes for AI analysis
119
- STAGED_DIFF=$(git diff --cached --no-color)
120
-
121
- if [ -n "$STAGED_DIFF" ]; then
122
- echo "๐Ÿ”ฌ Running AI Code Review..."
123
- npx codeflow-hook analyze-diff "$STAGED_DIFF" || exit 1
124
- fi
125
-
126
- echo "โœ… All checks passed!"
127
- exit 0
128
- `;
129
-
130
- fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
131
-
132
- spinner.succeed('Git hooks installed successfully');
133
- console.log(chalk.blue('๐Ÿ“‹ Installed hooks:'));
134
- console.log(chalk.gray(' - pre-commit: AI analysis on staged changes'));
135
- console.log(chalk.gray(' - pre-push: CI/CD simulation with AI review + tests'));
136
-
137
- } catch (error) {
138
- spinner.fail('Failed to install hooks');
139
- console.error(chalk.red(error.message));
140
- process.exit(1);
141
- }
142
- });
143
-
144
- // Analyze diff with configured AI provider
145
- program
146
- .command('analyze-diff')
147
- .description('Analyze git diff with configured AI provider')
148
- .argument('<diff>', 'Git diff content')
149
- .action(async (diff) => {
150
- try {
151
- const configPath = path.join(process.env.HOME, '.codeflow-hook', 'config.json');
152
-
153
- if (!fs.existsSync(configPath)) {
154
- console.log(chalk.red('No configuration found. Run: codeflow-hook config -k <api-key>'));
155
- process.exit(1);
156
- }
157
-
158
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
159
-
160
- if (diff.trim() === '') {
161
- console.log(chalk.gray('โ„น๏ธ No changes to analyze'));
162
- return;
163
- }
164
-
165
- const spinner = ora(`Analyzing code with ${config.provider}...`).start();
166
- const prompt = generateCodeReviewPrompt(diff);
167
-
168
- let result;
169
- try {
170
- result = await callAIProvider(config, prompt);
171
- } catch (error) {
172
- spinner.fail('Analysis failed');
173
- console.error(chalk.red(`AI API Error: ${error.message}`));
174
- process.exit(1);
175
- }
176
-
177
- spinner.succeed('Analysis complete');
178
-
179
- // Parse and display results
180
- displayAnalysisResults(result);
181
-
182
- } catch (error) {
183
- console.log(chalk.red(`Configuration error: ${error.message}`));
184
- process.exit(1);
185
- }
186
- });
187
-
188
- // Show status
189
- program
190
- .command('status')
191
- .description('Show installation and configuration status')
192
- .action(() => {
193
- console.log(chalk.blue('๐Ÿ” Codeflow Hook Status'));
194
- console.log();
195
-
196
- // Check configuration
197
- const configPath = path.join(process.env.HOME, '.codeflow-hook', 'config.json');
198
- if (fs.existsSync(configPath)) {
199
- console.log(chalk.green('โœ… Configuration: Found'));
200
- } else {
201
- console.log(chalk.red('โŒ Configuration: Not found (run: codeflow-hook config)'));
202
- }
203
-
204
- // Check git hooks
205
- const hooksDir = '.git/hooks';
206
- const preCommitHook = path.join(hooksDir, 'pre-commit');
207
- const prePushHook = path.join(hooksDir, 'pre-push');
208
-
209
- if (fs.existsSync(preCommitHook)) {
210
- console.log(chalk.green('โœ… Git Hook (pre-commit): Installed'));
211
- } else {
212
- console.log(chalk.red('โŒ Git Hook (pre-commit): Not installed'));
213
- }
214
-
215
- if (fs.existsSync(prePushHook)) {
216
- console.log(chalk.green('โœ… Git Hook (pre-push): Installed'));
217
- } else {
218
- console.log(chalk.red('โŒ Git Hook (pre-push): Not installed'));
219
- }
220
- });
221
-
222
- function generateCodeReviewPrompt(diff) {
223
- return `You are "Codeflow", a world-class AI software engineering assistant acting as a Principal Engineer. Your mission is to perform a rigorous and constructive code review on the provided code changes.
224
-
225
- **Guidelines:**
226
- - Focus on code quality, security, performance, and best practices
227
- - Be constructive and provide actionable suggestions
228
- - Rate the changes on a scale of 1-10 (where 10 is excellent)
229
- - If there are critical issues, suggest fixes
230
-
231
- **Format your response as:**
232
- **Rating:** [1-10]/10
233
- **Summary:** [Brief summary]
234
-
235
- **Issues:** (if any)
236
- - [Issue description and suggestion]
237
-
238
- **Recommendations:** (if any)
239
- - [Recommendation]
240
-
241
- **Code Changes to Review:**
242
- \`\`\`
243
- ${diff}
244
- \`\`\`
245
-
246
- Provide your analysis:`;
247
- }
248
-
249
- function callAIProvider(config, prompt) {
250
- switch (config.provider) {
251
- case 'openai':
252
- return callOpenAI(config, prompt);
253
- case 'claude':
254
- return callClaude(config, prompt);
255
- case 'gemini':
256
- default:
257
- return callGemini(config, prompt);
258
- }
259
- }
260
-
261
- async function callGemini(config, prompt) {
262
- const payload = {
263
- contents: [{
264
- parts: [{
265
- text: prompt
266
- }]
267
- }],
268
- generationConfig: {
269
- temperature: 0.2,
270
- topK: 40,
271
- topP: 0.95,
272
- maxOutputTokens: 2048,
273
- }
274
- };
275
-
276
- const response = await axios.post(`${config.apiUrl}?key=${config.apiKey}`, payload, {
277
- headers: {
278
- 'Content-Type': 'application/json'
279
- }
280
- });
281
-
282
- return response.data.candidates[0].content.parts[0].text;
283
- }
284
-
285
- async function callOpenAI(config, prompt) {
286
- const payload = {
287
- model: config.model,
288
- messages: [{ role: 'user', content: prompt }],
289
- temperature: 0.2,
290
- max_tokens: 2048
291
- };
292
-
293
- const response = await axios.post(config.apiUrl, payload, {
294
- headers: {
295
- 'Content-Type': 'application/json',
296
- 'Authorization': `Bearer ${config.apiKey}`
297
- }
298
- });
299
-
300
- return response.data.choices[0].message.content;
301
- }
302
-
303
- async function callClaude(config, prompt) {
304
- const payload = {
305
- model: config.model,
306
- max_tokens: 2048,
307
- messages: [{ role: 'user', content: prompt }]
308
- };
309
-
310
- const response = await axios.post(config.apiUrl, payload, {
311
- headers: {
312
- 'Content-Type': 'application/json',
313
- 'x-api-key': config.apiKey,
314
- 'anthropic-version': '2023-06-01'
315
- }
316
- });
317
-
318
- return response.data.content[0].text;
319
- }
320
-
321
- function displayAnalysisResults(result) {
322
- // Parse the AI response and format it nicely
323
- const lines = result.split('\n');
324
-
325
- for (const line of lines) {
326
- if (line.startsWith('**Rating:**')) {
327
- const rating = line.match(/\*\*Rating:\*\*\s*(\d+)/);
328
- if (rating) {
329
- const score = parseInt(rating[1]);
330
- if (score >= 8) {
331
- console.log(chalk.green(`โญ ${line}`));
332
- } else if (score >= 5) {
333
- console.log(chalk.yellow(`โš ๏ธ ${line}`));
334
- } else {
335
- console.log(chalk.red(`โŒ ${line}`));
336
- }
337
- }
338
- } else if (line.includes('**Issues:**') || line.includes('**Recommendations:**')) {
339
- console.log(chalk.blue(line));
340
- } else if (line.startsWith('- ')) {
341
- console.log(chalk.gray(line));
342
- } else if (line.includes('**Summary:**')) {
343
- console.log(chalk.cyan(line));
344
- } else {
345
- console.log(line);
346
- }
347
- }
348
-
349
- console.log();
350
- }
351
-
352
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import axios from 'axios';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os'; // Make sure os is imported
10
+ import readline from 'readline';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('codeflow-hook')
16
+ .description('Interactive CI/CD simulator and AI-powered code reviewer')
17
+ .version('1.0.0');
18
+
19
+ // Configure AI provider settings
20
+ program
21
+ .command('config')
22
+ .description('Configure AI provider settings')
23
+ .option('-p, --provider <provider>', 'AI provider (gemini, openai, claude)', 'gemini')
24
+ .option('-k, --key <key>', 'API key for the chosen provider')
25
+ .option('-u, --url <url>', 'Custom API URL (optional)')
26
+ .option('-m, --model <model>', 'AI model name (optional - uses provider default)')
27
+ .action(async (options) => {
28
+ // Use USERPROFILE on Windows instead of HOME which might be undefined
29
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
30
+ const configDir = path.join(homeDir, '.codeflow-hook');
31
+ if (!fs.existsSync(configDir)) {
32
+ fs.mkdirSync(configDir, { recursive: true });
33
+ }
34
+
35
+ const configPath = path.join(configDir, 'config.json');
36
+
37
+ // Configuration cascade: Check for project-level config first, then fall back to global
38
+ const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
39
+ const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
40
+
41
+ const existingConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
42
+
43
+ // Show config cascade message
44
+ if (Object.keys(projectConfig).length > 0) {
45
+ console.log(chalk.blue('๐Ÿ“ Using configuration cascade (project โ†’ global):'));
46
+ console.log(chalk.gray(` Project config: ${projectConfigPath}`));
47
+ if (fs.existsSync(configPath)) {
48
+ console.log(chalk.gray(` Global config: ${configPath}`));
49
+ }
50
+ }
51
+
52
+ // Determine what the new provider and API key should be
53
+ const requestedProvider = (options.provider || existingConfig.provider || 'gemini').toLowerCase();
54
+ const requestedApiKey = options.key || existingConfig.apiKey;
55
+
56
+ // FIX: Explicit boolean coercion to prevent || operator from returning non-boolean values
57
+ // Step by step to ensure proper boolean evaluation
58
+ const hasNewKey = !!options.key;
59
+ const isFirstTimeSetup = !existingConfig.provider && !existingConfig.apiKey;
60
+ const shouldValidate = hasNewKey || isFirstTimeSetup;
61
+
62
+ console.log('VALIDATION DEBUG - provider:', requestedProvider, 'key_exists:', !!options.key, 'shouldValidate:', shouldValidate);
63
+
64
+ if (shouldValidate && requestedApiKey) {
65
+ console.log(chalk.blue('๐Ÿ” Validating API key for provider:', requestedProvider));
66
+ const validationSpinner = ora('Checking key permissions...').start();
67
+
68
+ try {
69
+ await validateApiKey(requestedProvider, requestedApiKey);
70
+ validationSpinner.succeed('API key validated');
71
+ } catch (error) {
72
+ validationSpinner.fail('Validation failed');
73
+ console.error(chalk.red(`โŒ ${error.message}`));
74
+ console.error(chalk.red(`๐Ÿ’ก Make sure you're using a valid ${requestedProvider.toUpperCase()} API key for the ${requestedProvider} provider.`));
75
+ process.exit(1);
76
+ }
77
+ } else {
78
+ console.log(chalk.yellow('โš ๏ธ Validation skipped - no API key validation needed'));
79
+ }
80
+
81
+ // Initialize config with existing values, then override with verified CLI options
82
+ const config = {
83
+ provider: requestedProvider,
84
+ apiKey: requestedApiKey,
85
+ apiUrl: options.url || existingConfig.apiUrl,
86
+ model: options.model || existingConfig.model
87
+ };
88
+
89
+ // Interactive model selection if model is not explicitly provided
90
+ if (!options.model) {
91
+ // FIX: More robust logic for when to fetch new models
92
+ const isNewProvider = existingConfig.provider && (existingConfig.provider.toLowerCase() !== config.provider.toLowerCase());
93
+ const hasNewKey = !!options.key;
94
+
95
+ const shouldFetchModels = !existingConfig.model || isNewProvider || hasNewKey;
96
+
97
+ if (shouldFetchModels) {
98
+ console.log(chalk.blue('๐Ÿ” Fetching available models...'));
99
+
100
+ const modelSpinner = ora('Contacting API...').start();
101
+ try {
102
+ const models = await fetchModels(config.provider, config.apiKey);
103
+ modelSpinner.succeed('Models fetched successfully');
104
+
105
+ console.log(chalk.blue('Available models:'));
106
+ models.forEach((model, index) => {
107
+ console.log(chalk.gray(` ${index + 1}. ${model}`));
108
+ });
109
+
110
+ const rl = readline.createInterface({
111
+ input: process.stdin,
112
+ output: process.stdout
113
+ });
114
+
115
+ const selectedModel = await new Promise(resolve => {
116
+ rl.question(chalk.blue(`Enter the model name (or number): `), (input) => {
117
+ rl.close();
118
+ resolve(input.trim());
119
+ });
120
+ });
121
+
122
+ // Check if user entered a number
123
+ const index = parseInt(selectedModel) - 1;
124
+ if (!isNaN(index) && index >= 0 && index < models.length) {
125
+ config.model = models[index];
126
+ } else {
127
+ config.model = selectedModel;
128
+ }
129
+
130
+ console.log(chalk.green(`โœ“ Selected model: ${config.model}`));
131
+
132
+ } catch (error) {
133
+ modelSpinner.fail('Failed to fetch models');
134
+ console.error(chalk.red(`โŒ API Error: ${error.message}`));
135
+ // Fallback to hardcoded list if API call fails
136
+ const fallbackModels = getFallbackModels(config.provider);
137
+
138
+ console.log(chalk.yellow('โš ๏ธ Using fallback model list:'));
139
+ fallbackModels.forEach((model, index) => {
140
+ console.log(chalk.gray(` ${index + 1}. ${model}`));
141
+ });
142
+
143
+ const rl = readline.createInterface({
144
+ input: process.stdin,
145
+ output: process.stdout
146
+ });
147
+
148
+ const selectedModel = await new Promise(resolve => {
149
+ rl.question(chalk.blue(`Select a model (name or number): `), (input) => {
150
+ rl.close();
151
+ resolve(input.trim());
152
+ });
153
+ });
154
+
155
+ const index = parseInt(selectedModel) - 1;
156
+ if (!isNaN(index) && index >= 0 && index < fallbackModels.length) {
157
+ config.model = fallbackModels[index];
158
+ } else {
159
+ config.model = selectedModel;
160
+ }
161
+
162
+ console.log(chalk.green(`โœ“ Selected fallback model: ${config.model}`));
163
+ }
164
+ } else {
165
+ // Reuse existing model and config
166
+ console.log(chalk.blue(`๐Ÿ“š Using existing configuration (provider: ${existingConfig.provider}, model: ${existingConfig.model})`));
167
+ }
168
+ } else {
169
+ // User explicitly provided model
170
+ console.log(chalk.green(`โœ“ Using specified model: ${config.model}`));
171
+ }
172
+
173
+ // Set default API URL if not provided by options or existing config
174
+ if (!config.apiUrl) {
175
+ switch (config.provider) {
176
+ case 'openai':
177
+ config.apiUrl = 'https://api.openai.com/v1/chat/completions';
178
+ break;
179
+ case 'claude':
180
+ config.apiUrl = 'https://api.anthropic.com/v1/messages';
181
+ break;
182
+ case 'gemini':
183
+ default:
184
+ // Updated Gemini API URL to v1 - using a base URL
185
+ config.apiUrl = 'https://generativelanguage.googleapis.com/v1/models';
186
+ break;
187
+ }
188
+ }
189
+
190
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
191
+ console.log(chalk.green(`โœ… Configuration saved for ${config.provider} provider`));
192
+ if (config.model) {
193
+ console.log(chalk.green(` Model: ${config.model}`));
194
+ }
195
+ });
196
+
197
+ // Install git hooks
198
+ program
199
+ .command('install')
200
+ .description('Install git hooks (pre-commit and pre-push)')
201
+ .option('--hooks-dir <dir>', 'Custom hooks directory', '.git/hooks')
202
+ .action(async (options) => {
203
+ const spinner = ora('Installing git hooks...').start();
204
+ try {
205
+ const hooksDir = path.resolve(options.hooksDir);
206
+ if (!fs.existsSync(hooksDir)) {
207
+ fs.mkdirSync(hooksDir, { recursive: true });
208
+ }
209
+
210
+ // CHANGE 1: The git hooks are modified to PIPE the diff content via stdin
211
+ const preCommitHook = `#!/usr/bin/env bash
212
+ # Codeflow pre-commit hook
213
+ set -e
214
+ echo "๐Ÿ”ฌ Running Codeflow AI Code Analysis..."
215
+ STAGED_DIFF=$(git diff --cached --no-color)
216
+ if [ -z "$STAGED_DIFF" ]; then
217
+ echo "โ„น๏ธ No staged changes to analyze"
218
+ exit 0
219
+ fi
220
+ # Use stdin to avoid "command line too long" error
221
+ echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff
222
+ `;
223
+
224
+ fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
225
+
226
+ const prePushHook = `#!/usr/bin/env bash
227
+ # Codeflow pre-push hook
228
+ set -e
229
+ echo "๐Ÿš€ Running Codeflow CI/CD simulation..."
230
+ if [ -f "package.json" ]; then
231
+ echo "๐Ÿงช Running tests..."
232
+ npm test || (echo "โŒ Tests failed" && exit 1)
233
+ fi
234
+ STAGED_DIFF=$(git diff --cached --no-color)
235
+ if [ -n "$STAGED_DIFF" ]; then
236
+ echo "๐Ÿ”ฌ Running AI Code Review..."
237
+ # Use stdin to avoid "command line too long" error
238
+ echo "$STAGED_DIFF" | npx codeflow-hook analyze-diff || exit 1
239
+ fi
240
+ echo "โœ… All checks passed!"
241
+ exit 0
242
+ `;
243
+ fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
244
+
245
+ spinner.succeed('Git hooks installed successfully');
246
+ console.log(chalk.blue('๐Ÿ“‹ Installed hooks:'));
247
+ console.log(chalk.gray(' - pre-commit: AI analysis on staged changes'));
248
+ console.log(chalk.gray(' - pre-push: CI/CD simulation with AI review + tests'));
249
+
250
+ } catch (error) {
251
+ spinner.fail('Failed to install hooks');
252
+ console.error(chalk.red(error.message));
253
+ process.exit(1);
254
+ }
255
+ });
256
+
257
+ // Analyze diff with configured AI provider
258
+ program
259
+ .command('analyze-diff')
260
+ .description('Analyze git diff with configured AI provider')
261
+ // CHANGE 2: The argument is now OPTIONAL (square brackets)
262
+ .argument('[diff]', 'Git diff content')
263
+ .action(async (diff) => {
264
+ try {
265
+ // CHANGE 3: New logic block to read from stdin if no argument is given
266
+ let diffContent = diff;
267
+ if (!diffContent) {
268
+ const chunks = [];
269
+ for await (const chunk of process.stdin) {
270
+ chunks.push(chunk);
271
+ }
272
+ diffContent = Buffer.concat(chunks).toString('utf8');
273
+ }
274
+
275
+ const configPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
276
+
277
+ if (!fs.existsSync(configPath)) {
278
+ console.log(chalk.red('No configuration found. Run: codeflow-hook config -k <api-key>'));
279
+ process.exit(1);
280
+ }
281
+
282
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
283
+
284
+ if (diffContent.trim() === '') {
285
+ console.log(chalk.gray('โ„น๏ธ No changes to analyze'));
286
+ return;
287
+ }
288
+
289
+ const spinner = ora(`Analyzing code with ${config.provider}...`).start();
290
+ const prompt = generateCodeReviewPrompt(diffContent);
291
+
292
+ let result;
293
+ try {
294
+ result = await callAIProvider(config, prompt);
295
+ } catch (error) {
296
+ spinner.fail('Analysis failed');
297
+ console.error(chalk.red(`AI API Error: ${error.message}`));
298
+ process.exit(1);
299
+ }
300
+
301
+ spinner.succeed('Analysis complete');
302
+ displayAnalysisResults(result);
303
+
304
+ } catch (error) {
305
+ console.log(chalk.red(`Configuration error: ${error.message}`));
306
+ process.exit(1);
307
+ }
308
+ });
309
+
310
+ // Show status
311
+ program
312
+ .command('status')
313
+ .description('Show installation and configuration status')
314
+ .action(() => {
315
+ console.log(chalk.blue('๐Ÿ” Codeflow Hook Status'));
316
+ console.log();
317
+
318
+ // Check configuration cascade
319
+ const globalConfigPath = path.join(os.homedir(), '.codeflow-hook', 'config.json');
320
+ const projectConfigPath = path.join(process.cwd(), '.codeflowrc.json');
321
+
322
+ const hasGlobalConfig = fs.existsSync(globalConfigPath);
323
+ const hasProjectConfig = fs.existsSync(projectConfigPath);
324
+
325
+ if (hasProjectConfig) {
326
+ console.log(chalk.green('โœ… Project Configuration: Found (.codeflowrc.json)'));
327
+ } else {
328
+ console.log(chalk.gray('โ„น๏ธ Project Configuration: Not found'));
329
+ }
330
+
331
+ if (hasGlobalConfig) {
332
+ console.log(chalk.green('โœ… Global Configuration: Found'));
333
+ } else {
334
+ console.log(chalk.red('โŒ Global Configuration: Not found (run: codeflow-hook config)'));
335
+ }
336
+
337
+ if (!hasGlobalConfig && !hasProjectConfig) {
338
+ console.log(chalk.red('โŒ No configuration found. Run: codeflow-hook config -k <api-key>'));
339
+ }
340
+
341
+ // Check git hooks
342
+ const hooksDir = '.git/hooks';
343
+ const preCommitHook = path.join(hooksDir, 'pre-commit');
344
+ const prePushHook = path.join(hooksDir, 'pre-push');
345
+
346
+ if (fs.existsSync(preCommitHook)) {
347
+ console.log(chalk.green('โœ… Git Hook (pre-commit): Installed'));
348
+ } else {
349
+ console.log(chalk.red('โŒ Git Hook (pre-commit): Not installed'));
350
+ }
351
+
352
+ if (fs.existsSync(prePushHook)) {
353
+ console.log(chalk.green('โœ… Git Hook (pre-push): Installed'));
354
+ } else {
355
+ console.log(chalk.red('โŒ Git Hook (pre-push): Not installed'));
356
+ }
357
+
358
+ console.log();
359
+ console.log(chalk.blue('๐Ÿ’ก Tips:'));
360
+ console.log(chalk.gray(' โ€ข Create .codeflowrc.json in project root for project-specific settings'));
361
+ console.log(chalk.gray(' โ€ข Large diffs (>20KB) will prompt for confirmation to avoid high costs'));
362
+ console.log(chalk.gray(' โ€ข Run "codeflow-hook config -h" for configuration options'));
363
+ });
364
+
365
+
366
+ async function validateApiKey(provider, apiKey) {
367
+ console.log(`DEBUG: validateApiKey called for provider: ${provider}`);
368
+ if (!apiKey) {
369
+ throw new Error('No API key provided');
370
+ }
371
+
372
+ try {
373
+ console.log(`DEBUG: Making API call for ${provider} validation...`);
374
+ switch (provider) {
375
+ case 'gemini':
376
+ // Test Gemini API key by making a simple models list request
377
+ const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
378
+ console.log(`DEBUG: Gemini URL: ${geminiUrl.substring(0, 80)}...`);
379
+ await axios.get(geminiUrl);
380
+ console.log(`DEBUG: Gemini API call succeeded`);
381
+ break;
382
+ case 'openai':
383
+ const openaiUrl = 'https://api.openai.com/v1/models';
384
+ console.log(`DEBUG: OpenAI URL: ${openaiUrl} with Bearer token`);
385
+ await axios.get(openaiUrl, {
386
+ headers: { 'Authorization': `Bearer ${apiKey}` }
387
+ });
388
+ console.log(`DEBUG: OpenAI API call succeeded`);
389
+ break;
390
+ case 'claude':
391
+ // Test Claude API key with a minimal request (Anthropic doesn't have models endpoint, so we use a very basic check)
392
+ console.log(`DEBUG: Claude validation call`);
393
+ try {
394
+ await axios.post('https://api.anthropic.com/v1/messages', {
395
+ model: 'claude-3-haiku-20240307',
396
+ max_tokens: 1,
397
+ messages: [{ role: 'user', content: 'Test' }]
398
+ }, {
399
+ headers: {
400
+ 'x-api-key': apiKey,
401
+ 'anthropic-version': '2023-06-01',
402
+ 'Content-Type': 'application/json'
403
+ }
404
+ });
405
+ } catch (claudeError) {
406
+ console.log(`DEBUG: Claude error status: ${claudeError.response?.status}`);
407
+ if (claudeError.response?.status === 401) {
408
+ throw new Error('Invalid Claude API key');
409
+ }
410
+ // If it's a different error (like rate limit), we'll allow it through
411
+ }
412
+ break;
413
+ default:
414
+ throw new Error(`Unsupported provider: ${provider}`);
415
+ }
416
+ } catch (error) {
417
+ console.log(`DEBUG: Validation failed with error: ${error.message}`);
418
+ console.log(`DEBUG: Error response status: ${error.response?.status}`);
419
+ if (error.response?.status === 401) {
420
+ throw new Error(`Invalid ${provider} API key`);
421
+ } else if (error.response?.status === 403) {
422
+ throw new Error(`API key lacks permissions for ${provider}`);
423
+ } else if (error.response?.status === 429) {
424
+ throw new Error(`API rate limit exceeded. Please try again later.`);
425
+ } else {
426
+ throw new Error(`${provider} API is currently unavailable: ${error.message}`);
427
+ }
428
+ }
429
+ }
430
+
431
+ async function fetchModels(provider, apiKey) {
432
+ switch (provider) {
433
+ case 'gemini':
434
+ const geminiResponse = await axios.get(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
435
+ return geminiResponse.data.models.map(m => m.name.replace('models/', ''));
436
+ case 'openai':
437
+ const openaiResponse = await axios.get('https://api.openai.com/v1/models', {
438
+ headers: { 'Authorization': `Bearer ${apiKey}` }
439
+ });
440
+ return openaiResponse.data.data.map(m => m.id);
441
+ case 'claude':
442
+ console.log(chalk.yellow("Claude does not support dynamic model fetching. Using a standard list."));
443
+ return getFallbackModels('claude');
444
+ default:
445
+ return [];
446
+ }
447
+ }
448
+
449
+ function getFallbackModels(provider) {
450
+ switch (provider) {
451
+ case 'gemini':
452
+ return ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest', 'gemini-pro'];
453
+ case 'openai':
454
+ return ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'];
455
+ case 'claude':
456
+ return ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'];
457
+ default:
458
+ return [];
459
+ }
460
+ }
461
+
462
+ function generateCodeReviewPrompt(diff) {
463
+ return `You are "Codeflow", a world-class AI software engineering assistant acting as a Principal Engineer. Your mission is to perform a rigorous and constructive code review on the provided code changes.
464
+
465
+ **Guidelines:**
466
+ - Focus on code quality, security, performance, and best practices
467
+ - Be constructive and provide actionable suggestions
468
+ - Rate the changes on a scale of 1-10 (where 10 is excellent)
469
+ - If there are critical issues, suggest fixes
470
+
471
+ **Format your response as:**
472
+ **Rating:** [1-10]/10
473
+ **Summary:** [Brief summary]
474
+
475
+ **Issues:** (if any)
476
+ - [Issue description and suggestion]
477
+
478
+ **Recommendations:** (if any)
479
+ - [Recommendation]
480
+
481
+ **Code Changes to Review:**
482
+ \`\`\`
483
+ ${diff}
484
+ \`\`\`
485
+
486
+ Provide your analysis:`;
487
+ }
488
+
489
+ function callAIProvider(config, prompt) {
490
+ switch (config.provider) {
491
+ case 'openai':
492
+ return callOpenAI(config, prompt);
493
+ case 'claude':
494
+ return callClaude(config, prompt);
495
+ case 'gemini':
496
+ default:
497
+ return callGemini(config, prompt);
498
+ }
499
+ }
500
+
501
+ async function callGemini(config, prompt) {
502
+ const payload = {
503
+ contents: [{
504
+ parts: [{
505
+ text: prompt
506
+ }]
507
+ }],
508
+ generationConfig: {
509
+ temperature: 0.2,
510
+ topK: 40,
511
+ topP: 0.95,
512
+ maxOutputTokens: 2048,
513
+ }
514
+ };
515
+
516
+ const url = `${config.apiUrl}/${config.model}:generateContent?key=${config.apiKey}`;
517
+ const response = await axios.post(url, payload, {
518
+ headers: {
519
+ 'Content-Type': 'application/json'
520
+ }
521
+ });
522
+
523
+ return response.data.candidates[0].content.parts[0].text;
524
+ }
525
+
526
+ async function callOpenAI(config, prompt) {
527
+ const payload = {
528
+ model: config.model,
529
+ messages: [{ role: 'user', content: prompt }],
530
+ temperature: 0.2,
531
+ max_tokens: 2048
532
+ };
533
+
534
+ const response = await axios.post(config.apiUrl, payload, {
535
+ headers: {
536
+ 'Content-Type': 'application/json',
537
+ 'Authorization': `Bearer ${config.apiKey}`
538
+ }
539
+ });
540
+
541
+ return response.data.choices[0].message.content;
542
+ }
543
+
544
+ async function callClaude(config, prompt) {
545
+ const payload = {
546
+ model: config.model,
547
+ max_tokens: 2048,
548
+ messages: [{ role: 'user', content: prompt }]
549
+ };
550
+
551
+ const response = await axios.post(config.apiUrl, payload, {
552
+ headers: {
553
+ 'Content-Type': 'application/json',
554
+ 'x-api-key': config.apiKey,
555
+ 'anthropic-version': '2023-06-01'
556
+ }
557
+ });
558
+
559
+ return response.data.content[0].text;
560
+ }
561
+
562
+ function displayAnalysisResults(result) {
563
+ // Parse the AI response and format it nicely
564
+ const lines = result.split('\n');
565
+
566
+ for (const line of lines) {
567
+ if (line.startsWith('**Rating:**')) {
568
+ const rating = line.match(/\*\*Rating:\*\*\s*(\d+)/);
569
+ if (rating) {
570
+ const score = parseInt(rating[1]);
571
+ if (score >= 8) {
572
+ console.log(chalk.green(`โญ ${line}`));
573
+ } else if (score >= 5) {
574
+ console.log(chalk.yellow(`โš ๏ธ ${line}`));
575
+ } else {
576
+ console.log(chalk.red(`โŒ ${line}`));
577
+ }
578
+ }
579
+ } else if (line.includes('**Issues:**') || line.includes('**Recommendations:**')) {
580
+ console.log(chalk.blue(line));
581
+ } else if (line.startsWith('- ')) {
582
+ console.log(chalk.gray(line));
583
+ } else if (line.includes('**Summary:**')) {
584
+ console.log(chalk.cyan(line));
585
+ } else {
586
+ console.log(line);
587
+ }
588
+ }
589
+
590
+ console.log();
591
+ }
592
+
593
+ // Make sure the final line uses parseAsync
594
+ program.parseAsync(process.argv);