@codebakers/cli 3.2.0 → 3.3.1

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/index.ts CHANGED
@@ -22,43 +22,46 @@ import { pushPatterns, pushPatternsInteractive } from './commands/push-patterns.
22
22
  import { go } from './commands/go.js';
23
23
  import { extend } from './commands/extend.js';
24
24
  import { billing } from './commands/billing.js';
25
- import { getCachedUpdateInfo, setCachedUpdateInfo, getCliVersion } from './config.js';
25
+ import { getCachedUpdateInfo, setCachedUpdateInfo, getCliVersion, getCachedPatternInfo, setCachedPatternInfo, getApiKey, getApiUrl, getTrialState, hasValidAccess } from './config.js';
26
+ import { checkForUpdates } from './lib/api.js';
27
+ import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
28
+ import { join } from 'path';
26
29
 
27
30
  // ============================================
28
31
  // Automatic Update Notification
29
32
  // ============================================
30
33
 
31
- const CURRENT_VERSION = '3.2.0';
34
+ const CURRENT_VERSION = '3.3.1';
32
35
 
33
36
  async function checkForUpdatesInBackground(): Promise<void> {
34
37
  // Check if we have a valid cached result first (fast path)
35
38
  const cached = getCachedUpdateInfo();
36
39
  if (cached) {
37
40
  if (cached.latestVersion !== CURRENT_VERSION) {
38
- showUpdateBanner(CURRENT_VERSION, cached.latestVersion);
41
+ showUpdateBanner(CURRENT_VERSION, cached.latestVersion, false);
39
42
  }
40
43
  return;
41
44
  }
42
45
 
43
- // Fetch from npm registry (with timeout to not block CLI)
46
+ // Use the API-based version check (with controlled rollout support)
44
47
  try {
45
- const controller = new AbortController();
46
- const timeout = setTimeout(() => controller.abort(), 3000);
47
-
48
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
49
- headers: { 'Accept': 'application/json' },
50
- signal: controller.signal,
51
- });
48
+ const updateInfo = await checkForUpdates();
52
49
 
53
- clearTimeout(timeout);
50
+ if (updateInfo) {
51
+ setCachedUpdateInfo(updateInfo.latestVersion);
54
52
 
55
- if (response.ok) {
56
- const data = await response.json();
57
- const latestVersion = data.version;
58
- setCachedUpdateInfo(latestVersion);
53
+ // Show blocked version warning first (critical)
54
+ if (updateInfo.isBlocked) {
55
+ showBlockedVersionWarning(updateInfo.currentVersion, updateInfo.latestVersion);
56
+ return;
57
+ }
59
58
 
60
- if (latestVersion !== CURRENT_VERSION) {
61
- showUpdateBanner(CURRENT_VERSION, latestVersion);
59
+ // Only show update banner if auto-update is enabled for this version
60
+ if (updateInfo.autoUpdateEnabled && updateInfo.autoUpdateVersion) {
61
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.autoUpdateVersion, true);
62
+ } else if (updateInfo.updateAvailable) {
63
+ // Update available but not auto-update enabled - show regular banner
64
+ showUpdateBanner(updateInfo.currentVersion, updateInfo.latestVersion, false);
62
65
  }
63
66
  }
64
67
  } catch {
@@ -66,11 +69,27 @@ async function checkForUpdatesInBackground(): Promise<void> {
66
69
  }
67
70
  }
68
71
 
69
- function showUpdateBanner(currentVersion: string, latestVersion: string): void {
72
+ function showBlockedVersionWarning(currentVersion: string, recommendedVersion: string): void {
73
+ console.log(chalk.red(`
74
+ ╭─────────────────────────────────────────────────────────╮
75
+ │ │
76
+ │ ${chalk.bold('⚠️ VERSION BLOCKED')} │
77
+ │ │
78
+ │ Your CLI version ${chalk.gray(currentVersion)} has critical issues. │
79
+ │ Please update immediately to ${chalk.green(recommendedVersion)} │
80
+ │ │
81
+ │ Run ${chalk.cyan('npm i -g @codebakers/cli@latest')} to update │
82
+ │ │
83
+ ╰─────────────────────────────────────────────────────────╯
84
+ `));
85
+ }
86
+
87
+ function showUpdateBanner(currentVersion: string, latestVersion: string, isRecommended: boolean): void {
88
+ const updateType = isRecommended ? chalk.green('Recommended update') : chalk.bold('Update available!');
70
89
  console.log(chalk.yellow(`
71
90
  ╭─────────────────────────────────────────────────────────╮
72
91
  │ │
73
- │ ${chalk.bold('Update available!')} ${chalk.gray(currentVersion)} → ${chalk.green(latestVersion)} │
92
+ │ ${updateType} ${chalk.gray(currentVersion)} → ${chalk.green(latestVersion)} │
74
93
  │ │
75
94
  │ Run ${chalk.cyan('npm i -g @codebakers/cli@latest')} to update │
76
95
  │ │
@@ -78,6 +97,181 @@ function showUpdateBanner(currentVersion: string, latestVersion: string): void {
78
97
  `));
79
98
  }
80
99
 
100
+ // ============================================
101
+ // Automatic Pattern Updates
102
+ // ============================================
103
+
104
+ interface PatternVersionInfo {
105
+ version: string;
106
+ moduleCount: number;
107
+ updatedAt: string;
108
+ cliVersion: string;
109
+ }
110
+
111
+ interface ContentResponse {
112
+ version: string;
113
+ router: string;
114
+ modules: Record<string, string>;
115
+ }
116
+
117
+ function getLocalPatternVersion(): string | null {
118
+ const cwd = process.cwd();
119
+ const versionFile = join(cwd, '.claude', '.version.json');
120
+
121
+ if (!existsSync(versionFile)) return null;
122
+
123
+ try {
124
+ const content = readFileSync(versionFile, 'utf-8');
125
+ const info: PatternVersionInfo = JSON.parse(content);
126
+ return info.version;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function isCodeBakersProject(): boolean {
133
+ const cwd = process.cwd();
134
+ return existsSync(join(cwd, 'CLAUDE.md')) || existsSync(join(cwd, '.claude'));
135
+ }
136
+
137
+ async function autoUpdatePatterns(): Promise<void> {
138
+ // Only auto-update if this is a CodeBakers project
139
+ if (!isCodeBakersProject()) return;
140
+
141
+ // Only auto-update if user has valid access
142
+ if (!hasValidAccess()) return;
143
+
144
+ const localVersion = getLocalPatternVersion();
145
+
146
+ // Check if we have a valid cached result first (fast path)
147
+ const cached = getCachedPatternInfo();
148
+ if (cached) {
149
+ // If local matches latest, nothing to do
150
+ if (localVersion === cached.latestVersion) return;
151
+ // If we know there's an update but haven't updated yet, do it now
152
+ if (localVersion !== cached.latestVersion) {
153
+ await performPatternUpdate(cached.latestVersion);
154
+ }
155
+ return;
156
+ }
157
+
158
+ // Fetch from server to check for updates (with timeout)
159
+ try {
160
+ const controller = new AbortController();
161
+ const timeout = setTimeout(() => controller.abort(), 5000);
162
+
163
+ const apiUrl = getApiUrl();
164
+ const apiKey = getApiKey();
165
+ const trial = getTrialState();
166
+
167
+ // Build authorization header
168
+ let authHeader = '';
169
+ if (apiKey) {
170
+ authHeader = `Bearer ${apiKey}`;
171
+ } else if (trial?.trialId) {
172
+ authHeader = `Trial ${trial.trialId}`;
173
+ }
174
+
175
+ if (!authHeader) return;
176
+
177
+ // First, check the version endpoint (lightweight)
178
+ const versionResponse = await fetch(`${apiUrl}/api/content/version`, {
179
+ method: 'GET',
180
+ headers: {
181
+ 'Authorization': authHeader,
182
+ },
183
+ signal: controller.signal,
184
+ });
185
+
186
+ clearTimeout(timeout);
187
+
188
+ if (versionResponse.ok) {
189
+ const versionData = await versionResponse.json();
190
+ const serverVersion = versionData.version;
191
+
192
+ // Cache the version info
193
+ setCachedPatternInfo(serverVersion);
194
+
195
+ // If local version is different, update
196
+ if (localVersion !== serverVersion) {
197
+ await performPatternUpdate(serverVersion);
198
+ }
199
+ }
200
+ } catch {
201
+ // Silently fail - don't block CLI for pattern check
202
+ }
203
+ }
204
+
205
+ async function performPatternUpdate(targetVersion: string): Promise<void> {
206
+ const cwd = process.cwd();
207
+ const claudeMdPath = join(cwd, 'CLAUDE.md');
208
+ const claudeDir = join(cwd, '.claude');
209
+
210
+ try {
211
+ const apiUrl = getApiUrl();
212
+ const apiKey = getApiKey();
213
+ const trial = getTrialState();
214
+
215
+ let authHeader = '';
216
+ if (apiKey) {
217
+ authHeader = `Bearer ${apiKey}`;
218
+ } else if (trial?.trialId) {
219
+ authHeader = `Trial ${trial.trialId}`;
220
+ }
221
+
222
+ if (!authHeader) return;
223
+
224
+ const controller = new AbortController();
225
+ const timeout = setTimeout(() => controller.abort(), 10000);
226
+
227
+ const response = await fetch(`${apiUrl}/api/content`, {
228
+ method: 'GET',
229
+ headers: {
230
+ 'Authorization': authHeader,
231
+ },
232
+ signal: controller.signal,
233
+ });
234
+
235
+ clearTimeout(timeout);
236
+
237
+ if (!response.ok) return;
238
+
239
+ const content: ContentResponse = await response.json();
240
+
241
+ // Update CLAUDE.md
242
+ if (content.router) {
243
+ writeFileSync(claudeMdPath, content.router);
244
+ }
245
+
246
+ // Update pattern modules
247
+ if (content.modules && Object.keys(content.modules).length > 0) {
248
+ if (!existsSync(claudeDir)) {
249
+ mkdirSync(claudeDir, { recursive: true });
250
+ }
251
+
252
+ for (const [name, data] of Object.entries(content.modules)) {
253
+ writeFileSync(join(claudeDir, name), data);
254
+ }
255
+ }
256
+
257
+ // Write version file
258
+ const moduleCount = Object.keys(content.modules || {}).length;
259
+ const versionInfo: PatternVersionInfo = {
260
+ version: content.version,
261
+ moduleCount,
262
+ updatedAt: new Date().toISOString(),
263
+ cliVersion: getCliVersion(),
264
+ };
265
+ writeFileSync(join(claudeDir, '.version.json'), JSON.stringify(versionInfo, null, 2));
266
+
267
+ // Show subtle notification
268
+ console.log(chalk.green(` ✓ Patterns auto-updated to v${content.version} (${moduleCount} modules)\n`));
269
+
270
+ } catch {
271
+ // Silently fail - don't block the user
272
+ }
273
+ }
274
+
81
275
  // Show welcome message when no command is provided
82
276
  function showWelcome(): void {
83
277
  console.log(chalk.blue(`
@@ -127,7 +321,7 @@ const program = new Command();
127
321
  program
128
322
  .name('codebakers')
129
323
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
130
- .version('3.2.0');
324
+ .version('3.3.0');
131
325
 
132
326
  // Zero-friction trial entry (no signup required)
133
327
  program
@@ -282,7 +476,11 @@ program
282
476
 
283
477
  // Add update check hook (runs before every command)
284
478
  program.hook('preAction', async () => {
285
- await checkForUpdatesInBackground();
479
+ // Run CLI update check and pattern auto-update in parallel
480
+ await Promise.all([
481
+ checkForUpdatesInBackground(),
482
+ autoUpdatePatterns(),
483
+ ]);
286
484
  });
287
485
 
288
486
  // Show welcome if no command provided
package/src/lib/api.ts CHANGED
@@ -155,29 +155,108 @@ export function getCliVersion(): string {
155
155
 
156
156
  /**
157
157
  * Check if there's a newer version of the CLI available
158
+ * Uses the CodeBakers API for controlled rollouts (only recommends stable, tested versions)
159
+ * Falls back to npm registry if API is unavailable
158
160
  */
159
161
  export async function checkForUpdates(): Promise<{
160
162
  currentVersion: string;
161
163
  latestVersion: string;
162
164
  updateAvailable: boolean;
165
+ autoUpdateEnabled: boolean;
166
+ autoUpdateVersion: string | null;
167
+ isBlocked: boolean;
163
168
  } | null> {
164
169
  try {
165
170
  const currentVersion = getCliVersion();
166
- const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
171
+ const apiUrl = getApiUrl();
172
+
173
+ // First, try the CodeBakers API for controlled rollout info
174
+ try {
175
+ const response = await fetch(`${apiUrl}/api/cli/version`, {
176
+ headers: {
177
+ 'Accept': 'application/json',
178
+ 'X-CLI-Version': currentVersion,
179
+ },
180
+ });
181
+
182
+ if (response.ok) {
183
+ const data = await response.json();
184
+ const latestVersion = data.stableVersion || data.latestVersion;
185
+ const autoUpdateVersion = data.autoUpdateVersion;
186
+ const isBlocked = data.isBlocked === true;
187
+
188
+ return {
189
+ currentVersion,
190
+ latestVersion,
191
+ updateAvailable: latestVersion !== currentVersion,
192
+ autoUpdateEnabled: data.autoUpdateEnabled === true,
193
+ autoUpdateVersion: autoUpdateVersion || null,
194
+ isBlocked,
195
+ };
196
+ }
197
+ } catch {
198
+ // API unavailable, fall through to npm
199
+ }
200
+
201
+ // Fallback: check npm registry directly
202
+ const npmResponse = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
167
203
  headers: { 'Accept': 'application/json' },
168
204
  });
169
205
 
170
- if (!response.ok) return null;
206
+ if (!npmResponse.ok) return null;
171
207
 
172
- const data = await response.json();
173
- const latestVersion = data.version;
208
+ const npmData = await npmResponse.json();
209
+ const latestVersion = npmData.version;
174
210
 
175
211
  return {
176
212
  currentVersion,
177
213
  latestVersion,
178
214
  updateAvailable: currentVersion !== latestVersion,
215
+ autoUpdateEnabled: false, // npm fallback doesn't have controlled rollout
216
+ autoUpdateVersion: null,
217
+ isBlocked: false,
179
218
  };
180
219
  } catch {
181
220
  return null;
182
221
  }
183
222
  }
223
+
224
+ /**
225
+ * Report a CLI error to the server for tracking
226
+ * This helps identify problematic versions for blocking
227
+ */
228
+ export async function reportCliError(
229
+ errorType: string,
230
+ errorMessage: string,
231
+ context?: Record<string, unknown>
232
+ ): Promise<void> {
233
+ try {
234
+ const apiUrl = getApiUrl();
235
+ const cliVersion = getCliVersion();
236
+
237
+ // Fire and forget - don't block on error reporting
238
+ fetch(`${apiUrl}/api/cli/error-report`, {
239
+ method: 'POST',
240
+ headers: {
241
+ 'Content-Type': 'application/json',
242
+ 'X-CLI-Version': cliVersion,
243
+ },
244
+ body: JSON.stringify({
245
+ cliVersion,
246
+ errorType,
247
+ errorMessage,
248
+ stackTrace: context?.stack,
249
+ context: JSON.stringify({
250
+ ...context,
251
+ nodeVersion: process.version,
252
+ platform: process.platform,
253
+ arch: process.arch,
254
+ }),
255
+ }),
256
+ }).catch(() => {
257
+ // Ignore reporting failures
258
+ });
259
+ } catch {
260
+ // Never fail on error reporting
261
+ }
262
+ }
package/src/mcp/server.ts CHANGED
@@ -108,6 +108,31 @@ class CodeBakersServer {
108
108
  return {};
109
109
  }
110
110
 
111
+ /**
112
+ * Confirm download to server (non-blocking analytics)
113
+ */
114
+ private async confirmDownload(version: string, moduleCount: number): Promise<void> {
115
+ try {
116
+ const headers: Record<string, string> = {
117
+ 'Content-Type': 'application/json',
118
+ ...this.getAuthHeaders(),
119
+ };
120
+
121
+ await fetch(`${this.apiUrl}/api/content/confirm`, {
122
+ method: 'POST',
123
+ headers,
124
+ body: JSON.stringify({
125
+ version,
126
+ moduleCount,
127
+ cliVersion: getCliVersion(),
128
+ command: 'auto-update',
129
+ }),
130
+ });
131
+ } catch {
132
+ // Silently ignore - this is just for analytics
133
+ }
134
+ }
135
+
111
136
  /**
112
137
  * Automatically check for and apply pattern updates
113
138
  * Runs silently in background - no user intervention needed
@@ -213,6 +238,21 @@ class CodeBakersServer {
213
238
  };
214
239
  fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2));
215
240
 
241
+ // Write notification file for AI to read and show to user
242
+ const notificationPath = path.join(claudeDir, '.update-notification.json');
243
+ const notification = {
244
+ type: 'patterns_updated',
245
+ previousVersion: installed?.version || 'unknown',
246
+ newVersion: content.version,
247
+ moduleCount,
248
+ updatedAt: new Date().toISOString(),
249
+ message: `CodeBakers patterns have been automatically updated from v${installed?.version || 'unknown'} to v${content.version} (${moduleCount} modules). Your AI tools now have the latest production patterns.`,
250
+ };
251
+ fs.writeFileSync(notificationPath, JSON.stringify(notification, null, 2));
252
+
253
+ // Confirm to server (non-blocking, fire-and-forget)
254
+ this.confirmDownload(content.version, moduleCount).catch(() => {});
255
+
216
256
  this.autoUpdateChecked = true;
217
257
  this.autoUpdateInProgress = false;
218
258
 
@@ -984,6 +1024,15 @@ class CodeBakersServer {
984
1024
  required: ['request'],
985
1025
  },
986
1026
  },
1027
+ {
1028
+ name: 'check_update_notification',
1029
+ description:
1030
+ 'ALWAYS CALL THIS AT THE START OF EACH SESSION. Checks if CodeBakers patterns were recently auto-updated and returns a notification message to show the user. If an update occurred, tell the user about it with the returned message. After showing, the notification is cleared.',
1031
+ inputSchema: {
1032
+ type: 'object' as const,
1033
+ properties: {},
1034
+ },
1035
+ },
987
1036
  ],
988
1037
  }));
989
1038
 
@@ -1111,6 +1160,9 @@ class CodeBakersServer {
1111
1160
  case 'add_api_route':
1112
1161
  return this.handleAddApiRoute(args as { request: string });
1113
1162
 
1163
+ case 'check_update_notification':
1164
+ return this.handleCheckUpdateNotification();
1165
+
1114
1166
  default:
1115
1167
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1116
1168
  }
@@ -4839,6 +4891,44 @@ export default ${asyncKeyword}function ${pageName}Page() {${authCheck}
4839
4891
  };
4840
4892
  }
4841
4893
 
4894
+ /**
4895
+ * Check for update notifications and return message to show user
4896
+ */
4897
+ private async handleCheckUpdateNotification() {
4898
+ const cwd = process.cwd();
4899
+ const notificationPath = path.join(cwd, '.claude', '.update-notification.json');
4900
+
4901
+ try {
4902
+ if (!fs.existsSync(notificationPath)) {
4903
+ return {
4904
+ content: [{
4905
+ type: 'text' as const,
4906
+ text: 'No update notification.',
4907
+ }],
4908
+ };
4909
+ }
4910
+
4911
+ const notification = JSON.parse(fs.readFileSync(notificationPath, 'utf-8'));
4912
+
4913
+ // Delete the notification file after reading (so it only shows once)
4914
+ fs.unlinkSync(notificationPath);
4915
+
4916
+ return {
4917
+ content: [{
4918
+ type: 'text' as const,
4919
+ text: `🍪 **CodeBakers Update**\n\n${notification.message}\n\n**Previous version:** ${notification.previousVersion}\n**New version:** ${notification.newVersion}\n**Modules:** ${notification.moduleCount}\n**Updated:** ${new Date(notification.updatedAt).toLocaleString()}`,
4920
+ }],
4921
+ };
4922
+ } catch {
4923
+ return {
4924
+ content: [{
4925
+ type: 'text' as const,
4926
+ text: 'No update notification.',
4927
+ }],
4928
+ };
4929
+ }
4930
+ }
4931
+
4842
4932
  private parseApiRouteRequest(request: string): {
4843
4933
  success: boolean;
4844
4934
  routeName?: string;