@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.
- package/dist/commands/doctor.js +10 -3
- package/dist/commands/go.d.ts +5 -1
- package/dist/commands/go.js +40 -14
- package/dist/config.d.ts +17 -0
- package/dist/config.js +35 -0
- package/dist/index.js +58 -3
- package/package.json +8 -3
- package/src/commands/doctor.ts +9 -3
- package/src/commands/go.ts +47 -14
- package/src/config.ts +41 -0
- package/src/index.ts +67 -3
- package/tests/api.test.ts +216 -0
- package/tests/doctor.test.ts +168 -0
- package/tests/go.test.ts +142 -0
- package/vitest.config.ts +16 -0
package/dist/commands/doctor.js
CHANGED
|
@@ -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 >=
|
|
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
|
|
134
|
+
message: `Only ${moduleCount} modules found (expected 47)`,
|
|
128
135
|
details: 'Run: codebakers upgrade to get all modules'
|
|
129
136
|
});
|
|
130
137
|
}
|
package/dist/commands/go.d.ts
CHANGED
package/dist/commands/go.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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 >=
|
|
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
|
|
148
|
+
message: `Only ${moduleCount} modules found (expected 47)`,
|
|
143
149
|
details: 'Run: codebakers upgrade to get all modules'
|
|
144
150
|
});
|
|
145
151
|
} else {
|
package/src/commands/go.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
+
});
|
package/tests/go.test.ts
ADDED
|
@@ -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
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|