@codebakers/cli 3.0.1 → 3.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.
@@ -118,13 +118,20 @@ function checkProject() {
118
118
  try {
119
119
  const files = (0, fs_1.readdirSync)(claudeDir).filter(f => f.endsWith('.md'));
120
120
  const moduleCount = files.length;
121
- if (moduleCount >= 10) {
122
- results.push({ ok: true, message: `${moduleCount} modules present` });
121
+ if (moduleCount >= 40) {
122
+ results.push({ ok: true, message: `${moduleCount} modules present (full set)` });
123
+ }
124
+ else if (moduleCount >= 10) {
125
+ results.push({
126
+ ok: true,
127
+ message: `${moduleCount} modules present (partial set)`,
128
+ details: 'Run: codebakers upgrade to get all 47 modules'
129
+ });
123
130
  }
124
131
  else if (moduleCount > 0) {
125
132
  results.push({
126
133
  ok: false,
127
- message: `Only ${moduleCount} modules found (expected 10+)`,
134
+ message: `Only ${moduleCount} modules found (expected 47)`,
128
135
  details: 'Run: codebakers upgrade to get all modules'
129
136
  });
130
137
  }
@@ -1,4 +1,8 @@
1
+ interface GoOptions {
2
+ verbose?: boolean;
3
+ }
1
4
  /**
2
5
  * Zero-friction entry point - start using CodeBakers instantly
3
6
  */
4
- export declare function go(): Promise<void>;
7
+ export declare function go(options?: GoOptions): Promise<void>;
8
+ export {};
@@ -24,10 +24,18 @@ function prompt(question) {
24
24
  });
25
25
  });
26
26
  }
27
+ function log(message, options) {
28
+ if (options?.verbose) {
29
+ console.log(chalk_1.default.gray(` [verbose] ${message}`));
30
+ }
31
+ }
27
32
  /**
28
33
  * Zero-friction entry point - start using CodeBakers instantly
29
34
  */
30
- async function go() {
35
+ async function go(options = {}) {
36
+ log('Starting go command...', options);
37
+ log(`API URL: ${(0, config_js_1.getApiUrl)()}`, options);
38
+ log(`Working directory: ${process.cwd()}`, options);
31
39
  console.log(chalk_1.default.blue(`
32
40
  ╔═══════════════════════════════════════════════════════════╗
33
41
  ║ ║
@@ -36,14 +44,17 @@ async function go() {
36
44
  ╚═══════════════════════════════════════════════════════════╝
37
45
  `));
38
46
  // Check if user already has an API key (paid user)
47
+ log('Checking for existing API key...', options);
39
48
  const apiKey = (0, config_js_1.getApiKey)();
40
49
  if (apiKey) {
50
+ log(`Found API key: ${apiKey.substring(0, 8)}...`, options);
41
51
  console.log(chalk_1.default.green(' ✓ You\'re already logged in with an API key!\n'));
42
52
  // Still install patterns if not already installed
43
- await installPatternsWithApiKey(apiKey);
44
- await configureMCP();
53
+ await installPatternsWithApiKey(apiKey, options);
54
+ await configureMCP(options);
45
55
  return;
46
56
  }
57
+ log('No API key found, checking trial state...', options);
47
58
  // Check existing trial
48
59
  const existingTrial = (0, config_js_1.getTrialState)();
49
60
  if (existingTrial && !(0, config_js_1.isTrialExpired)()) {
@@ -54,8 +65,8 @@ async function go() {
54
65
  console.log(chalk_1.default.cyan(' codebakers extend\n'));
55
66
  }
56
67
  // Install patterns if not already installed
57
- await installPatterns(existingTrial.trialId);
58
- await configureMCP();
68
+ await installPatterns(existingTrial.trialId, options);
69
+ await configureMCP(options);
59
70
  return;
60
71
  }
61
72
  // Check if trial expired
@@ -132,9 +143,9 @@ async function go() {
132
143
  spinner.succeed(`Trial started (${data.daysRemaining} days free)`);
133
144
  console.log('');
134
145
  // Install patterns (CLAUDE.md and .claude/)
135
- await installPatterns(data.trialId);
146
+ await installPatterns(data.trialId, options);
136
147
  // Configure MCP
137
- await configureMCP();
148
+ await configureMCP(options);
138
149
  // Show success message
139
150
  console.log(chalk_1.default.green(`
140
151
  ╔═══════════════════════════════════════════════════════════╗
@@ -164,7 +175,8 @@ async function go() {
164
175
  }
165
176
  }
166
177
  }
167
- async function configureMCP() {
178
+ async function configureMCP(options = {}) {
179
+ log('Configuring MCP integration...', options);
168
180
  const spinner = (0, ora_1.default)('Configuring Claude Code integration...').start();
169
181
  const isWindows = process.platform === 'win32';
170
182
  const mcpCmd = isWindows
@@ -231,10 +243,12 @@ async function attemptAutoRestart() {
231
243
  /**
232
244
  * Install pattern files for API key users (paid users)
233
245
  */
234
- async function installPatternsWithApiKey(apiKey) {
246
+ async function installPatternsWithApiKey(apiKey, options = {}) {
247
+ log('Installing patterns with API key...', options);
235
248
  const spinner = (0, ora_1.default)('Installing CodeBakers patterns...').start();
236
249
  const cwd = process.cwd();
237
250
  const apiUrl = (0, config_js_1.getApiUrl)();
251
+ log(`Fetching from: ${apiUrl}/api/content`, options);
238
252
  try {
239
253
  const response = await fetch(`${apiUrl}/api/content`, {
240
254
  method: 'GET',
@@ -243,13 +257,17 @@ async function installPatternsWithApiKey(apiKey) {
243
257
  },
244
258
  });
245
259
  if (!response.ok) {
260
+ log(`Response not OK: ${response.status} ${response.statusText}`, options);
246
261
  spinner.warn('Could not download patterns');
247
262
  return;
248
263
  }
264
+ log('Response OK, parsing JSON...', options);
249
265
  const content = await response.json();
250
- await writePatternFiles(cwd, content, spinner);
266
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
267
+ await writePatternFiles(cwd, content, spinner, options);
251
268
  }
252
269
  catch (error) {
270
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
253
271
  spinner.warn('Could not install patterns');
254
272
  console.log(chalk_1.default.gray(' Check your internet connection.\n'));
255
273
  }
@@ -257,12 +275,14 @@ async function installPatternsWithApiKey(apiKey) {
257
275
  /**
258
276
  * Install pattern files (CLAUDE.md and .claude/) for trial users
259
277
  */
260
- async function installPatterns(trialId) {
278
+ async function installPatterns(trialId, options = {}) {
279
+ log(`Installing patterns with trial ID: ${trialId.substring(0, 8)}...`, options);
261
280
  const spinner = (0, ora_1.default)('Installing CodeBakers patterns...').start();
262
281
  const cwd = process.cwd();
263
282
  const apiUrl = (0, config_js_1.getApiUrl)();
264
283
  try {
265
284
  // Fetch patterns using trial ID
285
+ log(`Fetching from: ${apiUrl}/api/content`, options);
266
286
  const response = await fetch(`${apiUrl}/api/content`, {
267
287
  method: 'GET',
268
288
  headers: {
@@ -270,6 +290,7 @@ async function installPatterns(trialId) {
270
290
  },
271
291
  });
272
292
  if (!response.ok) {
293
+ log(`Primary endpoint failed: ${response.status}, trying trial endpoint...`, options);
273
294
  // Try without auth - some patterns may be available for trial
274
295
  const publicResponse = await fetch(`${apiUrl}/api/content/trial`, {
275
296
  method: 'GET',
@@ -278,22 +299,27 @@ async function installPatterns(trialId) {
278
299
  },
279
300
  });
280
301
  if (!publicResponse.ok) {
302
+ log(`Trial endpoint also failed: ${publicResponse.status}`, options);
281
303
  spinner.warn('Could not download patterns (will use MCP tools)');
282
304
  return;
283
305
  }
284
306
  const content = await publicResponse.json();
285
- await writePatternFiles(cwd, content, spinner);
307
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
308
+ await writePatternFiles(cwd, content, spinner, options);
286
309
  return;
287
310
  }
288
311
  const content = await response.json();
289
- await writePatternFiles(cwd, content, spinner);
312
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
313
+ await writePatternFiles(cwd, content, spinner, options);
290
314
  }
291
315
  catch (error) {
316
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
292
317
  spinner.warn('Could not install patterns (will use MCP tools)');
293
318
  console.log(chalk_1.default.gray(' Patterns will be available via MCP tools.\n'));
294
319
  }
295
320
  }
296
- async function writePatternFiles(cwd, content, spinner) {
321
+ async function writePatternFiles(cwd, content, spinner, options = {}) {
322
+ log(`Writing pattern files to ${cwd}...`, options);
297
323
  // Check if patterns already exist
298
324
  const claudeMdPath = (0, path_1.join)(cwd, 'CLAUDE.md');
299
325
  if ((0, fs_1.existsSync)(claudeMdPath)) {
package/dist/config.d.ts CHANGED
@@ -43,6 +43,8 @@ interface ConfigSchema {
43
43
  serviceKeys: ServiceKeys;
44
44
  lastKeySync: string | null;
45
45
  trial: TrialState | null;
46
+ lastUpdateCheck: string | null;
47
+ latestKnownVersion: string | null;
46
48
  }
47
49
  export declare function getApiKey(): string | null;
48
50
  export declare function setApiKey(key: string): void;
@@ -105,4 +107,19 @@ export declare function hasValidAccess(): boolean;
105
107
  * Get authentication mode: 'apiKey', 'trial', or 'none'
106
108
  */
107
109
  export declare function getAuthMode(): 'apiKey' | 'trial' | 'none';
110
+ /**
111
+ * Get cached update info if still valid (within 24 hours)
112
+ */
113
+ export declare function getCachedUpdateInfo(): {
114
+ latestVersion: string;
115
+ checkedAt: string;
116
+ } | null;
117
+ /**
118
+ * Cache the latest version from npm registry
119
+ */
120
+ export declare function setCachedUpdateInfo(latestVersion: string): void;
121
+ /**
122
+ * Get the current CLI version from package.json
123
+ */
124
+ export declare function getCliVersion(): string;
108
125
  export {};
package/dist/config.js CHANGED
@@ -32,6 +32,9 @@ exports.isTrialExpired = isTrialExpired;
32
32
  exports.getTrialDaysRemaining = getTrialDaysRemaining;
33
33
  exports.hasValidAccess = hasValidAccess;
34
34
  exports.getAuthMode = getAuthMode;
35
+ exports.getCachedUpdateInfo = getCachedUpdateInfo;
36
+ exports.setCachedUpdateInfo = setCachedUpdateInfo;
37
+ exports.getCliVersion = getCliVersion;
35
38
  const conf_1 = __importDefault(require("conf"));
36
39
  const fs_1 = require("fs");
37
40
  const path_1 = require("path");
@@ -126,6 +129,8 @@ const config = new conf_1.default({
126
129
  serviceKeys: defaultServiceKeys,
127
130
  lastKeySync: null,
128
131
  trial: null,
132
+ lastUpdateCheck: null,
133
+ latestKnownVersion: null,
129
134
  },
130
135
  // Migration to add new keys when upgrading from old version
131
136
  migrations: {
@@ -411,3 +416,33 @@ function getAuthMode() {
411
416
  return 'trial';
412
417
  return 'none';
413
418
  }
419
+ // ============================================
420
+ // Update Notification Cache
421
+ // ============================================
422
+ const UPDATE_CHECK_INTERVAL_HOURS = 24;
423
+ /**
424
+ * Get cached update info if still valid (within 24 hours)
425
+ */
426
+ function getCachedUpdateInfo() {
427
+ const lastCheck = config.get('lastUpdateCheck');
428
+ const latestVersion = config.get('latestKnownVersion');
429
+ if (!lastCheck || !latestVersion)
430
+ return null;
431
+ const hoursSinceCheck = (Date.now() - new Date(lastCheck).getTime()) / (1000 * 60 * 60);
432
+ if (hoursSinceCheck > UPDATE_CHECK_INTERVAL_HOURS)
433
+ return null;
434
+ return { latestVersion, checkedAt: lastCheck };
435
+ }
436
+ /**
437
+ * Cache the latest version from npm registry
438
+ */
439
+ function setCachedUpdateInfo(latestVersion) {
440
+ config.set('lastUpdateCheck', new Date().toISOString());
441
+ config.set('latestKnownVersion', latestVersion);
442
+ }
443
+ /**
444
+ * Get the current CLI version from package.json
445
+ */
446
+ function getCliVersion() {
447
+ return '3.2.0'; // Keep in sync with package.json
448
+ }
package/dist/index.js CHANGED
@@ -26,6 +26,53 @@ const push_patterns_js_1 = require("./commands/push-patterns.js");
26
26
  const go_js_1 = require("./commands/go.js");
27
27
  const extend_js_1 = require("./commands/extend.js");
28
28
  const billing_js_1 = require("./commands/billing.js");
29
+ const config_js_2 = require("./config.js");
30
+ // ============================================
31
+ // Automatic Update Notification
32
+ // ============================================
33
+ const CURRENT_VERSION = '3.2.0';
34
+ async function checkForUpdatesInBackground() {
35
+ // Check if we have a valid cached result first (fast path)
36
+ const cached = (0, config_js_2.getCachedUpdateInfo)();
37
+ if (cached) {
38
+ if (cached.latestVersion !== CURRENT_VERSION) {
39
+ showUpdateBanner(CURRENT_VERSION, cached.latestVersion);
40
+ }
41
+ return;
42
+ }
43
+ // Fetch from npm registry (with timeout to not block CLI)
44
+ try {
45
+ const controller = new AbortController();
46
+ const timeout = setTimeout(() => controller.abort(), 3000);
47
+ const response = await fetch('https://registry.npmjs.org/@codebakers/cli/latest', {
48
+ headers: { 'Accept': 'application/json' },
49
+ signal: controller.signal,
50
+ });
51
+ clearTimeout(timeout);
52
+ if (response.ok) {
53
+ const data = await response.json();
54
+ const latestVersion = data.version;
55
+ (0, config_js_2.setCachedUpdateInfo)(latestVersion);
56
+ if (latestVersion !== CURRENT_VERSION) {
57
+ showUpdateBanner(CURRENT_VERSION, latestVersion);
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // Silently fail - don't block CLI for update check
63
+ }
64
+ }
65
+ function showUpdateBanner(currentVersion, latestVersion) {
66
+ console.log(chalk_1.default.yellow(`
67
+ ╭─────────────────────────────────────────────────────────╮
68
+ │ │
69
+ │ ${chalk_1.default.bold('Update available!')} ${chalk_1.default.gray(currentVersion)} → ${chalk_1.default.green(latestVersion)} │
70
+ │ │
71
+ │ Run ${chalk_1.default.cyan('npm i -g @codebakers/cli@latest')} to update │
72
+ │ │
73
+ ╰─────────────────────────────────────────────────────────╯
74
+ `));
75
+ }
29
76
  // Show welcome message when no command is provided
30
77
  function showWelcome() {
31
78
  console.log(chalk_1.default.blue(`
@@ -67,13 +114,14 @@ const program = new commander_1.Command();
67
114
  program
68
115
  .name('codebakers')
69
116
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
70
- .version('2.9.0');
117
+ .version('3.2.0');
71
118
  // Zero-friction trial entry (no signup required)
72
119
  program
73
120
  .command('go')
74
121
  .alias('start')
75
122
  .description('Start using CodeBakers instantly (no signup required)')
76
- .action(go_js_1.go);
123
+ .option('-v, --verbose', 'Show detailed debug output for troubleshooting')
124
+ .action((options) => (0, go_js_1.go)({ verbose: options.verbose }));
77
125
  program
78
126
  .command('extend')
79
127
  .description('Extend your free trial with GitHub')
@@ -198,9 +246,16 @@ program
198
246
  .command('mcp-uninstall')
199
247
  .description('Remove MCP configuration from Claude Code')
200
248
  .action(mcp_config_js_1.mcpUninstall);
249
+ // Add update check hook (runs before every command)
250
+ program.hook('preAction', async () => {
251
+ await checkForUpdatesInBackground();
252
+ });
201
253
  // Show welcome if no command provided
202
254
  if (process.argv.length <= 2) {
203
- showWelcome();
255
+ // Still check for updates when showing welcome
256
+ checkForUpdatesInBackground().then(() => {
257
+ showWelcome();
258
+ });
204
259
  }
205
260
  else {
206
261
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -12,7 +12,10 @@
12
12
  "build": "tsc",
13
13
  "dev": "tsx src/index.ts",
14
14
  "start": "node dist/index.js",
15
- "mcp": "tsx src/mcp/server.ts"
15
+ "mcp": "tsx src/mcp/server.ts",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage"
16
19
  },
17
20
  "keywords": [
18
21
  "codebakers",
@@ -33,7 +36,9 @@
33
36
  },
34
37
  "devDependencies": {
35
38
  "@types/node": "^20.10.0",
39
+ "@vitest/coverage-v8": "^2.1.9",
36
40
  "tsx": "^4.7.0",
37
- "typescript": "^5.3.0"
41
+ "typescript": "^5.3.0",
42
+ "vitest": "^2.1.9"
38
43
  }
39
44
  }
@@ -134,12 +134,18 @@ function checkProject(): CheckResult[] {
134
134
  const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
135
135
  const moduleCount = files.length;
136
136
 
137
- if (moduleCount >= 10) {
138
- results.push({ ok: true, message: `${moduleCount} modules present` });
137
+ if (moduleCount >= 40) {
138
+ results.push({ ok: true, message: `${moduleCount} modules present (full set)` });
139
+ } else if (moduleCount >= 10) {
140
+ results.push({
141
+ ok: true,
142
+ message: `${moduleCount} modules present (partial set)`,
143
+ details: 'Run: codebakers upgrade to get all 47 modules'
144
+ });
139
145
  } else if (moduleCount > 0) {
140
146
  results.push({
141
147
  ok: false,
142
- message: `Only ${moduleCount} modules found (expected 10+)`,
148
+ message: `Only ${moduleCount} modules found (expected 47)`,
143
149
  details: 'Run: codebakers upgrade to get all modules'
144
150
  });
145
151
  } else {
@@ -35,10 +35,24 @@ interface ContentResponse {
35
35
  modules: Record<string, string>;
36
36
  }
37
37
 
38
+ interface GoOptions {
39
+ verbose?: boolean;
40
+ }
41
+
42
+ function log(message: string, options?: GoOptions): void {
43
+ if (options?.verbose) {
44
+ console.log(chalk.gray(` [verbose] ${message}`));
45
+ }
46
+ }
47
+
38
48
  /**
39
49
  * Zero-friction entry point - start using CodeBakers instantly
40
50
  */
41
- export async function go(): Promise<void> {
51
+ export async function go(options: GoOptions = {}): Promise<void> {
52
+ log('Starting go command...', options);
53
+ log(`API URL: ${getApiUrl()}`, options);
54
+ log(`Working directory: ${process.cwd()}`, options);
55
+
42
56
  console.log(chalk.blue(`
43
57
  ╔═══════════════════════════════════════════════════════════╗
44
58
  ║ ║
@@ -48,15 +62,18 @@ export async function go(): Promise<void> {
48
62
  `));
49
63
 
50
64
  // Check if user already has an API key (paid user)
65
+ log('Checking for existing API key...', options);
51
66
  const apiKey = getApiKey();
52
67
  if (apiKey) {
68
+ log(`Found API key: ${apiKey.substring(0, 8)}...`, options);
53
69
  console.log(chalk.green(' ✓ You\'re already logged in with an API key!\n'));
54
70
 
55
71
  // Still install patterns if not already installed
56
- await installPatternsWithApiKey(apiKey);
57
- await configureMCP();
72
+ await installPatternsWithApiKey(apiKey, options);
73
+ await configureMCP(options);
58
74
  return;
59
75
  }
76
+ log('No API key found, checking trial state...', options);
60
77
 
61
78
  // Check existing trial
62
79
  const existingTrial = getTrialState();
@@ -71,9 +88,9 @@ export async function go(): Promise<void> {
71
88
  }
72
89
 
73
90
  // Install patterns if not already installed
74
- await installPatterns(existingTrial.trialId);
91
+ await installPatterns(existingTrial.trialId, options);
75
92
 
76
- await configureMCP();
93
+ await configureMCP(options);
77
94
  return;
78
95
  }
79
96
 
@@ -162,10 +179,10 @@ export async function go(): Promise<void> {
162
179
  console.log('');
163
180
 
164
181
  // Install patterns (CLAUDE.md and .claude/)
165
- await installPatterns(data.trialId);
182
+ await installPatterns(data.trialId, options);
166
183
 
167
184
  // Configure MCP
168
- await configureMCP();
185
+ await configureMCP(options);
169
186
 
170
187
  // Show success message
171
188
  console.log(chalk.green(`
@@ -197,7 +214,8 @@ export async function go(): Promise<void> {
197
214
  }
198
215
  }
199
216
 
200
- async function configureMCP(): Promise<void> {
217
+ async function configureMCP(options: GoOptions = {}): Promise<void> {
218
+ log('Configuring MCP integration...', options);
201
219
  const spinner = ora('Configuring Claude Code integration...').start();
202
220
  const isWindows = process.platform === 'win32';
203
221
 
@@ -274,11 +292,14 @@ async function attemptAutoRestart(): Promise<void> {
274
292
  /**
275
293
  * Install pattern files for API key users (paid users)
276
294
  */
277
- async function installPatternsWithApiKey(apiKey: string): Promise<void> {
295
+ async function installPatternsWithApiKey(apiKey: string, options: GoOptions = {}): Promise<void> {
296
+ log('Installing patterns with API key...', options);
278
297
  const spinner = ora('Installing CodeBakers patterns...').start();
279
298
  const cwd = process.cwd();
280
299
  const apiUrl = getApiUrl();
281
300
 
301
+ log(`Fetching from: ${apiUrl}/api/content`, options);
302
+
282
303
  try {
283
304
  const response = await fetch(`${apiUrl}/api/content`, {
284
305
  method: 'GET',
@@ -288,14 +309,18 @@ async function installPatternsWithApiKey(apiKey: string): Promise<void> {
288
309
  });
289
310
 
290
311
  if (!response.ok) {
312
+ log(`Response not OK: ${response.status} ${response.statusText}`, options);
291
313
  spinner.warn('Could not download patterns');
292
314
  return;
293
315
  }
294
316
 
317
+ log('Response OK, parsing JSON...', options);
295
318
  const content: ContentResponse = await response.json();
296
- await writePatternFiles(cwd, content, spinner);
319
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
320
+ await writePatternFiles(cwd, content, spinner, options);
297
321
 
298
322
  } catch (error) {
323
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
299
324
  spinner.warn('Could not install patterns');
300
325
  console.log(chalk.gray(' Check your internet connection.\n'));
301
326
  }
@@ -304,13 +329,15 @@ async function installPatternsWithApiKey(apiKey: string): Promise<void> {
304
329
  /**
305
330
  * Install pattern files (CLAUDE.md and .claude/) for trial users
306
331
  */
307
- async function installPatterns(trialId: string): Promise<void> {
332
+ async function installPatterns(trialId: string, options: GoOptions = {}): Promise<void> {
333
+ log(`Installing patterns with trial ID: ${trialId.substring(0, 8)}...`, options);
308
334
  const spinner = ora('Installing CodeBakers patterns...').start();
309
335
  const cwd = process.cwd();
310
336
  const apiUrl = getApiUrl();
311
337
 
312
338
  try {
313
339
  // Fetch patterns using trial ID
340
+ log(`Fetching from: ${apiUrl}/api/content`, options);
314
341
  const response = await fetch(`${apiUrl}/api/content`, {
315
342
  method: 'GET',
316
343
  headers: {
@@ -319,6 +346,7 @@ async function installPatterns(trialId: string): Promise<void> {
319
346
  });
320
347
 
321
348
  if (!response.ok) {
349
+ log(`Primary endpoint failed: ${response.status}, trying trial endpoint...`, options);
322
350
  // Try without auth - some patterns may be available for trial
323
351
  const publicResponse = await fetch(`${apiUrl}/api/content/trial`, {
324
352
  method: 'GET',
@@ -328,25 +356,30 @@ async function installPatterns(trialId: string): Promise<void> {
328
356
  });
329
357
 
330
358
  if (!publicResponse.ok) {
359
+ log(`Trial endpoint also failed: ${publicResponse.status}`, options);
331
360
  spinner.warn('Could not download patterns (will use MCP tools)');
332
361
  return;
333
362
  }
334
363
 
335
364
  const content: ContentResponse = await publicResponse.json();
336
- await writePatternFiles(cwd, content, spinner);
365
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
366
+ await writePatternFiles(cwd, content, spinner, options);
337
367
  return;
338
368
  }
339
369
 
340
370
  const content: ContentResponse = await response.json();
341
- await writePatternFiles(cwd, content, spinner);
371
+ log(`Received version: ${content.version}, modules: ${Object.keys(content.modules || {}).length}`, options);
372
+ await writePatternFiles(cwd, content, spinner, options);
342
373
 
343
374
  } catch (error) {
375
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, options);
344
376
  spinner.warn('Could not install patterns (will use MCP tools)');
345
377
  console.log(chalk.gray(' Patterns will be available via MCP tools.\n'));
346
378
  }
347
379
  }
348
380
 
349
- async function writePatternFiles(cwd: string, content: ContentResponse, spinner: ReturnType<typeof ora>): Promise<void> {
381
+ async function writePatternFiles(cwd: string, content: ContentResponse, spinner: ReturnType<typeof ora>, options: GoOptions = {}): Promise<void> {
382
+ log(`Writing pattern files to ${cwd}...`, options);
350
383
  // Check if patterns already exist
351
384
  const claudeMdPath = join(cwd, 'CLAUDE.md');
352
385
  if (existsSync(claudeMdPath)) {
package/src/config.ts CHANGED
@@ -116,6 +116,9 @@ interface ConfigSchema {
116
116
  lastKeySync: string | null; // ISO date of last sync with server
117
117
  // Trial state (for zero-friction onboarding)
118
118
  trial: TrialState | null;
119
+ // Update notification cache
120
+ lastUpdateCheck: string | null; // ISO date of last npm registry check
121
+ latestKnownVersion: string | null; // Cached latest version from npm
119
122
  }
120
123
 
121
124
  // Create default service keys object with all keys set to null
@@ -133,6 +136,8 @@ const config = new Conf<ConfigSchema>({
133
136
  serviceKeys: defaultServiceKeys,
134
137
  lastKeySync: null,
135
138
  trial: null,
139
+ lastUpdateCheck: null,
140
+ latestKnownVersion: null,
136
141
  },
137
142
  // Migration to add new keys when upgrading from old version
138
143
  migrations: {
@@ -489,3 +494,39 @@ export function getAuthMode(): 'apiKey' | 'trial' | 'none' {
489
494
 
490
495
  return 'none';
491
496
  }
497
+
498
+ // ============================================
499
+ // Update Notification Cache
500
+ // ============================================
501
+
502
+ const UPDATE_CHECK_INTERVAL_HOURS = 24;
503
+
504
+ /**
505
+ * Get cached update info if still valid (within 24 hours)
506
+ */
507
+ export function getCachedUpdateInfo(): { latestVersion: string; checkedAt: string } | null {
508
+ const lastCheck = config.get('lastUpdateCheck');
509
+ const latestVersion = config.get('latestKnownVersion');
510
+
511
+ if (!lastCheck || !latestVersion) return null;
512
+
513
+ const hoursSinceCheck = (Date.now() - new Date(lastCheck).getTime()) / (1000 * 60 * 60);
514
+ if (hoursSinceCheck > UPDATE_CHECK_INTERVAL_HOURS) return null;
515
+
516
+ return { latestVersion, checkedAt: lastCheck };
517
+ }
518
+
519
+ /**
520
+ * Cache the latest version from npm registry
521
+ */
522
+ export function setCachedUpdateInfo(latestVersion: string): void {
523
+ config.set('lastUpdateCheck', new Date().toISOString());
524
+ config.set('latestKnownVersion', latestVersion);
525
+ }
526
+
527
+ /**
528
+ * Get the current CLI version from package.json
529
+ */
530
+ export function getCliVersion(): string {
531
+ return '3.2.0'; // Keep in sync with package.json
532
+ }
package/src/index.ts CHANGED
@@ -22,6 +22,61 @@ 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';
26
+
27
+ // ============================================
28
+ // Automatic Update Notification
29
+ // ============================================
30
+
31
+ const CURRENT_VERSION = '3.2.0';
32
+
33
+ async function checkForUpdatesInBackground(): Promise<void> {
34
+ // Check if we have a valid cached result first (fast path)
35
+ const cached = getCachedUpdateInfo();
36
+ if (cached) {
37
+ if (cached.latestVersion !== CURRENT_VERSION) {
38
+ showUpdateBanner(CURRENT_VERSION, cached.latestVersion);
39
+ }
40
+ return;
41
+ }
42
+
43
+ // Fetch from npm registry (with timeout to not block CLI)
44
+ 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
+ });
52
+
53
+ clearTimeout(timeout);
54
+
55
+ if (response.ok) {
56
+ const data = await response.json();
57
+ const latestVersion = data.version;
58
+ setCachedUpdateInfo(latestVersion);
59
+
60
+ if (latestVersion !== CURRENT_VERSION) {
61
+ showUpdateBanner(CURRENT_VERSION, latestVersion);
62
+ }
63
+ }
64
+ } catch {
65
+ // Silently fail - don't block CLI for update check
66
+ }
67
+ }
68
+
69
+ function showUpdateBanner(currentVersion: string, latestVersion: string): void {
70
+ console.log(chalk.yellow(`
71
+ ╭─────────────────────────────────────────────────────────╮
72
+ │ │
73
+ │ ${chalk.bold('Update available!')} ${chalk.gray(currentVersion)} → ${chalk.green(latestVersion)} │
74
+ │ │
75
+ │ Run ${chalk.cyan('npm i -g @codebakers/cli@latest')} to update │
76
+ │ │
77
+ ╰─────────────────────────────────────────────────────────╯
78
+ `));
79
+ }
25
80
 
26
81
  // Show welcome message when no command is provided
27
82
  function showWelcome(): void {
@@ -72,14 +127,15 @@ const program = new Command();
72
127
  program
73
128
  .name('codebakers')
74
129
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
75
- .version('2.9.0');
130
+ .version('3.2.0');
76
131
 
77
132
  // Zero-friction trial entry (no signup required)
78
133
  program
79
134
  .command('go')
80
135
  .alias('start')
81
136
  .description('Start using CodeBakers instantly (no signup required)')
82
- .action(go);
137
+ .option('-v, --verbose', 'Show detailed debug output for troubleshooting')
138
+ .action((options) => go({ verbose: options.verbose }));
83
139
 
84
140
  program
85
141
  .command('extend')
@@ -224,9 +280,17 @@ program
224
280
  .description('Remove MCP configuration from Claude Code')
225
281
  .action(mcpUninstall);
226
282
 
283
+ // Add update check hook (runs before every command)
284
+ program.hook('preAction', async () => {
285
+ await checkForUpdatesInBackground();
286
+ });
287
+
227
288
  // Show welcome if no command provided
228
289
  if (process.argv.length <= 2) {
229
- showWelcome();
290
+ // Still check for updates when showing welcome
291
+ checkForUpdatesInBackground().then(() => {
292
+ showWelcome();
293
+ });
230
294
  } else {
231
295
  program.parse();
232
296
  }
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Mock fetch globally
4
+ const mockFetch = vi.fn();
5
+ global.fetch = mockFetch;
6
+
7
+ describe('API communication', () => {
8
+ beforeEach(() => {
9
+ mockFetch.mockClear();
10
+ });
11
+
12
+ describe('content API', () => {
13
+ it('should fetch content with API key', async () => {
14
+ mockFetch.mockResolvedValueOnce({
15
+ ok: true,
16
+ json: () => Promise.resolve({
17
+ version: '5.1',
18
+ router: '# Router content',
19
+ modules: {
20
+ '00-core.md': '# Core',
21
+ '02-auth.md': '# Auth',
22
+ },
23
+ }),
24
+ });
25
+
26
+ const response = await fetch('https://codebakers.ai/api/content', {
27
+ headers: { Authorization: 'Bearer test-key' },
28
+ });
29
+
30
+ expect(response.ok).toBe(true);
31
+ const data = await response.json();
32
+ expect(data.version).toBe('5.1');
33
+ expect(Object.keys(data.modules).length).toBe(2);
34
+ });
35
+
36
+ it('should fetch content with trial ID', async () => {
37
+ mockFetch.mockResolvedValueOnce({
38
+ ok: true,
39
+ json: () => Promise.resolve({
40
+ version: '5.1',
41
+ router: '# Router content',
42
+ modules: { '00-core.md': '# Core' },
43
+ }),
44
+ });
45
+
46
+ const response = await fetch('https://codebakers.ai/api/content', {
47
+ headers: { 'X-Trial-ID': 'trial-123' },
48
+ });
49
+
50
+ expect(response.ok).toBe(true);
51
+ });
52
+
53
+ it('should handle unauthorized response', async () => {
54
+ mockFetch.mockResolvedValueOnce({
55
+ ok: false,
56
+ status: 401,
57
+ json: () => Promise.resolve({ error: 'Unauthorized' }),
58
+ });
59
+
60
+ const response = await fetch('https://codebakers.ai/api/content', {
61
+ headers: { Authorization: 'Bearer invalid-key' },
62
+ });
63
+
64
+ expect(response.ok).toBe(false);
65
+ expect(response.status).toBe(401);
66
+ });
67
+
68
+ it('should handle network errors', async () => {
69
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
70
+
71
+ await expect(
72
+ fetch('https://codebakers.ai/api/content')
73
+ ).rejects.toThrow('Network error');
74
+ });
75
+ });
76
+
77
+ describe('trial API', () => {
78
+ it('should start a new trial', async () => {
79
+ mockFetch.mockResolvedValueOnce({
80
+ ok: true,
81
+ json: () => Promise.resolve({
82
+ trialId: 'trial-123',
83
+ stage: 'anonymous',
84
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
85
+ daysRemaining: 7,
86
+ }),
87
+ });
88
+
89
+ const response = await fetch('https://codebakers.ai/api/trial/start', {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({
93
+ deviceHash: 'test-hash',
94
+ machineId: 'test-machine',
95
+ platform: 'test',
96
+ hostname: 'test-host',
97
+ }),
98
+ });
99
+
100
+ expect(response.ok).toBe(true);
101
+ const data = await response.json();
102
+ expect(data.trialId).toBe('trial-123');
103
+ expect(data.daysRemaining).toBe(7);
104
+ });
105
+
106
+ it('should handle trial not available', async () => {
107
+ mockFetch.mockResolvedValueOnce({
108
+ ok: false,
109
+ status: 400,
110
+ json: () => Promise.resolve({ error: 'trial_not_available' }),
111
+ });
112
+
113
+ const response = await fetch('https://codebakers.ai/api/trial/start', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ deviceHash: 'used-hash' }),
117
+ });
118
+
119
+ expect(response.ok).toBe(false);
120
+ const data = await response.json();
121
+ expect(data.error).toBe('trial_not_available');
122
+ });
123
+
124
+ it('should handle expired trial', async () => {
125
+ mockFetch.mockResolvedValueOnce({
126
+ ok: true,
127
+ json: () => Promise.resolve({
128
+ stage: 'expired',
129
+ canExtend: true,
130
+ }),
131
+ });
132
+
133
+ const response = await fetch('https://codebakers.ai/api/trial/start', {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({ deviceHash: 'expired-hash' }),
137
+ });
138
+
139
+ const data = await response.json();
140
+ expect(data.stage).toBe('expired');
141
+ expect(data.canExtend).toBe(true);
142
+ });
143
+ });
144
+
145
+ describe('API key validation', () => {
146
+ it('should validate a correct API key', async () => {
147
+ mockFetch.mockResolvedValueOnce({
148
+ ok: true,
149
+ json: () => Promise.resolve({ valid: true }),
150
+ });
151
+
152
+ const response = await fetch('https://codebakers.ai/api/verify', {
153
+ headers: { Authorization: 'Bearer valid-key' },
154
+ });
155
+
156
+ expect(response.ok).toBe(true);
157
+ const data = await response.json();
158
+ expect(data.valid).toBe(true);
159
+ });
160
+
161
+ it('should reject an invalid API key', async () => {
162
+ mockFetch.mockResolvedValueOnce({
163
+ ok: false,
164
+ status: 401,
165
+ json: () => Promise.resolve({ valid: false, error: 'Invalid API key' }),
166
+ });
167
+
168
+ const response = await fetch('https://codebakers.ai/api/verify', {
169
+ headers: { Authorization: 'Bearer invalid-key' },
170
+ });
171
+
172
+ expect(response.ok).toBe(false);
173
+ });
174
+ });
175
+ });
176
+
177
+ describe('error handling', () => {
178
+ beforeEach(() => {
179
+ mockFetch.mockClear();
180
+ });
181
+
182
+ it('should provide helpful error messages for timeout', async () => {
183
+ mockFetch.mockRejectedValueOnce(new Error('timeout'));
184
+
185
+ try {
186
+ await fetch('https://codebakers.ai/api/content');
187
+ } catch (error) {
188
+ expect(error).toBeInstanceOf(Error);
189
+ if (error instanceof Error) {
190
+ expect(error.message).toContain('timeout');
191
+ }
192
+ }
193
+ });
194
+
195
+ it('should handle rate limiting', async () => {
196
+ mockFetch.mockResolvedValueOnce({
197
+ ok: false,
198
+ status: 429,
199
+ json: () => Promise.resolve({ error: 'Too many requests' }),
200
+ });
201
+
202
+ const response = await fetch('https://codebakers.ai/api/content');
203
+ expect(response.status).toBe(429);
204
+ });
205
+
206
+ it('should handle server errors', async () => {
207
+ mockFetch.mockResolvedValueOnce({
208
+ ok: false,
209
+ status: 500,
210
+ json: () => Promise.resolve({ error: 'Internal server error' }),
211
+ });
212
+
213
+ const response = await fetch('https://codebakers.ai/api/content');
214
+ expect(response.status).toBe(500);
215
+ });
216
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+
6
+ describe('doctor command checks', () => {
7
+ let testDir: string;
8
+ let originalCwd: string;
9
+
10
+ beforeEach(() => {
11
+ originalCwd = process.cwd();
12
+ testDir = join(tmpdir(), `codebakers-doctor-test-${Date.now()}`);
13
+ mkdirSync(testDir, { recursive: true });
14
+ process.chdir(testDir);
15
+ });
16
+
17
+ afterEach(() => {
18
+ process.chdir(originalCwd);
19
+ try {
20
+ rmSync(testDir, { recursive: true, force: true });
21
+ } catch {
22
+ // Ignore cleanup errors
23
+ }
24
+ });
25
+
26
+ describe('CLAUDE.md checks', () => {
27
+ it('should detect missing CLAUDE.md', () => {
28
+ const claudeMdPath = join(testDir, 'CLAUDE.md');
29
+ expect(existsSync(claudeMdPath)).toBe(false);
30
+ });
31
+
32
+ it('should detect valid CodeBakers CLAUDE.md', () => {
33
+ const claudeMdPath = join(testDir, 'CLAUDE.md');
34
+ writeFileSync(claudeMdPath, '# CODEBAKERS SMART ROUTER\nVersion: 5.1');
35
+
36
+ const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
37
+ const isCodeBakers = content.includes('CODEBAKERS') || content.includes('CodeBakers');
38
+ expect(isCodeBakers).toBe(true);
39
+ });
40
+
41
+ it('should detect non-CodeBakers CLAUDE.md', () => {
42
+ const claudeMdPath = join(testDir, 'CLAUDE.md');
43
+ writeFileSync(claudeMdPath, '# My Custom Instructions');
44
+
45
+ const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
46
+ const isCodeBakers = content.includes('CODEBAKERS') || content.includes('CodeBakers');
47
+ expect(isCodeBakers).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('.claude folder checks', () => {
52
+ it('should detect missing .claude folder', () => {
53
+ const claudeDir = join(testDir, '.claude');
54
+ expect(existsSync(claudeDir)).toBe(false);
55
+ });
56
+
57
+ it('should count modules in .claude folder', () => {
58
+ const claudeDir = join(testDir, '.claude');
59
+ mkdirSync(claudeDir, { recursive: true });
60
+
61
+ // Create test modules
62
+ const modules = [
63
+ '00-core.md',
64
+ '01-database.md',
65
+ '02-auth.md',
66
+ '03-api.md',
67
+ '04-frontend.md',
68
+ ];
69
+
70
+ for (const mod of modules) {
71
+ writeFileSync(join(claudeDir, mod), `# ${mod}`);
72
+ }
73
+
74
+ const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
75
+ expect(files.length).toBe(5);
76
+ });
77
+
78
+ it('should detect insufficient modules (less than 10)', () => {
79
+ const claudeDir = join(testDir, '.claude');
80
+ mkdirSync(claudeDir, { recursive: true });
81
+
82
+ // Create only 3 modules
83
+ writeFileSync(join(claudeDir, '00-core.md'), '# Core');
84
+ writeFileSync(join(claudeDir, '01-database.md'), '# Database');
85
+ writeFileSync(join(claudeDir, '02-auth.md'), '# Auth');
86
+
87
+ const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
88
+ expect(files.length).toBeLessThan(10);
89
+ });
90
+
91
+ it('should detect full module set (47+ modules)', () => {
92
+ const claudeDir = join(testDir, '.claude');
93
+ mkdirSync(claudeDir, { recursive: true });
94
+
95
+ // Create 47 modules
96
+ for (let i = 0; i < 47; i++) {
97
+ const name = i.toString().padStart(2, '0');
98
+ writeFileSync(join(claudeDir, `${name}-module.md`), `# Module ${i}`);
99
+ }
100
+
101
+ const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
102
+ expect(files.length).toBeGreaterThanOrEqual(47);
103
+ });
104
+
105
+ it('should check for required 00-core.md', () => {
106
+ const claudeDir = join(testDir, '.claude');
107
+ mkdirSync(claudeDir, { recursive: true });
108
+
109
+ const corePath = join(claudeDir, '00-core.md');
110
+ expect(existsSync(corePath)).toBe(false);
111
+
112
+ writeFileSync(corePath, '# Core patterns');
113
+ expect(existsSync(corePath)).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe('project state checks', () => {
118
+ it('should handle missing PROJECT-STATE.md gracefully', () => {
119
+ const statePath = join(testDir, 'PROJECT-STATE.md');
120
+ // This is optional, should not fail
121
+ expect(existsSync(statePath)).toBe(false);
122
+ });
123
+
124
+ it('should detect existing PROJECT-STATE.md', () => {
125
+ const statePath = join(testDir, 'PROJECT-STATE.md');
126
+ writeFileSync(statePath, '# Project State\n');
127
+ expect(existsSync(statePath)).toBe(true);
128
+ });
129
+ });
130
+ });
131
+
132
+ describe('doctor summary', () => {
133
+ it('should calculate correct pass/fail counts', () => {
134
+ const checks = [
135
+ { ok: true, message: 'Check 1' },
136
+ { ok: true, message: 'Check 2' },
137
+ { ok: false, message: 'Check 3' },
138
+ { ok: true, message: 'Check 4' },
139
+ ];
140
+
141
+ const passed = checks.filter(c => c.ok).length;
142
+ const failed = checks.filter(c => !c.ok).length;
143
+ const total = checks.length;
144
+
145
+ expect(passed).toBe(3);
146
+ expect(failed).toBe(1);
147
+ expect(total).toBe(4);
148
+ });
149
+
150
+ it('should provide fix suggestions for common issues', () => {
151
+ const suggestions: string[] = [];
152
+
153
+ // Simulate missing CLAUDE.md
154
+ const hasClaudeMd = false;
155
+ if (!hasClaudeMd) {
156
+ suggestions.push('Run: codebakers install');
157
+ }
158
+
159
+ // Simulate missing hook
160
+ const hasHook = false;
161
+ if (!hasHook) {
162
+ suggestions.push('Run: codebakers install-hook');
163
+ }
164
+
165
+ expect(suggestions).toContain('Run: codebakers install');
166
+ expect(suggestions).toContain('Run: codebakers install-hook');
167
+ });
168
+ });
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, writeFileSync, mkdirSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+
6
+ // Mock modules before importing
7
+ vi.mock('../src/config.js', () => ({
8
+ getApiKey: vi.fn(() => null),
9
+ getTrialState: vi.fn(() => null),
10
+ setTrialState: vi.fn(),
11
+ getApiUrl: vi.fn(() => 'https://codebakers.ai'),
12
+ isTrialExpired: vi.fn(() => false),
13
+ getTrialDaysRemaining: vi.fn(() => 7),
14
+ }));
15
+
16
+ vi.mock('../src/lib/fingerprint.js', () => ({
17
+ getDeviceFingerprint: vi.fn(() => ({
18
+ deviceHash: 'test-hash',
19
+ machineId: 'test-machine',
20
+ platform: 'test',
21
+ hostname: 'test-host',
22
+ })),
23
+ }));
24
+
25
+ describe('go command', () => {
26
+ let testDir: string;
27
+
28
+ beforeEach(() => {
29
+ // Create a temp directory for each test
30
+ testDir = join(tmpdir(), `codebakers-test-${Date.now()}`);
31
+ mkdirSync(testDir, { recursive: true });
32
+ process.chdir(testDir);
33
+ });
34
+
35
+ afterEach(() => {
36
+ // Clean up
37
+ try {
38
+ rmSync(testDir, { recursive: true, force: true });
39
+ } catch {
40
+ // Ignore cleanup errors
41
+ }
42
+ });
43
+
44
+ it('should create CLAUDE.md when patterns are installed', async () => {
45
+ const claudeMdPath = join(testDir, 'CLAUDE.md');
46
+
47
+ // Simulate pattern installation
48
+ writeFileSync(claudeMdPath, '# CodeBakers Router');
49
+
50
+ expect(existsSync(claudeMdPath)).toBe(true);
51
+ });
52
+
53
+ it('should create .claude directory with modules', async () => {
54
+ const claudeDir = join(testDir, '.claude');
55
+ mkdirSync(claudeDir, { recursive: true });
56
+
57
+ // Simulate module installation
58
+ writeFileSync(join(claudeDir, '00-core.md'), '# Core patterns');
59
+ writeFileSync(join(claudeDir, '02-auth.md'), '# Auth patterns');
60
+
61
+ expect(existsSync(claudeDir)).toBe(true);
62
+ expect(existsSync(join(claudeDir, '00-core.md'))).toBe(true);
63
+ expect(existsSync(join(claudeDir, '02-auth.md'))).toBe(true);
64
+ });
65
+
66
+ it('should handle network errors gracefully', async () => {
67
+ // Mock fetch to fail
68
+ global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
69
+
70
+ // The go command should not throw, just warn
71
+ expect(() => {
72
+ // Simulating the error handling logic
73
+ try {
74
+ throw new Error('Network error');
75
+ } catch (error) {
76
+ if (error instanceof Error && error.message.includes('Network')) {
77
+ // Expected behavior - handle gracefully
78
+ }
79
+ }
80
+ }).not.toThrow();
81
+ });
82
+ });
83
+
84
+ describe('pattern file writing', () => {
85
+ let testDir: string;
86
+
87
+ beforeEach(() => {
88
+ testDir = join(tmpdir(), `codebakers-test-${Date.now()}`);
89
+ mkdirSync(testDir, { recursive: true });
90
+ });
91
+
92
+ afterEach(() => {
93
+ try {
94
+ rmSync(testDir, { recursive: true, force: true });
95
+ } catch {
96
+ // Ignore cleanup errors
97
+ }
98
+ });
99
+
100
+ it('should not overwrite existing CLAUDE.md', () => {
101
+ const claudeMdPath = join(testDir, 'CLAUDE.md');
102
+ const originalContent = '# My existing CLAUDE.md';
103
+
104
+ writeFileSync(claudeMdPath, originalContent);
105
+
106
+ // Simulate check before writing
107
+ if (existsSync(claudeMdPath)) {
108
+ // Should skip writing
109
+ const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
110
+ expect(content).toBe(originalContent);
111
+ }
112
+ });
113
+
114
+ it('should add .claude/ to .gitignore if exists', () => {
115
+ const gitignorePath = join(testDir, '.gitignore');
116
+ writeFileSync(gitignorePath, 'node_modules/\n.env\n');
117
+
118
+ // Simulate gitignore update
119
+ let gitignore = require('fs').readFileSync(gitignorePath, 'utf-8');
120
+ if (!gitignore.includes('.claude/')) {
121
+ gitignore += '\n# CodeBakers patterns\n.claude/\n';
122
+ writeFileSync(gitignorePath, gitignore);
123
+ }
124
+
125
+ const updatedContent = require('fs').readFileSync(gitignorePath, 'utf-8');
126
+ expect(updatedContent).toContain('.claude/');
127
+ });
128
+
129
+ it('should handle missing .gitignore gracefully', () => {
130
+ const gitignorePath = join(testDir, '.gitignore');
131
+
132
+ // Don't create .gitignore
133
+ expect(existsSync(gitignorePath)).toBe(false);
134
+
135
+ // Should not throw when trying to update non-existent .gitignore
136
+ expect(() => {
137
+ if (existsSync(gitignorePath)) {
138
+ // Would update gitignore
139
+ }
140
+ }).not.toThrow();
141
+ });
142
+ });
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/mcp/**', 'src/templates/**'],
13
+ },
14
+ testTimeout: 30000,
15
+ },
16
+ });