@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.
@@ -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 || !this.apiKey) {
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: { 'Authorization': `Bearer ${this.apiKey}` },
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: { 'Authorization': `Bearer ${this.apiKey}` },
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.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {},
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
- if (!this.apiKey) {
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 login` first.'
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
- Authorization: `Bearer ${this.apiKey}`,
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
- Authorization: `Bearer ${this.apiKey}`,
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
- Authorization: `Bearer ${this.apiKey}`,
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: { Authorization: `Bearer ${this.apiKey}` },
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
- results.push('### Next Steps:\n');
1537
- results.push('1. **Set up Supabase:** Go to https://supabase.com and create a free project');
1538
- results.push('2. **Add credentials:** Copy your Supabase URL and anon key to `.env.local`');
1539
- results.push('3. **Start building:** Just tell me what features you want!\n');
1540
- results.push('### Example:\n');
1541
- results.push('> "Add user authentication with email/password"');
1542
- results.push('> "Create a dashboard with stats cards"');
1543
- results.push('> "Build a todo list with CRUD operations"');
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: { Authorization: `Bearer ${this.apiKey}` },
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
- Authorization: `Bearer ${this.apiKey}`,
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
- Authorization: `Bearer ${this.apiKey}`,
3089
+ ...this.getAuthHeaders(),
2747
3090
  },
2748
3091
  body: JSON.stringify({
2749
3092
  eventType,