@codebakers/cli 2.6.2 → 2.8.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.
@@ -0,0 +1,222 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createInterface } from 'readline';
4
+ import { existsSync, readFileSync, readdirSync } from 'fs';
5
+ import { join, basename } from 'path';
6
+ import { getApiUrl, getApiKey } from '../config.js';
7
+
8
+ function prompt(question: string): Promise<string> {
9
+ const rl = createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+
14
+ return new Promise((resolve) => {
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer.trim());
18
+ });
19
+ });
20
+ }
21
+
22
+ interface PushOptions {
23
+ version: string;
24
+ changelog?: string;
25
+ autoPublish?: boolean;
26
+ sourcePath?: string;
27
+ }
28
+
29
+ interface ModulesContent {
30
+ [key: string]: string;
31
+ }
32
+
33
+ /**
34
+ * Read all module files from a directory
35
+ */
36
+ function readModulesFromDir(dirPath: string): ModulesContent {
37
+ const modules: ModulesContent = {};
38
+
39
+ if (!existsSync(dirPath)) {
40
+ return modules;
41
+ }
42
+
43
+ const files = readdirSync(dirPath).filter(f => f.endsWith('.md'));
44
+
45
+ for (const file of files) {
46
+ const filePath = join(dirPath, file);
47
+ const content = readFileSync(filePath, 'utf-8');
48
+ const moduleName = basename(file, '.md');
49
+ modules[moduleName] = content;
50
+ }
51
+
52
+ return modules;
53
+ }
54
+
55
+ /**
56
+ * Push patterns to the server via admin API
57
+ */
58
+ export async function pushPatterns(options: PushOptions): Promise<void> {
59
+ console.log(chalk.blue('\n CodeBakers Push Patterns\n'));
60
+
61
+ const sourcePath = options.sourcePath || process.cwd();
62
+
63
+ // Check for API key - first from config, then from environment
64
+ const apiKey = getApiKey() || process.env.CODEBAKERS_ADMIN_KEY;
65
+ if (!apiKey) {
66
+ console.log(chalk.red(' ✗ No API key found\n'));
67
+ console.log(chalk.gray(' Either:'));
68
+ console.log(chalk.cyan(' 1. Run `codebakers setup` to configure your API key'));
69
+ console.log(chalk.cyan(' 2. Set CODEBAKERS_ADMIN_KEY environment variable\n'));
70
+ process.exit(1);
71
+ }
72
+
73
+ // Read main files
74
+ const claudeMdPath = join(sourcePath, 'CLAUDE.md');
75
+ const cursorRulesPath = join(sourcePath, '.cursorrules');
76
+
77
+ let claudeMdContent: string | null = null;
78
+ let cursorRulesContent: string | null = null;
79
+
80
+ if (existsSync(claudeMdPath)) {
81
+ claudeMdContent = readFileSync(claudeMdPath, 'utf-8');
82
+ console.log(chalk.green(` ✓ Found CLAUDE.md (${(claudeMdContent.length / 1024).toFixed(1)} KB)`));
83
+ } else {
84
+ console.log(chalk.yellow(` ⚠ CLAUDE.md not found at ${claudeMdPath}`));
85
+ }
86
+
87
+ if (existsSync(cursorRulesPath)) {
88
+ cursorRulesContent = readFileSync(cursorRulesPath, 'utf-8');
89
+ console.log(chalk.green(` ✓ Found .cursorrules (${(cursorRulesContent.length / 1024).toFixed(1)} KB)`));
90
+ } else {
91
+ console.log(chalk.yellow(` ⚠ .cursorrules not found at ${cursorRulesPath}`));
92
+ }
93
+
94
+ if (!claudeMdContent && !cursorRulesContent) {
95
+ console.log(chalk.red('\n ✗ No pattern files found. Nothing to push.\n'));
96
+ process.exit(1);
97
+ }
98
+
99
+ // Read module directories
100
+ const claudeModulesPath = join(sourcePath, '.claude');
101
+ const cursorModulesPath = join(sourcePath, '.cursorrules-modules');
102
+
103
+ const modulesContent = readModulesFromDir(claudeModulesPath);
104
+ const cursorModulesContent = readModulesFromDir(cursorModulesPath);
105
+
106
+ const claudeModuleCount = Object.keys(modulesContent).length;
107
+ const cursorModuleCount = Object.keys(cursorModulesContent).length;
108
+
109
+ if (claudeModuleCount > 0) {
110
+ console.log(chalk.green(` ✓ Found ${claudeModuleCount} Claude modules in .claude/`));
111
+ }
112
+
113
+ if (cursorModuleCount > 0) {
114
+ console.log(chalk.green(` ✓ Found ${cursorModuleCount} Cursor modules in .cursorrules-modules/`));
115
+ }
116
+
117
+ console.log('');
118
+
119
+ // Show summary
120
+ console.log(chalk.white(' Push Summary:\n'));
121
+ console.log(chalk.gray(` Version: ${chalk.cyan(options.version)}`));
122
+ console.log(chalk.gray(` Auto-publish: ${options.autoPublish ? chalk.green('Yes') : chalk.yellow('No')}`));
123
+ if (options.changelog) {
124
+ console.log(chalk.gray(` Changelog: ${chalk.dim(options.changelog.slice(0, 60))}...`));
125
+ }
126
+ console.log('');
127
+
128
+ // Files to upload
129
+ console.log(chalk.white(' Files to upload:\n'));
130
+ if (claudeMdContent) console.log(chalk.cyan(' • CLAUDE.md'));
131
+ if (cursorRulesContent) console.log(chalk.cyan(' • .cursorrules'));
132
+ for (const mod of Object.keys(modulesContent)) {
133
+ console.log(chalk.dim(` • .claude/${mod}.md`));
134
+ }
135
+ for (const mod of Object.keys(cursorModulesContent)) {
136
+ console.log(chalk.dim(` • .cursorrules-modules/${mod}.md`));
137
+ }
138
+ console.log('');
139
+
140
+ // Confirm
141
+ const confirm = await prompt(chalk.gray(' Push these patterns? (y/N): '));
142
+ if (confirm.toLowerCase() !== 'y') {
143
+ console.log(chalk.gray('\n Cancelled.\n'));
144
+ process.exit(0);
145
+ }
146
+
147
+ // Push to server
148
+ const spinner = ora('Pushing patterns to server...').start();
149
+
150
+ try {
151
+ const apiUrl = getApiUrl();
152
+ const response = await fetch(`${apiUrl}/api/admin/content/push`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': `Bearer ${apiKey}`,
157
+ },
158
+ body: JSON.stringify({
159
+ version: options.version,
160
+ claudeMdContent,
161
+ cursorRulesContent,
162
+ modulesContent: claudeModuleCount > 0 ? modulesContent : undefined,
163
+ cursorModulesContent: cursorModuleCount > 0 ? cursorModulesContent : undefined,
164
+ changelog: options.changelog,
165
+ autoPublish: options.autoPublish,
166
+ }),
167
+ });
168
+
169
+ const data = await response.json();
170
+
171
+ if (!response.ok) {
172
+ spinner.fail(chalk.red(`Push failed: ${data.error || 'Unknown error'}`));
173
+ process.exit(1);
174
+ }
175
+
176
+ spinner.succeed(chalk.green('Patterns pushed successfully!'));
177
+
178
+ console.log(chalk.white('\n Result:\n'));
179
+ console.log(chalk.gray(` Version ID: ${chalk.cyan(data.version?.id || 'N/A')}`));
180
+ console.log(chalk.gray(` Version: ${chalk.cyan(data.version?.version || options.version)}`));
181
+ console.log(chalk.gray(` Published: ${data.published ? chalk.green('Yes') : chalk.yellow('No - run publish manually')}`));
182
+ console.log('');
183
+ console.log(chalk.green(` ${data.message}\n`));
184
+
185
+ } catch (error) {
186
+ spinner.fail(chalk.red('Failed to connect to server'));
187
+ console.log(chalk.gray(`\n Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`));
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Interactive push command
194
+ */
195
+ export async function pushPatternsInteractive(): Promise<void> {
196
+ console.log(chalk.blue('\n CodeBakers Push Patterns\n'));
197
+
198
+ // Get version
199
+ const version = await prompt(chalk.cyan(' Version (e.g., 4.5): '));
200
+ if (!version) {
201
+ console.log(chalk.red('\n Version is required.\n'));
202
+ process.exit(1);
203
+ }
204
+
205
+ // Get changelog
206
+ const changelog = await prompt(chalk.cyan(' Changelog (optional): '));
207
+
208
+ // Ask about auto-publish
209
+ const autoPublishAnswer = await prompt(chalk.cyan(' Auto-publish? (y/N): '));
210
+ const autoPublish = autoPublishAnswer.toLowerCase() === 'y';
211
+
212
+ // Get source path
213
+ const sourcePathAnswer = await prompt(chalk.cyan(' Source path (Enter for current directory): '));
214
+ const sourcePath = sourcePathAnswer || process.cwd();
215
+
216
+ await pushPatterns({
217
+ version,
218
+ changelog: changelog || undefined,
219
+ autoPublish,
220
+ sourcePath,
221
+ });
222
+ }
@@ -4,6 +4,7 @@ import { createInterface } from 'readline';
4
4
  import { execSync } from 'child_process';
5
5
  import { setApiKey, getApiKey, getApiUrl, syncServiceKeys, SERVICE_KEY_LABELS, type ServiceName } from '../config.js';
6
6
  import { validateApiKey, formatApiError, checkForUpdates, getCliVersion, type ApiError } from '../lib/api.js';
7
+ import { showQuickStartGuide, formatFriendlyError, getNetworkError, getAuthError } from '../lib/progress.js';
7
8
 
8
9
  function prompt(question: string): Promise<string> {
9
10
  const rl = createInterface({
@@ -67,8 +68,16 @@ export async function setup(): Promise<void> {
67
68
  } catch (error) {
68
69
  spinner.fail('Invalid API key');
69
70
 
70
- if (error && typeof error === 'object' && 'recoverySteps' in error) {
71
- console.log(chalk.red(`\n ${formatApiError(error as ApiError)}\n`));
71
+ // Use friendly error formatting
72
+ if (error && typeof error === 'object' && 'code' in error) {
73
+ const code = (error as { code: string }).code;
74
+ if (code === 'NETWORK_ERROR') {
75
+ console.log(formatFriendlyError(getNetworkError()));
76
+ } else if (code === 'UNAUTHORIZED' || code === 'INVALID_FORMAT') {
77
+ console.log(formatFriendlyError(getAuthError()));
78
+ } else if ('recoverySteps' in error) {
79
+ console.log(chalk.red(`\n ${formatApiError(error as ApiError)}\n`));
80
+ }
72
81
  } else {
73
82
  const message = error instanceof Error ? error.message : 'API key validation failed';
74
83
  console.log(chalk.red(`\n ${message}\n`));
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import { upgrade } from './commands/upgrade.js';
18
18
  import { config } from './commands/config.js';
19
19
  import { audit } from './commands/audit.js';
20
20
  import { heal, healWatch } from './commands/heal.js';
21
+ import { pushPatterns, pushPatternsInteractive } from './commands/push-patterns.js';
21
22
 
22
23
  // Show welcome message when no command is provided
23
24
  function showWelcome(): void {
@@ -68,7 +69,7 @@ const program = new Command();
68
69
  program
69
70
  .name('codebakers')
70
71
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
71
- .version('1.7.0');
72
+ .version('2.8.0');
72
73
 
73
74
  // Primary command - one-time setup
74
75
  program
@@ -162,6 +163,27 @@ program
162
163
  }
163
164
  });
164
165
 
166
+ // Admin commands
167
+ program
168
+ .command('push-patterns')
169
+ .description('Push pattern files to the server (admin only)')
170
+ .option('-v, --version <version>', 'Version number (e.g., 4.5)')
171
+ .option('-c, --changelog <message>', 'Changelog message')
172
+ .option('-p, --publish', 'Auto-publish after push')
173
+ .option('-s, --source <path>', 'Source directory (default: current directory)')
174
+ .action(async (options) => {
175
+ if (options.version) {
176
+ await pushPatterns({
177
+ version: options.version,
178
+ changelog: options.changelog,
179
+ autoPublish: options.publish,
180
+ sourcePath: options.source,
181
+ });
182
+ } else {
183
+ await pushPatternsInteractive();
184
+ }
185
+ });
186
+
165
187
  // MCP Server commands
166
188
  program
167
189
  .command('serve')
@@ -0,0 +1,192 @@
1
+ import chalk from 'chalk';
2
+ import ora, { Ora } from 'ora';
3
+
4
+ /**
5
+ * Multi-step progress tracker for CLI operations
6
+ */
7
+ export interface Step {
8
+ name: string;
9
+ action: () => Promise<void>;
10
+ }
11
+
12
+ export class ProgressTracker {
13
+ private currentStep = 0;
14
+ private totalSteps: number;
15
+ private steps: Step[];
16
+ private spinner: Ora | null = null;
17
+
18
+ constructor(steps: Step[]) {
19
+ this.steps = steps;
20
+ this.totalSteps = steps.length;
21
+ }
22
+
23
+ async run(): Promise<void> {
24
+ for (let i = 0; i < this.steps.length; i++) {
25
+ this.currentStep = i + 1;
26
+ const step = this.steps[i];
27
+
28
+ this.spinner = ora({
29
+ text: this.formatStepText(step.name),
30
+ prefixText: this.formatPrefix(),
31
+ }).start();
32
+
33
+ try {
34
+ await step.action();
35
+ this.spinner.succeed(this.formatStepText(step.name));
36
+ } catch (error) {
37
+ this.spinner.fail(this.formatStepText(step.name));
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+
43
+ private formatPrefix(): string {
44
+ return chalk.gray(` [${this.currentStep}/${this.totalSteps}]`);
45
+ }
46
+
47
+ private formatStepText(text: string): string {
48
+ return text;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Show a success box after completing a command
54
+ */
55
+ export function showSuccessBox(title: string, lines: string[]): void {
56
+ const maxLength = Math.max(title.length, ...lines.map(l => l.length)) + 4;
57
+ const border = '═'.repeat(maxLength);
58
+
59
+ console.log(chalk.green(`\n ╔${border}╗`));
60
+ console.log(chalk.green(` ║`) + chalk.bold.white(` ${title.padEnd(maxLength - 2)} `) + chalk.green(`║`));
61
+ console.log(chalk.green(` ╠${border}╣`));
62
+
63
+ for (const line of lines) {
64
+ console.log(chalk.green(` ║`) + chalk.white(` ${line.padEnd(maxLength - 2)} `) + chalk.green(`║`));
65
+ }
66
+
67
+ console.log(chalk.green(` ╚${border}╝\n`));
68
+ }
69
+
70
+ /**
71
+ * Display a quick start guide with examples
72
+ */
73
+ export function showQuickStartGuide(): void {
74
+ console.log(chalk.blue('\n ═══════════════════════════════════════════════════════════'));
75
+ console.log(chalk.bold.white('\n 📚 Quick Start Guide\n'));
76
+ console.log(chalk.blue(' ═══════════════════════════════════════════════════════════\n'));
77
+
78
+ console.log(chalk.white(' Just ask the AI in natural language:\n'));
79
+
80
+ const examples = [
81
+ {
82
+ prompt: '"Build me a user dashboard with stats cards"',
83
+ desc: 'AI will use CodeBakers patterns for layout, components, and data fetching',
84
+ },
85
+ {
86
+ prompt: '"Add Stripe subscription to my app"',
87
+ desc: 'AI loads payment patterns: checkout, webhooks, customer portal',
88
+ },
89
+ {
90
+ prompt: '"Create an API endpoint for user profiles"',
91
+ desc: 'AI generates: route handler, validation, error handling, types',
92
+ },
93
+ {
94
+ prompt: '"Set up auth with Google and email login"',
95
+ desc: 'AI implements OAuth + email auth following security patterns',
96
+ },
97
+ ];
98
+
99
+ for (const example of examples) {
100
+ console.log(chalk.cyan(` → ${example.prompt}`));
101
+ console.log(chalk.gray(` ${example.desc}\n`));
102
+ }
103
+
104
+ console.log(chalk.blue(' ───────────────────────────────────────────────────────────\n'));
105
+
106
+ console.log(chalk.white(' Pro Tips:\n'));
107
+ console.log(chalk.gray(' • Ask the AI ') + chalk.cyan('"What patterns do you have for payments?"'));
108
+ console.log(chalk.gray(' • Say ') + chalk.cyan('"/build a SaaS app"') + chalk.gray(' to start a full project'));
109
+ console.log(chalk.gray(' • Try ') + chalk.cyan('"/audit"') + chalk.gray(' to check code quality'));
110
+ console.log(chalk.gray(' • Use ') + chalk.cyan('"codebakers doctor"') + chalk.gray(' if something seems wrong\n'));
111
+
112
+ console.log(chalk.blue(' ═══════════════════════════════════════════════════════════\n'));
113
+ }
114
+
115
+ /**
116
+ * Format user-friendly error with context and recovery steps
117
+ */
118
+ export interface FriendlyError {
119
+ title: string;
120
+ message: string;
121
+ cause?: string;
122
+ recovery: string[];
123
+ helpUrl?: string;
124
+ }
125
+
126
+ export function formatFriendlyError(error: FriendlyError): string {
127
+ let output = '';
128
+
129
+ output += chalk.red(`\n ❌ ${error.title}\n`);
130
+ output += chalk.white(`\n ${error.message}\n`);
131
+
132
+ if (error.cause) {
133
+ output += chalk.gray(`\n Why: ${error.cause}\n`);
134
+ }
135
+
136
+ if (error.recovery.length > 0) {
137
+ output += chalk.yellow(`\n How to fix:\n`);
138
+ for (let i = 0; i < error.recovery.length; i++) {
139
+ output += chalk.white(` ${i + 1}. ${error.recovery[i]}\n`);
140
+ }
141
+ }
142
+
143
+ if (error.helpUrl) {
144
+ output += chalk.gray(`\n More help: ${error.helpUrl}\n`);
145
+ }
146
+
147
+ return output;
148
+ }
149
+
150
+ /**
151
+ * Common error handlers with friendly messages
152
+ */
153
+ export function getNetworkError(): FriendlyError {
154
+ return {
155
+ title: 'Connection Failed',
156
+ message: 'Could not connect to CodeBakers servers.',
157
+ cause: 'This usually means a network issue or the server is temporarily unavailable.',
158
+ recovery: [
159
+ 'Check your internet connection',
160
+ 'Try again in a few seconds',
161
+ 'If using a VPN, try disabling it temporarily',
162
+ 'Check status.codebakers.ai for server status',
163
+ ],
164
+ helpUrl: 'https://codebakers.ai/docs/troubleshooting',
165
+ };
166
+ }
167
+
168
+ export function getAuthError(): FriendlyError {
169
+ return {
170
+ title: 'Authentication Failed',
171
+ message: 'Your API key is invalid or expired.',
172
+ recovery: [
173
+ 'Go to codebakers.ai/dashboard',
174
+ 'Copy your API key (starts with cb_)',
175
+ 'Run: codebakers setup',
176
+ ],
177
+ helpUrl: 'https://codebakers.ai/docs/getting-started',
178
+ };
179
+ }
180
+
181
+ export function getSubscriptionError(): FriendlyError {
182
+ return {
183
+ title: 'Subscription Required',
184
+ message: 'This feature requires an active subscription.',
185
+ recovery: [
186
+ 'Visit codebakers.ai/pricing to see plans',
187
+ 'Start a free trial with: codebakers setup',
188
+ 'Contact support if you believe this is an error',
189
+ ],
190
+ helpUrl: 'https://codebakers.ai/pricing',
191
+ };
192
+ }