@codebakers/cli 1.6.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/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
- interface ServiceKeys {
6
- github: string | null;
7
- supabase: string | null;
8
- vercel: string | null;
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
- github: null,
26
- supabase: null,
27
- vercel: null,
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
- export type ServiceName = 'github' | 'supabase' | 'vercel';
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 status') + chalk.gray(' Check what\'s installed\n'));
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.6.0');
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
+ }