@codebakers/cli 2.9.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/billing.d.ts +4 -0
- package/dist/commands/billing.js +91 -0
- package/dist/commands/extend.d.ts +4 -0
- package/dist/commands/extend.js +141 -0
- package/dist/commands/go.d.ts +4 -0
- package/dist/commands/go.js +328 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +83 -1
- package/dist/index.js +23 -5
- package/dist/lib/fingerprint.d.ts +23 -0
- package/dist/lib/fingerprint.js +136 -0
- package/dist/mcp/server.js +1319 -16
- package/package.json +1 -1
- package/src/commands/billing.ts +99 -0
- package/src/commands/extend.ts +157 -0
- package/src/commands/go.ts +386 -0
- package/src/config.ts +101 -1
- package/src/index.ts +26 -5
- package/src/lib/fingerprint.ts +122 -0
- package/src/mcp/server.ts +1524 -17
package/src/config.ts
CHANGED
|
@@ -94,12 +94,28 @@ type ServiceKeys = {
|
|
|
94
94
|
[K in ServiceName]: string | null;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
export type TrialStage = 'anonymous' | 'extended' | 'expired' | 'converted';
|
|
98
|
+
|
|
99
|
+
export interface TrialState {
|
|
100
|
+
trialId: string;
|
|
101
|
+
stage: TrialStage;
|
|
102
|
+
deviceHash: string;
|
|
103
|
+
expiresAt: string; // ISO date
|
|
104
|
+
startedAt: string; // ISO date
|
|
105
|
+
extendedAt?: string; // ISO date
|
|
106
|
+
githubUsername?: string;
|
|
107
|
+
projectId?: string;
|
|
108
|
+
projectName?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
interface ConfigSchema {
|
|
98
112
|
apiKey: string | null;
|
|
99
113
|
apiUrl: string;
|
|
100
114
|
experienceLevel: ExperienceLevel;
|
|
101
115
|
serviceKeys: ServiceKeys;
|
|
102
116
|
lastKeySync: string | null; // ISO date of last sync with server
|
|
117
|
+
// Trial state (for zero-friction onboarding)
|
|
118
|
+
trial: TrialState | null;
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
// Create default service keys object with all keys set to null
|
|
@@ -109,13 +125,14 @@ const defaultServiceKeys: ServiceKeys = Object.fromEntries(
|
|
|
109
125
|
|
|
110
126
|
const config = new Conf<ConfigSchema>({
|
|
111
127
|
projectName: 'codebakers',
|
|
112
|
-
projectVersion: '1.
|
|
128
|
+
projectVersion: '1.8.0',
|
|
113
129
|
defaults: {
|
|
114
130
|
apiKey: null,
|
|
115
131
|
apiUrl: 'https://codebakers.ai',
|
|
116
132
|
experienceLevel: 'intermediate',
|
|
117
133
|
serviceKeys: defaultServiceKeys,
|
|
118
134
|
lastKeySync: null,
|
|
135
|
+
trial: null,
|
|
119
136
|
},
|
|
120
137
|
// Migration to add new keys when upgrading from old version
|
|
121
138
|
migrations: {
|
|
@@ -132,6 +149,12 @@ const config = new Conf<ConfigSchema>({
|
|
|
132
149
|
|
|
133
150
|
store.set('serviceKeys', newKeys);
|
|
134
151
|
},
|
|
152
|
+
'1.8.0': (store) => {
|
|
153
|
+
// Add trial field if not present
|
|
154
|
+
if (!store.has('trial')) {
|
|
155
|
+
store.set('trial', null);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
135
158
|
},
|
|
136
159
|
});
|
|
137
160
|
|
|
@@ -389,3 +412,80 @@ export function getConfigPath(): string {
|
|
|
389
412
|
export function getConfigStore(): ConfigSchema {
|
|
390
413
|
return config.store;
|
|
391
414
|
}
|
|
415
|
+
|
|
416
|
+
// ============================================================
|
|
417
|
+
// Trial State Management (Zero-Friction Onboarding)
|
|
418
|
+
// ============================================================
|
|
419
|
+
|
|
420
|
+
export function getTrialState(): TrialState | null {
|
|
421
|
+
return config.get('trial');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function setTrialState(trial: TrialState): void {
|
|
425
|
+
config.set('trial', trial);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function clearTrialState(): void {
|
|
429
|
+
config.set('trial', null);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function updateTrialState(updates: Partial<TrialState>): void {
|
|
433
|
+
const current = config.get('trial');
|
|
434
|
+
if (current) {
|
|
435
|
+
config.set('trial', { ...current, ...updates });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if the current trial has expired
|
|
441
|
+
*/
|
|
442
|
+
export function isTrialExpired(): boolean {
|
|
443
|
+
const trial = config.get('trial');
|
|
444
|
+
if (!trial) return true;
|
|
445
|
+
|
|
446
|
+
const expiresAt = new Date(trial.expiresAt);
|
|
447
|
+
return new Date() > expiresAt;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get the number of days remaining in the trial
|
|
452
|
+
*/
|
|
453
|
+
export function getTrialDaysRemaining(): number {
|
|
454
|
+
const trial = config.get('trial');
|
|
455
|
+
if (!trial) return 0;
|
|
456
|
+
|
|
457
|
+
const expiresAt = new Date(trial.expiresAt);
|
|
458
|
+
const now = new Date();
|
|
459
|
+
const diffMs = expiresAt.getTime() - now.getTime();
|
|
460
|
+
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
461
|
+
|
|
462
|
+
return Math.max(0, diffDays);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Check if user has any valid access (API key OR active trial)
|
|
467
|
+
*/
|
|
468
|
+
export function hasValidAccess(): boolean {
|
|
469
|
+
// Paid users always have access
|
|
470
|
+
const apiKey = config.get('apiKey');
|
|
471
|
+
if (apiKey) return true;
|
|
472
|
+
|
|
473
|
+
// Check trial
|
|
474
|
+
const trial = config.get('trial');
|
|
475
|
+
if (!trial) return false;
|
|
476
|
+
|
|
477
|
+
return !isTrialExpired();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get authentication mode: 'apiKey', 'trial', or 'none'
|
|
482
|
+
*/
|
|
483
|
+
export function getAuthMode(): 'apiKey' | 'trial' | 'none' {
|
|
484
|
+
const apiKey = config.get('apiKey');
|
|
485
|
+
if (apiKey) return 'apiKey';
|
|
486
|
+
|
|
487
|
+
const trial = config.get('trial');
|
|
488
|
+
if (trial && !isTrialExpired()) return 'trial';
|
|
489
|
+
|
|
490
|
+
return 'none';
|
|
491
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,9 @@ import { config } from './commands/config.js';
|
|
|
19
19
|
import { audit } from './commands/audit.js';
|
|
20
20
|
import { heal, healWatch } from './commands/heal.js';
|
|
21
21
|
import { pushPatterns, pushPatternsInteractive } from './commands/push-patterns.js';
|
|
22
|
+
import { go } from './commands/go.js';
|
|
23
|
+
import { extend } from './commands/extend.js';
|
|
24
|
+
import { billing } from './commands/billing.js';
|
|
22
25
|
|
|
23
26
|
// Show welcome message when no command is provided
|
|
24
27
|
function showWelcome(): void {
|
|
@@ -33,7 +36,7 @@ function showWelcome(): void {
|
|
|
33
36
|
`));
|
|
34
37
|
|
|
35
38
|
console.log(chalk.white(' Getting Started:\n'));
|
|
36
|
-
console.log(chalk.cyan(' codebakers
|
|
39
|
+
console.log(chalk.cyan(' codebakers go') + chalk.gray(' Start free trial instantly (no signup!)'));
|
|
37
40
|
console.log(chalk.cyan(' codebakers scaffold') + chalk.gray(' Create a new project from scratch'));
|
|
38
41
|
console.log(chalk.cyan(' codebakers init') + chalk.gray(' Add patterns to existing project\n'));
|
|
39
42
|
|
|
@@ -57,9 +60,9 @@ function showWelcome(): void {
|
|
|
57
60
|
console.log(chalk.cyan(' codebakers doctor') + chalk.gray(' Check CodeBakers setup\n'));
|
|
58
61
|
|
|
59
62
|
console.log(chalk.white(' All Commands:\n'));
|
|
60
|
-
console.log(chalk.gray(' setup, scaffold, init, generate, upgrade, status
|
|
61
|
-
console.log(chalk.gray('
|
|
62
|
-
console.log(chalk.gray(' serve, mcp-config, mcp-uninstall\n'));
|
|
63
|
+
console.log(chalk.gray(' go, extend, billing, setup, scaffold, init, generate, upgrade, status'));
|
|
64
|
+
console.log(chalk.gray(' audit, heal, doctor, config, login, install, uninstall'));
|
|
65
|
+
console.log(chalk.gray(' install-hook, uninstall-hook, serve, mcp-config, mcp-uninstall\n'));
|
|
63
66
|
|
|
64
67
|
console.log(chalk.gray(' Run ') + chalk.cyan('codebakers <command> --help') + chalk.gray(' for more info\n'));
|
|
65
68
|
}
|
|
@@ -71,7 +74,25 @@ program
|
|
|
71
74
|
.description('CodeBakers CLI - Production patterns for AI-assisted development')
|
|
72
75
|
.version('2.9.0');
|
|
73
76
|
|
|
74
|
-
//
|
|
77
|
+
// Zero-friction trial entry (no signup required)
|
|
78
|
+
program
|
|
79
|
+
.command('go')
|
|
80
|
+
.alias('start')
|
|
81
|
+
.description('Start using CodeBakers instantly (no signup required)')
|
|
82
|
+
.action(go);
|
|
83
|
+
|
|
84
|
+
program
|
|
85
|
+
.command('extend')
|
|
86
|
+
.description('Extend your free trial with GitHub')
|
|
87
|
+
.action(extend);
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('billing')
|
|
91
|
+
.alias('subscribe')
|
|
92
|
+
.description('Manage subscription or upgrade to paid plan')
|
|
93
|
+
.action(billing);
|
|
94
|
+
|
|
95
|
+
// Primary command - one-time setup (for paid users)
|
|
75
96
|
program
|
|
76
97
|
.command('setup')
|
|
77
98
|
.description('One-time setup: login + configure Claude Code (recommended)')
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Device fingerprinting for zero-friction trial system
|
|
7
|
+
* Creates a stable, unique identifier for each device to prevent trial abuse
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface DeviceFingerprint {
|
|
11
|
+
machineId: string;
|
|
12
|
+
deviceHash: string;
|
|
13
|
+
platform: string;
|
|
14
|
+
hostname: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a stable machine identifier based on OS
|
|
19
|
+
* - Windows: MachineGuid from registry
|
|
20
|
+
* - macOS: IOPlatformUUID from system
|
|
21
|
+
* - Linux: /etc/machine-id
|
|
22
|
+
*/
|
|
23
|
+
function getMachineId(): string {
|
|
24
|
+
try {
|
|
25
|
+
const platform = os.platform();
|
|
26
|
+
|
|
27
|
+
if (platform === 'win32') {
|
|
28
|
+
// Windows: Use MachineGuid from registry
|
|
29
|
+
const output = execSync(
|
|
30
|
+
'reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
|
31
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
32
|
+
);
|
|
33
|
+
const match = output.match(/MachineGuid\s+REG_SZ\s+(.+)/);
|
|
34
|
+
if (match && match[1]) {
|
|
35
|
+
return match[1].trim();
|
|
36
|
+
}
|
|
37
|
+
} else if (platform === 'darwin') {
|
|
38
|
+
// macOS: Use hardware UUID
|
|
39
|
+
const output = execSync(
|
|
40
|
+
'ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID',
|
|
41
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
42
|
+
);
|
|
43
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"(.+)"/);
|
|
44
|
+
if (match && match[1]) {
|
|
45
|
+
return match[1];
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Linux: Use machine-id
|
|
49
|
+
const output = execSync('cat /etc/machine-id', {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
return output.trim();
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Fallback handled below
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback: Create a stable hash from hostname + username + home directory
|
|
60
|
+
// This is less reliable but works when we can't access system IDs
|
|
61
|
+
const fallbackData = [
|
|
62
|
+
os.hostname(),
|
|
63
|
+
os.userInfo().username,
|
|
64
|
+
os.homedir(),
|
|
65
|
+
os.platform(),
|
|
66
|
+
os.arch(),
|
|
67
|
+
].join('|');
|
|
68
|
+
|
|
69
|
+
return crypto.createHash('sha256').update(fallbackData).digest('hex').slice(0, 36);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get a complete device fingerprint
|
|
74
|
+
* The deviceHash is the primary identifier used for trial tracking
|
|
75
|
+
*/
|
|
76
|
+
export function getDeviceFingerprint(): DeviceFingerprint {
|
|
77
|
+
const machineId = getMachineId();
|
|
78
|
+
|
|
79
|
+
// Collect stable machine characteristics
|
|
80
|
+
const fingerprintData = {
|
|
81
|
+
machineId,
|
|
82
|
+
hostname: os.hostname(),
|
|
83
|
+
username: os.userInfo().username,
|
|
84
|
+
platform: os.platform(),
|
|
85
|
+
arch: os.arch(),
|
|
86
|
+
cpuModel: os.cpus()[0]?.model || 'unknown',
|
|
87
|
+
totalMemory: Math.floor(os.totalmem() / (1024 * 1024 * 1024)), // GB rounded
|
|
88
|
+
homeDir: os.homedir(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Create a stable hash from all characteristics
|
|
92
|
+
const deviceHash = crypto
|
|
93
|
+
.createHash('sha256')
|
|
94
|
+
.update(JSON.stringify(fingerprintData))
|
|
95
|
+
.digest('hex');
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
machineId,
|
|
99
|
+
deviceHash,
|
|
100
|
+
platform: os.platform(),
|
|
101
|
+
hostname: os.hostname(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate that we can create a fingerprint
|
|
107
|
+
* Used for diagnostics
|
|
108
|
+
*/
|
|
109
|
+
export function canCreateFingerprint(): { success: boolean; error?: string } {
|
|
110
|
+
try {
|
|
111
|
+
const fp = getDeviceFingerprint();
|
|
112
|
+
if (fp.deviceHash && fp.deviceHash.length === 64) {
|
|
113
|
+
return { success: true };
|
|
114
|
+
}
|
|
115
|
+
return { success: false, error: 'Invalid fingerprint generated' };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|