@codebakers/cli 1.5.0 → 2.0.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/commands/audit.d.ts +19 -0
- package/dist/commands/audit.js +730 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +176 -0
- package/dist/commands/doctor.js +59 -4
- package/dist/commands/heal.d.ts +41 -0
- package/dist/commands/heal.js +734 -0
- package/dist/commands/login.js +12 -16
- package/dist/commands/provision.d.ts +55 -3
- package/dist/commands/provision.js +243 -74
- package/dist/commands/scaffold.js +221 -41
- package/dist/commands/setup.js +60 -19
- package/dist/commands/upgrade.d.ts +4 -0
- package/dist/commands/upgrade.js +90 -0
- package/dist/config.d.ts +61 -5
- package/dist/config.js +268 -5
- package/dist/index.js +44 -3
- package/dist/lib/api.d.ts +45 -0
- package/dist/lib/api.js +159 -0
- package/dist/mcp/server.js +146 -0
- package/package.json +1 -1
- package/src/commands/audit.ts +827 -0
- package/src/commands/config.ts +216 -0
- package/src/commands/doctor.ts +69 -4
- package/src/commands/heal.ts +889 -0
- package/src/commands/login.ts +14 -18
- package/src/commands/provision.ts +323 -101
- package/src/commands/scaffold.ts +257 -43
- package/src/commands/setup.ts +65 -20
- package/src/commands/upgrade.ts +110 -0
- package/src/config.ts +320 -11
- package/src/index.ts +48 -3
- package/src/lib/api.ts +183 -0
- package/src/mcp/server.ts +160 -0
package/src/config.ts
CHANGED
|
@@ -1,34 +1,144 @@
|
|
|
1
1
|
import Conf from 'conf';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
|
|
3
5
|
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical list of ALL supported service keys
|
|
9
|
+
* This must match the server contract at src/lib/contracts/service-keys.ts
|
|
10
|
+
*/
|
|
11
|
+
export const SERVICE_KEYS = [
|
|
12
|
+
// Infrastructure (provisioning-capable)
|
|
13
|
+
'github',
|
|
14
|
+
'supabase',
|
|
15
|
+
'vercel',
|
|
16
|
+
// AI
|
|
17
|
+
'openai',
|
|
18
|
+
'anthropic',
|
|
19
|
+
// Payments
|
|
20
|
+
'stripe',
|
|
21
|
+
// Communication
|
|
22
|
+
'twilio_sid',
|
|
23
|
+
'twilio_auth',
|
|
24
|
+
'resend',
|
|
25
|
+
'vapi',
|
|
26
|
+
// Monitoring
|
|
27
|
+
'sentry',
|
|
28
|
+
// Media
|
|
29
|
+
'cloudinary',
|
|
30
|
+
'pexels',
|
|
31
|
+
'midjourney',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type ServiceName = typeof SERVICE_KEYS[number];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Map service key names to environment variable names
|
|
38
|
+
*/
|
|
39
|
+
export const ENV_VAR_NAMES: Record<ServiceName, string> = {
|
|
40
|
+
github: 'GITHUB_TOKEN',
|
|
41
|
+
supabase: 'SUPABASE_ACCESS_TOKEN',
|
|
42
|
+
vercel: 'VERCEL_TOKEN',
|
|
43
|
+
openai: 'OPENAI_API_KEY',
|
|
44
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
45
|
+
stripe: 'STRIPE_SECRET_KEY',
|
|
46
|
+
twilio_sid: 'TWILIO_ACCOUNT_SID',
|
|
47
|
+
twilio_auth: 'TWILIO_AUTH_TOKEN',
|
|
48
|
+
resend: 'RESEND_API_KEY',
|
|
49
|
+
vapi: 'VAPI_API_KEY',
|
|
50
|
+
sentry: 'SENTRY_DSN',
|
|
51
|
+
cloudinary: 'CLOUDINARY_API_SECRET',
|
|
52
|
+
pexels: 'PEXELS_API_KEY',
|
|
53
|
+
midjourney: 'MIDJOURNEY_API_KEY',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Service key categories for display grouping
|
|
58
|
+
*/
|
|
59
|
+
export const SERVICE_KEY_CATEGORIES: Record<string, ServiceName[]> = {
|
|
60
|
+
infrastructure: ['github', 'supabase', 'vercel'],
|
|
61
|
+
ai: ['openai', 'anthropic'],
|
|
62
|
+
payments: ['stripe'],
|
|
63
|
+
communication: ['twilio_sid', 'twilio_auth', 'resend', 'vapi'],
|
|
64
|
+
monitoring: ['sentry'],
|
|
65
|
+
media: ['cloudinary', 'pexels', 'midjourney'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Service key labels for display
|
|
70
|
+
*/
|
|
71
|
+
export const SERVICE_KEY_LABELS: Record<ServiceName, string> = {
|
|
72
|
+
github: 'GitHub',
|
|
73
|
+
supabase: 'Supabase',
|
|
74
|
+
vercel: 'Vercel',
|
|
75
|
+
openai: 'OpenAI',
|
|
76
|
+
anthropic: 'Anthropic',
|
|
77
|
+
stripe: 'Stripe',
|
|
78
|
+
twilio_sid: 'Twilio SID',
|
|
79
|
+
twilio_auth: 'Twilio Auth',
|
|
80
|
+
resend: 'Resend',
|
|
81
|
+
vapi: 'VAPI',
|
|
82
|
+
sentry: 'Sentry',
|
|
83
|
+
cloudinary: 'Cloudinary',
|
|
84
|
+
pexels: 'Pexels',
|
|
85
|
+
midjourney: 'Midjourney',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Keys that can be used for auto-provisioning
|
|
90
|
+
*/
|
|
91
|
+
export const PROVISIONABLE_KEYS: ServiceName[] = ['github', 'supabase', 'vercel'];
|
|
92
|
+
|
|
93
|
+
type ServiceKeys = {
|
|
94
|
+
[K in ServiceName]: string | null;
|
|
95
|
+
};
|
|
10
96
|
|
|
11
97
|
interface ConfigSchema {
|
|
12
98
|
apiKey: string | null;
|
|
13
99
|
apiUrl: string;
|
|
14
100
|
experienceLevel: ExperienceLevel;
|
|
15
101
|
serviceKeys: ServiceKeys;
|
|
102
|
+
lastKeySync: string | null; // ISO date of last sync with server
|
|
16
103
|
}
|
|
17
104
|
|
|
105
|
+
// Create default service keys object with all keys set to null
|
|
106
|
+
const defaultServiceKeys: ServiceKeys = Object.fromEntries(
|
|
107
|
+
SERVICE_KEYS.map(key => [key, null])
|
|
108
|
+
) as ServiceKeys;
|
|
109
|
+
|
|
18
110
|
const config = new Conf<ConfigSchema>({
|
|
19
111
|
projectName: 'codebakers',
|
|
112
|
+
projectVersion: '1.7.0',
|
|
20
113
|
defaults: {
|
|
21
114
|
apiKey: null,
|
|
22
115
|
apiUrl: 'https://codebakers.ai',
|
|
23
116
|
experienceLevel: 'intermediate',
|
|
24
|
-
serviceKeys:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
117
|
+
serviceKeys: defaultServiceKeys,
|
|
118
|
+
lastKeySync: null,
|
|
119
|
+
},
|
|
120
|
+
// Migration to add new keys when upgrading from old version
|
|
121
|
+
migrations: {
|
|
122
|
+
'1.7.0': (store) => {
|
|
123
|
+
const oldKeys = store.get('serviceKeys') as Partial<ServiceKeys>;
|
|
124
|
+
const newKeys: ServiceKeys = { ...defaultServiceKeys };
|
|
125
|
+
|
|
126
|
+
// Preserve existing keys
|
|
127
|
+
for (const key of SERVICE_KEYS) {
|
|
128
|
+
if (oldKeys && key in oldKeys && oldKeys[key]) {
|
|
129
|
+
newKeys[key] = oldKeys[key] as string;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
store.set('serviceKeys', newKeys);
|
|
28
134
|
},
|
|
29
135
|
},
|
|
30
136
|
});
|
|
31
137
|
|
|
138
|
+
// ============================================================
|
|
139
|
+
// API Key Management
|
|
140
|
+
// ============================================================
|
|
141
|
+
|
|
32
142
|
export function getApiKey(): string | null {
|
|
33
143
|
return config.get('apiKey');
|
|
34
144
|
}
|
|
@@ -49,6 +159,10 @@ export function setApiUrl(url: string): void {
|
|
|
49
159
|
config.set('apiUrl', url);
|
|
50
160
|
}
|
|
51
161
|
|
|
162
|
+
// ============================================================
|
|
163
|
+
// Experience Level
|
|
164
|
+
// ============================================================
|
|
165
|
+
|
|
52
166
|
export function getExperienceLevel(): ExperienceLevel {
|
|
53
167
|
return config.get('experienceLevel');
|
|
54
168
|
}
|
|
@@ -57,12 +171,13 @@ export function setExperienceLevel(level: ExperienceLevel): void {
|
|
|
57
171
|
config.set('experienceLevel', level);
|
|
58
172
|
}
|
|
59
173
|
|
|
174
|
+
// ============================================================
|
|
60
175
|
// Service API Keys
|
|
61
|
-
|
|
176
|
+
// ============================================================
|
|
62
177
|
|
|
63
178
|
export function getServiceKey(service: ServiceName): string | null {
|
|
64
179
|
const keys = config.get('serviceKeys');
|
|
65
|
-
return keys[service];
|
|
180
|
+
return keys[service] ?? null;
|
|
66
181
|
}
|
|
67
182
|
|
|
68
183
|
export function setServiceKey(service: ServiceName, key: string): void {
|
|
@@ -77,6 +192,200 @@ export function clearServiceKey(service: ServiceName): void {
|
|
|
77
192
|
config.set('serviceKeys', keys);
|
|
78
193
|
}
|
|
79
194
|
|
|
195
|
+
export function clearAllServiceKeys(): void {
|
|
196
|
+
config.set('serviceKeys', { ...defaultServiceKeys });
|
|
197
|
+
config.set('lastKeySync', null);
|
|
198
|
+
}
|
|
199
|
+
|
|
80
200
|
export function getAllServiceKeys(): ServiceKeys {
|
|
81
201
|
return config.get('serviceKeys');
|
|
82
202
|
}
|
|
203
|
+
|
|
204
|
+
export function getConfiguredServiceKeys(): ServiceName[] {
|
|
205
|
+
const keys = config.get('serviceKeys');
|
|
206
|
+
return SERVICE_KEYS.filter(name => keys[name] !== null && keys[name] !== '');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getLastKeySync(): Date | null {
|
|
210
|
+
const lastSync = config.get('lastKeySync');
|
|
211
|
+
return lastSync ? new Date(lastSync) : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function setLastKeySync(date: Date): void {
|
|
215
|
+
config.set('lastKeySync', date.toISOString());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================
|
|
219
|
+
// Bulk Key Operations
|
|
220
|
+
// ============================================================
|
|
221
|
+
|
|
222
|
+
export interface SyncResult {
|
|
223
|
+
added: ServiceName[];
|
|
224
|
+
updated: ServiceName[];
|
|
225
|
+
unchanged: ServiceName[];
|
|
226
|
+
total: number;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sync keys from server response to local storage
|
|
231
|
+
*/
|
|
232
|
+
export function syncServiceKeys(serverKeys: Partial<ServiceKeys>): SyncResult {
|
|
233
|
+
const localKeys = config.get('serviceKeys');
|
|
234
|
+
const result: SyncResult = {
|
|
235
|
+
added: [],
|
|
236
|
+
updated: [],
|
|
237
|
+
unchanged: [],
|
|
238
|
+
total: 0,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
for (const keyName of SERVICE_KEYS) {
|
|
242
|
+
const serverValue = serverKeys[keyName];
|
|
243
|
+
const localValue = localKeys[keyName];
|
|
244
|
+
|
|
245
|
+
if (serverValue) {
|
|
246
|
+
result.total++;
|
|
247
|
+
|
|
248
|
+
if (!localValue) {
|
|
249
|
+
result.added.push(keyName);
|
|
250
|
+
localKeys[keyName] = serverValue;
|
|
251
|
+
} else if (localValue !== serverValue) {
|
|
252
|
+
result.updated.push(keyName);
|
|
253
|
+
localKeys[keyName] = serverValue;
|
|
254
|
+
} else {
|
|
255
|
+
result.unchanged.push(keyName);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
config.set('serviceKeys', localKeys);
|
|
261
|
+
config.set('lastKeySync', new Date().toISOString());
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================
|
|
267
|
+
// Environment File Operations
|
|
268
|
+
// ============================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Write service keys to a .env.local file
|
|
272
|
+
*/
|
|
273
|
+
export function writeKeysToEnvFile(projectPath: string, options?: {
|
|
274
|
+
includeEmpty?: boolean;
|
|
275
|
+
additionalVars?: Record<string, string>;
|
|
276
|
+
}): { written: number; path: string } {
|
|
277
|
+
const envPath = join(projectPath, '.env.local');
|
|
278
|
+
const keys = config.get('serviceKeys');
|
|
279
|
+
const lines: string[] = [];
|
|
280
|
+
|
|
281
|
+
// Header
|
|
282
|
+
lines.push('# Service Keys - Generated by CodeBakers CLI');
|
|
283
|
+
lines.push(`# Generated: ${new Date().toISOString()}`);
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
// Add additional vars first (like Supabase URL, etc.)
|
|
287
|
+
if (options?.additionalVars) {
|
|
288
|
+
lines.push('# Project Configuration');
|
|
289
|
+
for (const [name, value] of Object.entries(options.additionalVars)) {
|
|
290
|
+
lines.push(`${name}=${value}`);
|
|
291
|
+
}
|
|
292
|
+
lines.push('');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Group keys by category
|
|
296
|
+
for (const [category, keyNames] of Object.entries(SERVICE_KEY_CATEGORIES)) {
|
|
297
|
+
const categoryLines: string[] = [];
|
|
298
|
+
|
|
299
|
+
for (const keyName of keyNames) {
|
|
300
|
+
const value = keys[keyName];
|
|
301
|
+
const envVarName = ENV_VAR_NAMES[keyName];
|
|
302
|
+
|
|
303
|
+
if (value) {
|
|
304
|
+
categoryLines.push(`${envVarName}=${value}`);
|
|
305
|
+
} else if (options?.includeEmpty) {
|
|
306
|
+
categoryLines.push(`# ${envVarName}=`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (categoryLines.length > 0) {
|
|
311
|
+
lines.push(`# ${category.charAt(0).toUpperCase() + category.slice(1)}`);
|
|
312
|
+
lines.push(...categoryLines);
|
|
313
|
+
lines.push('');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Read existing .env.local and preserve non-CodeBakers variables
|
|
318
|
+
let existingContent = '';
|
|
319
|
+
if (existsSync(envPath)) {
|
|
320
|
+
existingContent = readFileSync(envPath, 'utf-8');
|
|
321
|
+
|
|
322
|
+
// Extract lines that aren't CodeBakers-managed
|
|
323
|
+
const existingLines = existingContent.split('\n');
|
|
324
|
+
const preservedLines: string[] = [];
|
|
325
|
+
let inCodeBakersSection = false;
|
|
326
|
+
|
|
327
|
+
for (const line of existingLines) {
|
|
328
|
+
if (line.includes('Generated by CodeBakers CLI')) {
|
|
329
|
+
inCodeBakersSection = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check if line is a CodeBakers-managed env var
|
|
334
|
+
const isCodeBakersVar = Object.values(ENV_VAR_NAMES).some(
|
|
335
|
+
envName => line.startsWith(`${envName}=`) || line.startsWith(`# ${envName}=`)
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (!isCodeBakersVar && !inCodeBakersSection && line.trim()) {
|
|
339
|
+
preservedLines.push(line);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Add preserved lines at the end
|
|
344
|
+
if (preservedLines.length > 0) {
|
|
345
|
+
lines.push('# Existing Configuration');
|
|
346
|
+
lines.push(...preservedLines);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const content = lines.join('\n');
|
|
351
|
+
writeFileSync(envPath, content);
|
|
352
|
+
|
|
353
|
+
const written = Object.values(keys).filter(v => v !== null).length;
|
|
354
|
+
return { written, path: envPath };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check if a key value appears valid (basic format check)
|
|
359
|
+
*/
|
|
360
|
+
export function validateKeyFormat(name: ServiceName, value: string): boolean {
|
|
361
|
+
if (!value || value.length < 8) return false;
|
|
362
|
+
|
|
363
|
+
const prefixes: Partial<Record<ServiceName, string[]>> = {
|
|
364
|
+
github: ['ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_'],
|
|
365
|
+
openai: ['sk-'],
|
|
366
|
+
anthropic: ['sk-ant-'],
|
|
367
|
+
stripe: ['sk_live_', 'sk_test_', 'rk_live_', 'rk_test_'],
|
|
368
|
+
twilio_sid: ['AC'],
|
|
369
|
+
resend: ['re_'],
|
|
370
|
+
supabase: ['sbp_'],
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const validPrefixes = prefixes[name];
|
|
374
|
+
if (validPrefixes) {
|
|
375
|
+
return validPrefixes.some(prefix => value.startsWith(prefix));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return true; // No specific format required
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================
|
|
382
|
+
// Config Path (for debugging)
|
|
383
|
+
// ============================================================
|
|
384
|
+
|
|
385
|
+
export function getConfigPath(): string {
|
|
386
|
+
return config.path;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function getConfigStore(): ConfigSchema {
|
|
390
|
+
return config.store;
|
|
391
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { mcpConfig, mcpUninstall } from './commands/mcp-config.js';
|
|
|
14
14
|
import { setup } from './commands/setup.js';
|
|
15
15
|
import { scaffold } from './commands/scaffold.js';
|
|
16
16
|
import { generate } from './commands/generate.js';
|
|
17
|
+
import { upgrade } from './commands/upgrade.js';
|
|
18
|
+
import { config } from './commands/config.js';
|
|
19
|
+
import { audit } from './commands/audit.js';
|
|
20
|
+
import { heal, healWatch } from './commands/heal.js';
|
|
17
21
|
|
|
18
22
|
// Show welcome message when no command is provided
|
|
19
23
|
function showWelcome(): void {
|
|
@@ -34,7 +38,9 @@ function showWelcome(): void {
|
|
|
34
38
|
|
|
35
39
|
console.log(chalk.white(' Development:\n'));
|
|
36
40
|
console.log(chalk.cyan(' codebakers generate') + chalk.gray(' Generate components, APIs, services'));
|
|
37
|
-
console.log(chalk.cyan(' codebakers
|
|
41
|
+
console.log(chalk.cyan(' codebakers upgrade') + chalk.gray(' Update patterns to latest version'));
|
|
42
|
+
console.log(chalk.cyan(' codebakers status') + chalk.gray(' Check what\'s installed'));
|
|
43
|
+
console.log(chalk.cyan(' codebakers config') + chalk.gray(' View or modify configuration\n'));
|
|
38
44
|
|
|
39
45
|
console.log(chalk.white(' Examples:\n'));
|
|
40
46
|
console.log(chalk.gray(' $ ') + chalk.cyan('codebakers scaffold'));
|
|
@@ -44,8 +50,13 @@ function showWelcome(): void {
|
|
|
44
50
|
console.log(chalk.gray(' $ ') + chalk.cyan('codebakers g api users'));
|
|
45
51
|
console.log(chalk.gray(' Generate a Next.js API route with validation\n'));
|
|
46
52
|
|
|
53
|
+
console.log(chalk.white(' Quality:\n'));
|
|
54
|
+
console.log(chalk.cyan(' codebakers audit') + chalk.gray(' Run automated code quality checks'));
|
|
55
|
+
console.log(chalk.cyan(' codebakers heal') + chalk.gray(' Auto-detect and fix common issues'));
|
|
56
|
+
console.log(chalk.cyan(' codebakers doctor') + chalk.gray(' Check CodeBakers setup\n'));
|
|
57
|
+
|
|
47
58
|
console.log(chalk.white(' All Commands:\n'));
|
|
48
|
-
console.log(chalk.gray(' setup, scaffold, init, generate, status, doctor, login'));
|
|
59
|
+
console.log(chalk.gray(' setup, scaffold, init, generate, upgrade, status, audit, heal, doctor, config, login'));
|
|
49
60
|
console.log(chalk.gray(' install, uninstall, install-hook, uninstall-hook'));
|
|
50
61
|
console.log(chalk.gray(' serve, mcp-config, mcp-uninstall\n'));
|
|
51
62
|
|
|
@@ -57,7 +68,7 @@ const program = new Command();
|
|
|
57
68
|
program
|
|
58
69
|
.name('codebakers')
|
|
59
70
|
.description('CodeBakers CLI - Production patterns for AI-assisted development')
|
|
60
|
-
.version('1.
|
|
71
|
+
.version('1.7.0');
|
|
61
72
|
|
|
62
73
|
// Primary command - one-time setup
|
|
63
74
|
program
|
|
@@ -82,6 +93,16 @@ program
|
|
|
82
93
|
.description('Generate code from templates (component, api, service, hook, page, schema, form)')
|
|
83
94
|
.action((type, name) => generate({ type, name }));
|
|
84
95
|
|
|
96
|
+
program
|
|
97
|
+
.command('upgrade')
|
|
98
|
+
.description('Update patterns to the latest version')
|
|
99
|
+
.action(upgrade);
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('config [action]')
|
|
103
|
+
.description('View or modify CLI configuration (show, path, keys, clear-keys, set-url, reset)')
|
|
104
|
+
.action((action) => config(action));
|
|
105
|
+
|
|
85
106
|
program
|
|
86
107
|
.command('login')
|
|
87
108
|
.description('Login with your API key')
|
|
@@ -117,6 +138,30 @@ program
|
|
|
117
138
|
.description('Check if CodeBakers is set up correctly')
|
|
118
139
|
.action(doctor);
|
|
119
140
|
|
|
141
|
+
program
|
|
142
|
+
.command('audit')
|
|
143
|
+
.description('Run automated code quality and security checks')
|
|
144
|
+
.action(async () => { await audit(); });
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command('heal')
|
|
148
|
+
.description('Auto-detect and fix common issues (TypeScript, deps, security)')
|
|
149
|
+
.option('--auto', 'Automatically apply safe fixes')
|
|
150
|
+
.option('--watch', 'Watch mode - continuously monitor and fix')
|
|
151
|
+
.option('--dry-run', 'Show what would be fixed without applying')
|
|
152
|
+
.option('--severity <level>', 'Filter by severity (critical, high, medium, low)')
|
|
153
|
+
.action(async (options) => {
|
|
154
|
+
if (options.watch) {
|
|
155
|
+
await healWatch();
|
|
156
|
+
} else {
|
|
157
|
+
await heal({
|
|
158
|
+
auto: options.auto,
|
|
159
|
+
dryRun: options.dryRun,
|
|
160
|
+
severity: options.severity
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
120
165
|
// MCP Server commands
|
|
121
166
|
program
|
|
122
167
|
.command('serve')
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getApiUrl } from '../config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API client utilities for CodeBakers CLI
|
|
5
|
+
* This is the single source of truth for API validation and error handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ApiError {
|
|
9
|
+
error: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
recoverySteps?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate an API key format
|
|
16
|
+
*/
|
|
17
|
+
export function isValidApiKeyFormat(apiKey: string): boolean {
|
|
18
|
+
if (!apiKey || typeof apiKey !== 'string') return false;
|
|
19
|
+
// Keys should start with cb_ and be at least 20 characters
|
|
20
|
+
return apiKey.startsWith('cb_') && apiKey.length >= 20;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate an API key against the server
|
|
25
|
+
* Returns true if valid, throws an ApiError if not
|
|
26
|
+
*/
|
|
27
|
+
export async function validateApiKey(apiKey: string): Promise<boolean> {
|
|
28
|
+
// First check format
|
|
29
|
+
if (!isValidApiKeyFormat(apiKey)) {
|
|
30
|
+
throw createApiError('Invalid API key format. Keys start with "cb_"', 'INVALID_FORMAT', [
|
|
31
|
+
'Check that you copied the full API key',
|
|
32
|
+
'Get your API key from: https://codebakers.ai/dashboard',
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const apiUrl = getApiUrl();
|
|
38
|
+
const response = await fetch(`${apiUrl}/api/content`, {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: {
|
|
41
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Parse error response
|
|
50
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
51
|
+
const errorMessage = errorBody.error || errorBody.message || 'API key validation failed';
|
|
52
|
+
|
|
53
|
+
if (response.status === 401) {
|
|
54
|
+
throw createApiError(errorMessage, 'UNAUTHORIZED', [
|
|
55
|
+
'Your API key may have been revoked or expired',
|
|
56
|
+
'Generate a new key at: https://codebakers.ai/dashboard',
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (response.status === 403) {
|
|
61
|
+
throw createApiError(errorMessage, 'FORBIDDEN', [
|
|
62
|
+
'Your subscription may have expired',
|
|
63
|
+
'Check your account status at: https://codebakers.ai/settings',
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw createApiError(errorMessage, 'UNKNOWN', [
|
|
68
|
+
'Try again in a few moments',
|
|
69
|
+
'If the problem persists, contact support',
|
|
70
|
+
]);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// If it's already an ApiError, rethrow it
|
|
73
|
+
if (error && typeof error === 'object' && 'recoverySteps' in error) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Network error
|
|
78
|
+
throw createApiError('Could not connect to CodeBakers server', 'NETWORK_ERROR', [
|
|
79
|
+
'Check your internet connection',
|
|
80
|
+
'Try again in a few moments',
|
|
81
|
+
'If using a VPN or proxy, try disabling it',
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a structured API error
|
|
88
|
+
*/
|
|
89
|
+
export function createApiError(message: string, code: string, recoverySteps: string[]): ApiError {
|
|
90
|
+
return {
|
|
91
|
+
error: message,
|
|
92
|
+
code,
|
|
93
|
+
recoverySteps,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format an API error for display
|
|
99
|
+
*/
|
|
100
|
+
export function formatApiError(error: ApiError): string {
|
|
101
|
+
let output = error.error;
|
|
102
|
+
|
|
103
|
+
if (error.recoverySteps && error.recoverySteps.length > 0) {
|
|
104
|
+
output += '\n\n Try:';
|
|
105
|
+
for (const step of error.recoverySteps) {
|
|
106
|
+
output += `\n • ${step}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if the current API key is valid (for doctor command)
|
|
115
|
+
*/
|
|
116
|
+
export async function checkApiKeyValidity(): Promise<{
|
|
117
|
+
valid: boolean;
|
|
118
|
+
error?: ApiError;
|
|
119
|
+
}> {
|
|
120
|
+
const { getApiKey } = await import('../config.js');
|
|
121
|
+
const apiKey = getApiKey();
|
|
122
|
+
|
|
123
|
+
if (!apiKey) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
error: createApiError('No API key configured', 'NOT_CONFIGURED', [
|
|
127
|
+
'Run: codebakers setup',
|
|
128
|
+
]),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await validateApiKey(apiKey);
|
|
134
|
+
return { valid: true };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
valid: false,
|
|
138
|
+
error: error as ApiError,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the current CLI version
|
|
145
|
+
*/
|
|
146
|
+
export function getCliVersion(): string {
|
|
147
|
+
try {
|
|
148
|
+
// Try to read from package.json
|
|
149
|
+
const packageJson = require('../../package.json');
|
|
150
|
+
return packageJson.version || 'unknown';
|
|
151
|
+
} catch {
|
|
152
|
+
return 'unknown';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if there's a newer version of the CLI available
|
|
158
|
+
*/
|
|
159
|
+
export async function checkForUpdates(): Promise<{
|
|
160
|
+
currentVersion: string;
|
|
161
|
+
latestVersion: string;
|
|
162
|
+
updateAvailable: boolean;
|
|
163
|
+
} | null> {
|
|
164
|
+
try {
|
|
165
|
+
const currentVersion = getCliVersion();
|
|
166
|
+
const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
|
|
167
|
+
headers: { 'Accept': 'application/json' },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) return null;
|
|
171
|
+
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
const latestVersion = data.version;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
currentVersion,
|
|
177
|
+
latestVersion,
|
|
178
|
+
updateAvailable: currentVersion !== latestVersion,
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|