@codebakers/cli 2.8.0 → 3.0.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/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 +173 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +83 -1
- package/dist/index.js +24 -6
- package/dist/lib/fingerprint.d.ts +23 -0
- package/dist/lib/fingerprint.js +136 -0
- package/dist/mcp/server.js +343 -26
- package/package.json +1 -1
- package/src/commands/billing.ts +99 -0
- package/src/commands/extend.ts +157 -0
- package/src/commands/go.ts +197 -0
- package/src/config.ts +101 -1
- package/src/index.ts +27 -6
- package/src/lib/fingerprint.ts +122 -0
- package/src/mcp/server.ts +372 -29
|
@@ -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
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ErrorCode,
|
|
9
9
|
McpError,
|
|
10
10
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
-
import { getApiKey, getApiUrl, getExperienceLevel, setExperienceLevel, getServiceKey, setServiceKey, type ExperienceLevel } from '../config.js';
|
|
11
|
+
import { getApiKey, getApiUrl, getExperienceLevel, setExperienceLevel, getServiceKey, setServiceKey, getTrialState, isTrialExpired, getTrialDaysRemaining, hasValidAccess, getAuthMode, type ExperienceLevel, type TrialState } from '../config.js';
|
|
12
12
|
import { audit as runAudit } from '../commands/audit.js';
|
|
13
13
|
import { heal as runHeal } from '../commands/heal.js';
|
|
14
14
|
import { getCliVersion } from '../lib/api.js';
|
|
@@ -61,12 +61,16 @@ class CodeBakersServer {
|
|
|
61
61
|
private server: Server;
|
|
62
62
|
private apiKey: string | null;
|
|
63
63
|
private apiUrl: string;
|
|
64
|
+
private trialState: TrialState | null;
|
|
65
|
+
private authMode: 'apiKey' | 'trial' | 'none';
|
|
64
66
|
private autoUpdateChecked = false;
|
|
65
67
|
private autoUpdateInProgress = false;
|
|
66
68
|
|
|
67
69
|
constructor() {
|
|
68
70
|
this.apiKey = getApiKey();
|
|
69
71
|
this.apiUrl = getApiUrl();
|
|
72
|
+
this.trialState = getTrialState();
|
|
73
|
+
this.authMode = getAuthMode();
|
|
70
74
|
|
|
71
75
|
this.server = new Server(
|
|
72
76
|
{
|
|
@@ -88,12 +92,28 @@ class CodeBakersServer {
|
|
|
88
92
|
});
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Get authorization headers for API requests
|
|
97
|
+
* Supports both API key (paid users) and trial ID (free users)
|
|
98
|
+
*/
|
|
99
|
+
private getAuthHeaders(): Record<string, string> {
|
|
100
|
+
if (this.apiKey) {
|
|
101
|
+
return { 'Authorization': `Bearer ${this.apiKey}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.trialState?.trialId) {
|
|
105
|
+
return { 'X-Trial-Id': this.trialState.trialId };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
/**
|
|
92
112
|
* Automatically check for and apply pattern updates
|
|
93
113
|
* Runs silently in background - no user intervention needed
|
|
94
114
|
*/
|
|
95
115
|
private async checkAndAutoUpdate(): Promise<void> {
|
|
96
|
-
if (this.autoUpdateChecked || this.autoUpdateInProgress ||
|
|
116
|
+
if (this.autoUpdateChecked || this.autoUpdateInProgress || this.authMode === 'none') {
|
|
97
117
|
return;
|
|
98
118
|
}
|
|
99
119
|
|
|
@@ -131,7 +151,7 @@ class CodeBakersServer {
|
|
|
131
151
|
|
|
132
152
|
// Fetch latest version
|
|
133
153
|
const response = await fetch(`${this.apiUrl}/api/content/version`, {
|
|
134
|
-
headers:
|
|
154
|
+
headers: this.getAuthHeaders(),
|
|
135
155
|
});
|
|
136
156
|
|
|
137
157
|
if (!response.ok) {
|
|
@@ -153,7 +173,7 @@ class CodeBakersServer {
|
|
|
153
173
|
|
|
154
174
|
// Fetch full content and update
|
|
155
175
|
const contentResponse = await fetch(`${this.apiUrl}/api/content`, {
|
|
156
|
-
headers:
|
|
176
|
+
headers: this.getAuthHeaders(),
|
|
157
177
|
});
|
|
158
178
|
|
|
159
179
|
if (!contentResponse.ok) {
|
|
@@ -400,7 +420,7 @@ class CodeBakersServer {
|
|
|
400
420
|
let latest: { version: string; moduleCount: number } | null = null;
|
|
401
421
|
try {
|
|
402
422
|
const response = await fetch(`${this.apiUrl}/api/content/version`, {
|
|
403
|
-
headers: this.
|
|
423
|
+
headers: this.getAuthHeaders(),
|
|
404
424
|
});
|
|
405
425
|
if (response.ok) {
|
|
406
426
|
latest = await response.json();
|
|
@@ -566,7 +586,7 @@ class CodeBakersServer {
|
|
|
566
586
|
{
|
|
567
587
|
name: 'scaffold_project',
|
|
568
588
|
description:
|
|
569
|
-
'Create a new project from scratch with Next.js + Supabase + Drizzle. Use this when user wants to build something new and no project exists yet. Creates all files, installs dependencies, and sets up CodeBakers patterns automatically.',
|
|
589
|
+
'Create a new project from scratch with Next.js + Supabase + Drizzle. Use this when user wants to build something new and no project exists yet. Creates all files, installs dependencies, and sets up CodeBakers patterns automatically. Set fullDeploy=true for seamless idea-to-deployment (creates GitHub repo, Supabase project, and deploys to Vercel). When fullDeploy=true, first call returns explanation - then call again with deployConfirmed=true after user confirms.',
|
|
570
590
|
inputSchema: {
|
|
571
591
|
type: 'object' as const,
|
|
572
592
|
properties: {
|
|
@@ -578,6 +598,14 @@ class CodeBakersServer {
|
|
|
578
598
|
type: 'string',
|
|
579
599
|
description: 'Brief description of what the project is for (used in PRD.md)',
|
|
580
600
|
},
|
|
601
|
+
fullDeploy: {
|
|
602
|
+
type: 'boolean',
|
|
603
|
+
description: 'If true, enables full deployment flow (GitHub + Supabase + Vercel). First call returns explanation for user confirmation.',
|
|
604
|
+
},
|
|
605
|
+
deployConfirmed: {
|
|
606
|
+
type: 'boolean',
|
|
607
|
+
description: 'Set to true AFTER user confirms they want full deployment. Only set this after showing user the explanation and getting their approval.',
|
|
608
|
+
},
|
|
581
609
|
},
|
|
582
610
|
required: ['projectName'],
|
|
583
611
|
},
|
|
@@ -871,13 +899,38 @@ class CodeBakersServer {
|
|
|
871
899
|
|
|
872
900
|
// Handle tool calls
|
|
873
901
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
874
|
-
|
|
902
|
+
// Check access: API key OR valid trial
|
|
903
|
+
if (this.authMode === 'none') {
|
|
875
904
|
throw new McpError(
|
|
876
905
|
ErrorCode.InvalidRequest,
|
|
877
|
-
'Not logged in. Run `codebakers
|
|
906
|
+
'Not logged in. Run `codebakers go` to start a free trial, or `codebakers setup` if you have an account.'
|
|
878
907
|
);
|
|
879
908
|
}
|
|
880
909
|
|
|
910
|
+
// Check if trial expired
|
|
911
|
+
if (this.authMode === 'trial' && isTrialExpired()) {
|
|
912
|
+
const trialState = getTrialState();
|
|
913
|
+
if (trialState?.stage === 'anonymous') {
|
|
914
|
+
throw new McpError(
|
|
915
|
+
ErrorCode.InvalidRequest,
|
|
916
|
+
'Trial expired. Run `codebakers extend` to add 7 more days with GitHub, or `codebakers billing` to upgrade.'
|
|
917
|
+
);
|
|
918
|
+
} else {
|
|
919
|
+
throw new McpError(
|
|
920
|
+
ErrorCode.InvalidRequest,
|
|
921
|
+
'Trial expired. Run `codebakers billing` to upgrade to a paid plan.'
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Show warning if trial expiring soon
|
|
927
|
+
if (this.authMode === 'trial') {
|
|
928
|
+
const daysRemaining = getTrialDaysRemaining();
|
|
929
|
+
if (daysRemaining <= 2) {
|
|
930
|
+
console.error(`[CodeBakers] Trial expires in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}. Run 'codebakers extend' or 'codebakers billing'.`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
881
934
|
const { name, arguments: args } = request.params;
|
|
882
935
|
|
|
883
936
|
switch (name) {
|
|
@@ -900,7 +953,7 @@ class CodeBakersServer {
|
|
|
900
953
|
return this.handleGetPatternSection(args as { pattern: string; section: string });
|
|
901
954
|
|
|
902
955
|
case 'scaffold_project':
|
|
903
|
-
return this.handleScaffoldProject(args as { projectName: string; description?: string });
|
|
956
|
+
return this.handleScaffoldProject(args as { projectName: string; description?: string; fullDeploy?: boolean; deployConfirmed?: boolean });
|
|
904
957
|
|
|
905
958
|
case 'init_project':
|
|
906
959
|
return this.handleInitProject(args as { projectName?: string });
|
|
@@ -969,7 +1022,7 @@ class CodeBakersServer {
|
|
|
969
1022
|
method: 'POST',
|
|
970
1023
|
headers: {
|
|
971
1024
|
'Content-Type': 'application/json',
|
|
972
|
-
|
|
1025
|
+
...this.getAuthHeaders(),
|
|
973
1026
|
},
|
|
974
1027
|
body: JSON.stringify({
|
|
975
1028
|
prompt: userRequest,
|
|
@@ -1095,9 +1148,7 @@ Show the user what their simple request was expanded into, then proceed with the
|
|
|
1095
1148
|
private async handleListPatterns() {
|
|
1096
1149
|
const response = await fetch(`${this.apiUrl}/api/patterns`, {
|
|
1097
1150
|
method: 'GET',
|
|
1098
|
-
headers:
|
|
1099
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
1100
|
-
},
|
|
1151
|
+
headers: this.getAuthHeaders(),
|
|
1101
1152
|
});
|
|
1102
1153
|
|
|
1103
1154
|
if (!response.ok) {
|
|
@@ -1156,7 +1207,7 @@ Show the user what their simple request was expanded into, then proceed with the
|
|
|
1156
1207
|
method: 'POST',
|
|
1157
1208
|
headers: {
|
|
1158
1209
|
'Content-Type': 'application/json',
|
|
1159
|
-
|
|
1210
|
+
...this.getAuthHeaders(),
|
|
1160
1211
|
},
|
|
1161
1212
|
body: JSON.stringify({ patterns }),
|
|
1162
1213
|
});
|
|
@@ -1180,7 +1231,7 @@ Show the user what their simple request was expanded into, then proceed with the
|
|
|
1180
1231
|
method: 'POST',
|
|
1181
1232
|
headers: {
|
|
1182
1233
|
'Content-Type': 'application/json',
|
|
1183
|
-
|
|
1234
|
+
...this.getAuthHeaders(),
|
|
1184
1235
|
},
|
|
1185
1236
|
body: JSON.stringify({ query }),
|
|
1186
1237
|
});
|
|
@@ -1376,10 +1427,15 @@ Show the user what their simple request was expanded into, then proceed with the
|
|
|
1376
1427
|
};
|
|
1377
1428
|
}
|
|
1378
1429
|
|
|
1379
|
-
private async handleScaffoldProject(args: { projectName: string; description?: string }) {
|
|
1380
|
-
const { projectName, description } = args;
|
|
1430
|
+
private async handleScaffoldProject(args: { projectName: string; description?: string; fullDeploy?: boolean; deployConfirmed?: boolean }) {
|
|
1431
|
+
const { projectName, description, fullDeploy, deployConfirmed } = args;
|
|
1381
1432
|
const cwd = process.cwd();
|
|
1382
1433
|
|
|
1434
|
+
// If fullDeploy requested but not confirmed, show explanation and ask for confirmation
|
|
1435
|
+
if (fullDeploy && !deployConfirmed) {
|
|
1436
|
+
return this.showFullDeployExplanation(projectName, description);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1383
1439
|
// Check if directory has files
|
|
1384
1440
|
const files = fs.readdirSync(cwd);
|
|
1385
1441
|
const hasFiles = files.filter(f => !f.startsWith('.')).length > 0;
|
|
@@ -1462,7 +1518,7 @@ Show the user what their simple request was expanded into, then proceed with the
|
|
|
1462
1518
|
|
|
1463
1519
|
const response = await fetch(`${this.apiUrl}/api/content`, {
|
|
1464
1520
|
method: 'GET',
|
|
1465
|
-
headers:
|
|
1521
|
+
headers: this.getAuthHeaders(),
|
|
1466
1522
|
});
|
|
1467
1523
|
|
|
1468
1524
|
if (response.ok) {
|
|
@@ -1533,14 +1589,22 @@ phase: setup
|
|
|
1533
1589
|
|
|
1534
1590
|
results.push('\n---\n');
|
|
1535
1591
|
results.push('## ✅ Project Created Successfully!\n');
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1592
|
+
|
|
1593
|
+
// If fullDeploy is enabled and confirmed, proceed with cloud deployment
|
|
1594
|
+
if (fullDeploy && deployConfirmed) {
|
|
1595
|
+
results.push('## 🚀 Starting Full Deployment...\n');
|
|
1596
|
+
const deployResults = await this.executeFullDeploy(projectName, cwd, description);
|
|
1597
|
+
results.push(...deployResults);
|
|
1598
|
+
} else {
|
|
1599
|
+
results.push('### Next Steps:\n');
|
|
1600
|
+
results.push('1. **Set up Supabase:** Go to https://supabase.com and create a free project');
|
|
1601
|
+
results.push('2. **Add credentials:** Copy your Supabase URL and anon key to `.env.local`');
|
|
1602
|
+
results.push('3. **Start building:** Just tell me what features you want!\n');
|
|
1603
|
+
results.push('### Example:\n');
|
|
1604
|
+
results.push('> "Add user authentication with email/password"');
|
|
1605
|
+
results.push('> "Create a dashboard with stats cards"');
|
|
1606
|
+
results.push('> "Build a todo list with CRUD operations"');
|
|
1607
|
+
}
|
|
1544
1608
|
|
|
1545
1609
|
} catch (error) {
|
|
1546
1610
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -1555,6 +1619,285 @@ phase: setup
|
|
|
1555
1619
|
};
|
|
1556
1620
|
}
|
|
1557
1621
|
|
|
1622
|
+
/**
|
|
1623
|
+
* Show explanation of what fullDeploy will do and ask for confirmation
|
|
1624
|
+
*/
|
|
1625
|
+
private showFullDeployExplanation(projectName: string, description?: string) {
|
|
1626
|
+
const explanation = `# 🚀 Full Deployment: ${projectName}
|
|
1627
|
+
|
|
1628
|
+
## What This Will Do
|
|
1629
|
+
|
|
1630
|
+
Full deployment creates a complete production-ready environment automatically:
|
|
1631
|
+
|
|
1632
|
+
### 1. 📁 Local Project
|
|
1633
|
+
- Create Next.js + Supabase + Drizzle project
|
|
1634
|
+
- Install all dependencies
|
|
1635
|
+
- Set up CodeBakers patterns
|
|
1636
|
+
|
|
1637
|
+
### 2. 🐙 GitHub Repository
|
|
1638
|
+
- Create a new private repository: \`${projectName}\`
|
|
1639
|
+
- Initialize git and push code
|
|
1640
|
+
- Set up .gitignore properly
|
|
1641
|
+
|
|
1642
|
+
### 3. 🗄️ Supabase Project
|
|
1643
|
+
- Create a new Supabase project
|
|
1644
|
+
- Get database connection string
|
|
1645
|
+
- Get API keys (anon + service role)
|
|
1646
|
+
- Auto-configure .env.local
|
|
1647
|
+
|
|
1648
|
+
### 4. 🔺 Vercel Deployment
|
|
1649
|
+
- Deploy to Vercel
|
|
1650
|
+
- Connect to GitHub for auto-deploys
|
|
1651
|
+
- Set all environment variables
|
|
1652
|
+
- Get your live URL
|
|
1653
|
+
|
|
1654
|
+
---
|
|
1655
|
+
|
|
1656
|
+
## Requirements
|
|
1657
|
+
|
|
1658
|
+
Make sure you have these CLIs installed and authenticated:
|
|
1659
|
+
- \`gh\` - GitHub CLI (run: \`gh auth login\`)
|
|
1660
|
+
- \`supabase\` - Supabase CLI (run: \`supabase login\`)
|
|
1661
|
+
- \`vercel\` - Vercel CLI (run: \`vercel login\`)
|
|
1662
|
+
|
|
1663
|
+
---
|
|
1664
|
+
|
|
1665
|
+
## 🎯 Result
|
|
1666
|
+
|
|
1667
|
+
After completion, you'll have:
|
|
1668
|
+
- ✅ GitHub repo with your code
|
|
1669
|
+
- ✅ Supabase project with database ready
|
|
1670
|
+
- ✅ Live URL on Vercel
|
|
1671
|
+
- ✅ Auto-deploys on every push
|
|
1672
|
+
|
|
1673
|
+
---
|
|
1674
|
+
|
|
1675
|
+
**⚠️ IMPORTANT: Ask the user to confirm before proceeding.**
|
|
1676
|
+
|
|
1677
|
+
To proceed, call \`scaffold_project\` again with:
|
|
1678
|
+
\`\`\`json
|
|
1679
|
+
{
|
|
1680
|
+
"projectName": "${projectName}",
|
|
1681
|
+
"description": "${description || ''}",
|
|
1682
|
+
"fullDeploy": true,
|
|
1683
|
+
"deployConfirmed": true
|
|
1684
|
+
}
|
|
1685
|
+
\`\`\`
|
|
1686
|
+
|
|
1687
|
+
Or if user declines, call without fullDeploy:
|
|
1688
|
+
\`\`\`json
|
|
1689
|
+
{
|
|
1690
|
+
"projectName": "${projectName}",
|
|
1691
|
+
"description": "${description || ''}"
|
|
1692
|
+
}
|
|
1693
|
+
\`\`\`
|
|
1694
|
+
`;
|
|
1695
|
+
|
|
1696
|
+
return {
|
|
1697
|
+
content: [{
|
|
1698
|
+
type: 'text' as const,
|
|
1699
|
+
text: explanation,
|
|
1700
|
+
}],
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Execute full cloud deployment (GitHub + Supabase + Vercel)
|
|
1706
|
+
*/
|
|
1707
|
+
private async executeFullDeploy(projectName: string, cwd: string, description?: string): Promise<string[]> {
|
|
1708
|
+
const results: string[] = [];
|
|
1709
|
+
|
|
1710
|
+
// Check for required CLIs
|
|
1711
|
+
const cliChecks = this.checkRequiredCLIs();
|
|
1712
|
+
if (cliChecks.missing.length > 0) {
|
|
1713
|
+
results.push('### ❌ Missing Required CLIs\n');
|
|
1714
|
+
results.push('The following CLIs are required for full deployment:\n');
|
|
1715
|
+
for (const cli of cliChecks.missing) {
|
|
1716
|
+
results.push(`- **${cli.name}**: ${cli.installCmd}`);
|
|
1717
|
+
}
|
|
1718
|
+
results.push('\nInstall the missing CLIs and try again.');
|
|
1719
|
+
return results;
|
|
1720
|
+
}
|
|
1721
|
+
results.push('✓ All required CLIs found\n');
|
|
1722
|
+
|
|
1723
|
+
// Step 1: Initialize Git and create GitHub repo
|
|
1724
|
+
results.push('### Step 1: GitHub Repository\n');
|
|
1725
|
+
try {
|
|
1726
|
+
// Initialize git
|
|
1727
|
+
execSync('git init', { cwd, stdio: 'pipe' });
|
|
1728
|
+
execSync('git add .', { cwd, stdio: 'pipe' });
|
|
1729
|
+
execSync('git commit -m "Initial commit from CodeBakers"', { cwd, stdio: 'pipe' });
|
|
1730
|
+
results.push('✓ Initialized git repository');
|
|
1731
|
+
|
|
1732
|
+
// Create GitHub repo
|
|
1733
|
+
const ghDescription = description || `${projectName} - Created with CodeBakers`;
|
|
1734
|
+
execSync(`gh repo create ${projectName} --private --source=. --push --description "${ghDescription}"`, { cwd, stdio: 'pipe' });
|
|
1735
|
+
results.push(`✓ Created GitHub repo: ${projectName}`);
|
|
1736
|
+
results.push(` → https://github.com/${this.getGitHubUsername()}/${projectName}\n`);
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1739
|
+
results.push(`⚠️ GitHub setup failed: ${msg}`);
|
|
1740
|
+
results.push(' You can create the repo manually: gh repo create\n');
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Step 2: Create Supabase project
|
|
1744
|
+
results.push('### Step 2: Supabase Project\n');
|
|
1745
|
+
try {
|
|
1746
|
+
// Create Supabase project (this may take a while)
|
|
1747
|
+
const orgId = this.getSupabaseOrgId();
|
|
1748
|
+
if (orgId) {
|
|
1749
|
+
execSync(`supabase projects create ${projectName} --org-id ${orgId} --region us-east-1 --db-password "${this.generatePassword()}"`, { cwd, stdio: 'pipe', timeout: 120000 });
|
|
1750
|
+
results.push(`✓ Created Supabase project: ${projectName}`);
|
|
1751
|
+
|
|
1752
|
+
// Get project credentials
|
|
1753
|
+
const projectsOutput = execSync('supabase projects list --output json', { cwd, encoding: 'utf-8' });
|
|
1754
|
+
const projects = JSON.parse(projectsOutput);
|
|
1755
|
+
const newProject = projects.find((p: { name: string }) => p.name === projectName);
|
|
1756
|
+
|
|
1757
|
+
if (newProject) {
|
|
1758
|
+
// Update .env.local with Supabase credentials
|
|
1759
|
+
const envPath = path.join(cwd, '.env.local');
|
|
1760
|
+
let envContent = fs.readFileSync(envPath, 'utf-8');
|
|
1761
|
+
envContent = envContent.replace('your-supabase-url', `https://${newProject.id}.supabase.co`);
|
|
1762
|
+
envContent = envContent.replace('your-anon-key', newProject.anon_key || 'YOUR_ANON_KEY');
|
|
1763
|
+
fs.writeFileSync(envPath, envContent);
|
|
1764
|
+
results.push('✓ Updated .env.local with Supabase credentials\n');
|
|
1765
|
+
}
|
|
1766
|
+
} else {
|
|
1767
|
+
results.push('⚠️ Could not detect Supabase organization');
|
|
1768
|
+
results.push(' Run: supabase orgs list\n');
|
|
1769
|
+
}
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1772
|
+
results.push(`⚠️ Supabase setup failed: ${msg}`);
|
|
1773
|
+
results.push(' Create project manually at: https://supabase.com/dashboard\n');
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Step 3: Deploy to Vercel
|
|
1777
|
+
results.push('### Step 3: Vercel Deployment\n');
|
|
1778
|
+
try {
|
|
1779
|
+
// Link to Vercel (creates new project)
|
|
1780
|
+
execSync('vercel link --yes', { cwd, stdio: 'pipe' });
|
|
1781
|
+
results.push('✓ Linked to Vercel');
|
|
1782
|
+
|
|
1783
|
+
// Set environment variables from .env.local
|
|
1784
|
+
const envPath = path.join(cwd, '.env.local');
|
|
1785
|
+
if (fs.existsSync(envPath)) {
|
|
1786
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
1787
|
+
const envVars = envContent.split('\n')
|
|
1788
|
+
.filter(line => line.includes('=') && !line.startsWith('#'))
|
|
1789
|
+
.map(line => {
|
|
1790
|
+
const [key, ...valueParts] = line.split('=');
|
|
1791
|
+
return { key: key.trim(), value: valueParts.join('=').trim() };
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
for (const { key, value } of envVars) {
|
|
1795
|
+
if (value && !value.includes('your-')) {
|
|
1796
|
+
try {
|
|
1797
|
+
execSync(`vercel env add ${key} production <<< "${value}"`, { cwd, stdio: 'pipe', shell: 'bash' });
|
|
1798
|
+
} catch {
|
|
1799
|
+
// Env var might already exist, try to update
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
results.push('✓ Set environment variables');
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Deploy to production
|
|
1807
|
+
const deployOutput = execSync('vercel --prod --yes', { cwd, encoding: 'utf-8' });
|
|
1808
|
+
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
1809
|
+
const deployUrl = urlMatch ? urlMatch[0] : 'Check Vercel dashboard';
|
|
1810
|
+
results.push(`✓ Deployed to Vercel`);
|
|
1811
|
+
results.push(` → ${deployUrl}\n`);
|
|
1812
|
+
|
|
1813
|
+
// Connect to GitHub for auto-deploys
|
|
1814
|
+
try {
|
|
1815
|
+
execSync('vercel git connect --yes', { cwd, stdio: 'pipe' });
|
|
1816
|
+
results.push('✓ Connected to GitHub for auto-deploys\n');
|
|
1817
|
+
} catch {
|
|
1818
|
+
results.push('⚠️ Could not auto-connect to GitHub\n');
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1823
|
+
results.push(`⚠️ Vercel deployment failed: ${msg}`);
|
|
1824
|
+
results.push(' Deploy manually: vercel --prod\n');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Summary
|
|
1828
|
+
results.push('---\n');
|
|
1829
|
+
results.push('## 🎉 Full Deployment Complete!\n');
|
|
1830
|
+
results.push('Your project is now live with:');
|
|
1831
|
+
results.push('- GitHub repo with CI/CD ready');
|
|
1832
|
+
results.push('- Supabase database configured');
|
|
1833
|
+
results.push('- Vercel hosting with auto-deploys\n');
|
|
1834
|
+
results.push('**Start building features - every push auto-deploys!**');
|
|
1835
|
+
|
|
1836
|
+
return results;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Check if required CLIs are installed
|
|
1841
|
+
*/
|
|
1842
|
+
private checkRequiredCLIs(): { installed: string[]; missing: { name: string; installCmd: string }[] } {
|
|
1843
|
+
const clis = [
|
|
1844
|
+
{ name: 'gh', cmd: 'gh --version', installCmd: 'npm install -g gh' },
|
|
1845
|
+
{ name: 'supabase', cmd: 'supabase --version', installCmd: 'npm install -g supabase' },
|
|
1846
|
+
{ name: 'vercel', cmd: 'vercel --version', installCmd: 'npm install -g vercel' },
|
|
1847
|
+
];
|
|
1848
|
+
|
|
1849
|
+
const installed: string[] = [];
|
|
1850
|
+
const missing: { name: string; installCmd: string }[] = [];
|
|
1851
|
+
|
|
1852
|
+
for (const cli of clis) {
|
|
1853
|
+
try {
|
|
1854
|
+
execSync(cli.cmd, { stdio: 'pipe' });
|
|
1855
|
+
installed.push(cli.name);
|
|
1856
|
+
} catch {
|
|
1857
|
+
missing.push({ name: cli.name, installCmd: cli.installCmd });
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return { installed, missing };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Get GitHub username from gh CLI
|
|
1866
|
+
*/
|
|
1867
|
+
private getGitHubUsername(): string {
|
|
1868
|
+
try {
|
|
1869
|
+
const output = execSync('gh api user --jq .login', { encoding: 'utf-8' });
|
|
1870
|
+
return output.trim();
|
|
1871
|
+
} catch {
|
|
1872
|
+
return 'YOUR_USERNAME';
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Get Supabase organization ID
|
|
1878
|
+
*/
|
|
1879
|
+
private getSupabaseOrgId(): string | null {
|
|
1880
|
+
try {
|
|
1881
|
+
const output = execSync('supabase orgs list --output json', { encoding: 'utf-8' });
|
|
1882
|
+
const orgs = JSON.parse(output);
|
|
1883
|
+
return orgs[0]?.id || null;
|
|
1884
|
+
} catch {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* Generate a secure random password for Supabase
|
|
1891
|
+
*/
|
|
1892
|
+
private generatePassword(): string {
|
|
1893
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
1894
|
+
let password = '';
|
|
1895
|
+
for (let i = 0; i < 24; i++) {
|
|
1896
|
+
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1897
|
+
}
|
|
1898
|
+
return password;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1558
1901
|
private async handleInitProject(args: { projectName?: string }) {
|
|
1559
1902
|
const cwd = process.cwd();
|
|
1560
1903
|
const results: string[] = [];
|
|
@@ -1581,7 +1924,7 @@ phase: setup
|
|
|
1581
1924
|
try {
|
|
1582
1925
|
const response = await fetch(`${this.apiUrl}/api/content`, {
|
|
1583
1926
|
method: 'GET',
|
|
1584
|
-
headers:
|
|
1927
|
+
headers: this.getAuthHeaders(),
|
|
1585
1928
|
});
|
|
1586
1929
|
|
|
1587
1930
|
if (!response.ok) {
|
|
@@ -2691,7 +3034,7 @@ Just describe what you want to build! I'll automatically:
|
|
|
2691
3034
|
method: 'POST',
|
|
2692
3035
|
headers: {
|
|
2693
3036
|
'Content-Type': 'application/json',
|
|
2694
|
-
|
|
3037
|
+
...this.getAuthHeaders(),
|
|
2695
3038
|
},
|
|
2696
3039
|
body: JSON.stringify({
|
|
2697
3040
|
category,
|
|
@@ -2743,7 +3086,7 @@ Just describe what you want to build! I'll automatically:
|
|
|
2743
3086
|
method: 'POST',
|
|
2744
3087
|
headers: {
|
|
2745
3088
|
'Content-Type': 'application/json',
|
|
2746
|
-
|
|
3089
|
+
...this.getAuthHeaders(),
|
|
2747
3090
|
},
|
|
2748
3091
|
body: JSON.stringify({
|
|
2749
3092
|
eventType,
|