@codebakers/cli 3.0.1 → 3.1.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/index.js CHANGED
@@ -67,13 +67,14 @@ const program = new commander_1.Command();
67
67
  program
68
68
  .name('codebakers')
69
69
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
70
- .version('2.9.0');
70
+ .version('3.1.0');
71
71
  // Zero-friction trial entry (no signup required)
72
72
  program
73
73
  .command('go')
74
74
  .alias('start')
75
75
  .description('Start using CodeBakers instantly (no signup required)')
76
- .action(go_js_1.go);
76
+ .option('-v, --verbose', 'Show detailed debug output for troubleshooting')
77
+ .action((options) => (0, go_js_1.go)({ verbose: options.verbose }));
77
78
  program
78
79
  .command('extend')
79
80
  .description('Extend your free trial with GitHub')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.0.1",
3
+ "version": "3.1.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/index.ts CHANGED
@@ -72,14 +72,15 @@ const program = new Command();
72
72
  program
73
73
  .name('codebakers')
74
74
  .description('CodeBakers CLI - Production patterns for AI-assisted development')
75
- .version('2.9.0');
75
+ .version('3.1.0');
76
76
 
77
77
  // Zero-friction trial entry (no signup required)
78
78
  program
79
79
  .command('go')
80
80
  .alias('start')
81
81
  .description('Start using CodeBakers instantly (no signup required)')
82
- .action(go);
82
+ .option('-v, --verbose', 'Show detailed debug output for troubleshooting')
83
+ .action((options) => go({ verbose: options.verbose }));
83
84
 
84
85
  program
85
86
  .command('extend')
@@ -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
+ });