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.
- package/README.md +251 -248
- package/bin/codeflow-hook.js +594 -352
- package/package.json +37 -25
package/bin/codeflow-hook.js
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
.option('-
|
|
24
|
-
.option('-
|
|
25
|
-
.option('-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
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);
|