@codebakers/cli 1.5.0 → 2.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.
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { createInterface } from 'readline';
4
- import { setApiKey, getApiUrl } from '../config.js';
4
+ import { setApiKey } from '../config.js';
5
+ import { validateApiKey, formatApiError, type ApiError } from '../lib/api.js';
5
6
 
6
7
  async function prompt(question: string): Promise<string> {
7
8
  const rl = createInterface({
@@ -12,7 +13,7 @@ async function prompt(question: string): Promise<string> {
12
13
  return new Promise((resolve) => {
13
14
  rl.question(question, (answer) => {
14
15
  rl.close();
15
- resolve(answer);
16
+ resolve(answer.trim());
16
17
  });
17
18
  });
18
19
  }
@@ -23,34 +24,29 @@ export async function login(): Promise<void> {
23
24
 
24
25
  const apiKey = await prompt(' Enter your API key: ');
25
26
 
26
- if (!apiKey || !apiKey.startsWith('cb_')) {
27
- console.log(chalk.red('\n Invalid API key format. Keys start with "cb_"\n'));
27
+ if (!apiKey) {
28
+ console.log(chalk.red('\n API key is required.\n'));
28
29
  process.exit(1);
29
30
  }
30
31
 
31
32
  const spinner = ora('Validating API key...').start();
32
33
 
33
34
  try {
34
- const apiUrl = getApiUrl();
35
- const response = await fetch(`${apiUrl}/api/content`, {
36
- method: 'GET',
37
- headers: {
38
- Authorization: `Bearer ${apiKey}`,
39
- },
40
- });
41
-
42
- if (!response.ok) {
43
- const error = await response.json().catch(() => ({}));
44
- throw new Error(error.error || 'Invalid API key');
45
- }
35
+ await validateApiKey(apiKey);
46
36
 
47
37
  setApiKey(apiKey);
48
38
  spinner.succeed('Logged in successfully!');
49
39
  console.log(chalk.green('\n You can now run `codebakers install` in your project.\n'));
50
40
  } catch (error) {
51
41
  spinner.fail('Login failed');
52
- const message = error instanceof Error ? error.message : 'Unknown error';
53
- console.log(chalk.red(`\n Error: ${message}\n`));
42
+
43
+ if (error && typeof error === 'object' && 'recoverySteps' in error) {
44
+ console.log(chalk.red(`\n ${formatApiError(error as ApiError)}\n`));
45
+ } else {
46
+ const message = error instanceof Error ? error.message : 'Unknown error';
47
+ console.log(chalk.red(`\n Error: ${message}\n`));
48
+ }
49
+
54
50
  process.exit(1);
55
51
  }
56
52
  }
@@ -1,8 +1,26 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { execSync, exec } from 'child_process';
3
+ import crypto from 'crypto';
4
+ import { execSync } from 'child_process';
4
5
  import { createInterface } from 'readline';
5
- import { getServiceKey, setServiceKey, type ServiceName } from '../config.js';
6
+ import { getServiceKey, setServiceKey, SERVICE_KEY_LABELS, type ServiceName } from '../config.js';
7
+
8
+ /**
9
+ * Supabase regions - allow user to select closest region
10
+ */
11
+ const SUPABASE_REGIONS = [
12
+ { id: 'us-east-1', name: 'US East (N. Virginia)', flag: '🇺🇸' },
13
+ { id: 'us-west-1', name: 'US West (N. California)', flag: '🇺🇸' },
14
+ { id: 'eu-west-1', name: 'EU West (Ireland)', flag: '🇮🇪' },
15
+ { id: 'eu-west-2', name: 'EU West (London)', flag: '🇬🇧' },
16
+ { id: 'eu-central-1', name: 'EU Central (Frankfurt)', flag: '🇩🇪' },
17
+ { id: 'ap-southeast-1', name: 'Asia Pacific (Singapore)', flag: '🇸🇬' },
18
+ { id: 'ap-southeast-2', name: 'Asia Pacific (Sydney)', flag: '🇦🇺' },
19
+ { id: 'ap-northeast-1', name: 'Asia Pacific (Tokyo)', flag: '🇯🇵' },
20
+ { id: 'sa-east-1', name: 'South America (São Paulo)', flag: '🇧🇷' },
21
+ ] as const;
22
+
23
+ type SupabaseRegion = typeof SUPABASE_REGIONS[number]['id'];
6
24
 
7
25
  function prompt(question: string): Promise<string> {
8
26
  const rl = createInterface({
@@ -28,13 +46,63 @@ export interface ProvisionResult {
28
46
  projectUrl: string;
29
47
  apiUrl: string;
30
48
  anonKey: string;
49
+ dbPassword: string;
31
50
  };
32
51
  vercel?: {
33
52
  projectId: string;
34
53
  projectUrl: string;
54
+ envVarsSet: boolean;
35
55
  };
36
56
  }
37
57
 
58
+ /**
59
+ * Retry a function with exponential backoff
60
+ */
61
+ async function withRetry<T>(
62
+ fn: () => Promise<T>,
63
+ options: {
64
+ retries?: number;
65
+ delay?: number;
66
+ onRetry?: (attempt: number, error: Error) => void;
67
+ } = {}
68
+ ): Promise<T> {
69
+ const { retries = 3, delay = 1000, onRetry } = options;
70
+
71
+ for (let attempt = 1; attempt <= retries; attempt++) {
72
+ try {
73
+ return await fn();
74
+ } catch (error) {
75
+ if (attempt === retries) {
76
+ throw error;
77
+ }
78
+
79
+ const err = error instanceof Error ? error : new Error(String(error));
80
+
81
+ // Don't retry on authentication errors
82
+ if (err.message.includes('401') || err.message.includes('403') || err.message.includes('Invalid')) {
83
+ throw error;
84
+ }
85
+
86
+ onRetry?.(attempt, err);
87
+ await new Promise(resolve => setTimeout(resolve, delay * attempt));
88
+ }
89
+ }
90
+
91
+ throw new Error('Max retries exceeded');
92
+ }
93
+
94
+ /**
95
+ * Generate a cryptographically secure password
96
+ */
97
+ function generateSecurePassword(): string {
98
+ // Generate 24 random bytes and convert to base64url (URL-safe, no special chars)
99
+ const bytes = crypto.randomBytes(24);
100
+ const password = bytes.toString('base64url');
101
+
102
+ // Ensure it meets typical password requirements (add special char and number)
103
+ return `${password}!1`;
104
+ }
105
+
38
106
  /**
39
107
  * Check if a service key is configured, prompt if not
40
108
  */
@@ -42,17 +110,17 @@ async function ensureServiceKey(service: ServiceName, instructions: string): Pro
42
110
  let key = getServiceKey(service);
43
111
 
44
112
  if (!key) {
45
- console.log(chalk.yellow(`\n No ${service} API key configured.\n`));
113
+ console.log(chalk.yellow(`\n No ${SERVICE_KEY_LABELS[service]} API key configured.\n`));
46
114
  console.log(chalk.gray(instructions));
47
115
 
48
- const inputKey = await prompt(`\n ${service} API key (or press Enter to skip): `);
116
+ const inputKey = await prompt(`\n ${SERVICE_KEY_LABELS[service]} API key (or press Enter to skip): `);
49
117
 
50
118
  if (inputKey) {
51
119
  setServiceKey(service, inputKey);
52
120
  key = inputKey;
53
- console.log(chalk.green(` ✓ ${service} key saved\n`));
121
+ console.log(chalk.green(` ✓ ${SERVICE_KEY_LABELS[service]} key saved\n`));
54
122
  } else {
55
- console.log(chalk.gray(` Skipping ${service} provisioning.\n`));
123
+ console.log(chalk.gray(` Skipping ${SERVICE_KEY_LABELS[service]} provisioning.\n`));
56
124
  return null;
57
125
  }
58
126
  }
@@ -60,6 +128,40 @@ async function ensureServiceKey(service: ServiceName, instructions: string): Pro
60
128
  return key;
61
129
  }
62
130
 
131
+ /**
132
+ * Prompt for Supabase region selection
133
+ */
134
+ async function selectSupabaseRegion(): Promise<SupabaseRegion> {
135
+ console.log(chalk.white('\n Select Supabase region (for lowest latency, pick closest to your users):\n'));
136
+
137
+ SUPABASE_REGIONS.forEach((region, index) => {
138
+ const isDefault = region.id === 'us-east-1';
139
+ console.log(
140
+ chalk.gray(` ${index + 1}. `) +
141
+ chalk.cyan(`${region.flag} ${region.name}`) +
142
+ (isDefault ? chalk.green(' (default)') : '')
143
+ );
144
+ });
145
+
146
+ console.log('');
147
+
148
+ const input = await prompt(` Enter 1-${SUPABASE_REGIONS.length} (or press Enter for default): `);
149
+
150
+ if (!input) {
151
+ return 'us-east-1';
152
+ }
153
+
154
+ const index = parseInt(input, 10) - 1;
155
+ if (index >= 0 && index < SUPABASE_REGIONS.length) {
156
+ const selected = SUPABASE_REGIONS[index];
157
+ console.log(chalk.green(` ✓ Selected: ${selected.flag} ${selected.name}\n`));
158
+ return selected.id;
159
+ }
160
+
161
+ console.log(chalk.yellow(' Invalid selection, using default (US East).\n'));
162
+ return 'us-east-1';
163
+ }
164
+
63
165
  /**
64
166
  * Create a GitHub repository
65
167
  */
@@ -71,7 +173,9 @@ export async function createGitHubRepo(
71
173
  try {
72
174
  execSync('gh --version', { stdio: 'pipe' });
73
175
  } catch {
74
- console.log(chalk.yellow(' GitHub CLI (gh) not found. Install it from: https://cli.github.com\n'));
176
+ console.log(chalk.yellow(' GitHub CLI (gh) not found.\n'));
177
+ console.log(chalk.gray(' Install from: https://cli.github.com'));
178
+ console.log(chalk.gray(' Or skip GitHub and create manually later.\n'));
75
179
  return null;
76
180
  }
77
181
 
@@ -80,16 +184,20 @@ export async function createGitHubRepo(
80
184
  execSync('gh auth status', { stdio: 'pipe' });
81
185
  } catch {
82
186
  console.log(chalk.yellow(' Not logged into GitHub CLI.\n'));
83
- console.log(chalk.gray(' Run: gh auth login\n'));
187
+ console.log(chalk.gray(' Run: gh auth login'));
188
+ console.log(chalk.gray(' Then try again.\n'));
84
189
  return null;
85
190
  }
86
191
 
87
192
  const spinner = ora('Creating GitHub repository...').start();
88
193
 
89
194
  try {
195
+ // Escape description for shell
196
+ const safeDescription = description.replace(/"/g, '\\"');
197
+
90
198
  // Create the repo
91
- const result = execSync(
92
- `gh repo create ${projectName} --public --description "${description}" --source . --remote origin --push`,
199
+ execSync(
200
+ `gh repo create ${projectName} --public --description "${safeDescription}" --source . --remote origin --push`,
93
201
  { encoding: 'utf-8', stdio: 'pipe' }
94
202
  );
95
203
 
@@ -107,8 +215,10 @@ export async function createGitHubRepo(
107
215
 
108
216
  if (message.includes('already exists')) {
109
217
  console.log(chalk.yellow(' Repository already exists. Skipping.\n'));
218
+ console.log(chalk.gray(' Tip: You can link an existing repo with: gh repo view\n'));
110
219
  } else {
111
- console.log(chalk.red(` Error: ${message}\n`));
220
+ console.log(chalk.red(` Error: ${message}`));
221
+ console.log(chalk.gray(' You can create the repository manually on GitHub.\n'));
112
222
  }
113
223
  return null;
114
224
  }
@@ -119,94 +229,118 @@ export async function createGitHubRepo(
119
229
  */
120
230
  export async function createSupabaseProject(
121
231
  projectName: string,
122
- organizationId?: string
123
- ): Promise<{ projectId: string; projectUrl: string; apiUrl: string; anonKey: string } | null> {
232
+ options?: {
233
+ organizationId?: string;
234
+ region?: SupabaseRegion;
235
+ }
236
+ ): Promise<{ projectId: string; projectUrl: string; apiUrl: string; anonKey: string; dbPassword: string } | null> {
124
237
  const accessToken = await ensureServiceKey('supabase',
125
238
  ' Get your access token from: https://supabase.com/dashboard/account/tokens'
126
239
  );
127
240
 
128
241
  if (!accessToken) return null;
129
242
 
243
+ // Select region if not provided
244
+ const region = options?.region || await selectSupabaseRegion();
245
+
130
246
  const spinner = ora('Creating Supabase project...').start();
131
247
 
132
248
  try {
249
+ let organizationId = options?.organizationId;
250
+
133
251
  // Get organization if not provided
134
252
  if (!organizationId) {
135
- const orgsResponse = await fetch('https://api.supabase.com/v1/organizations', {
136
- headers: {
137
- 'Authorization': `Bearer ${accessToken}`,
138
- },
139
- });
140
-
141
- if (!orgsResponse.ok) {
142
- throw new Error('Failed to fetch organizations. Check your access token.');
143
- }
253
+ spinner.text = 'Fetching organizations...';
254
+
255
+ const orgsResponse = await withRetry(
256
+ () => fetch('https://api.supabase.com/v1/organizations', {
257
+ headers: { 'Authorization': `Bearer ${accessToken}` },
258
+ }).then(r => {
259
+ if (!r.ok) throw new Error(`Failed to fetch organizations (${r.status})`);
260
+ return r.json();
261
+ }),
262
+ {
263
+ onRetry: (attempt) => {
264
+ spinner.text = `Fetching organizations (retry ${attempt})...`;
265
+ },
266
+ }
267
+ );
144
268
 
145
- const orgs = await orgsResponse.json();
146
- if (orgs.length === 0) {
147
- throw new Error('No organizations found. Create one at supabase.com first.');
269
+ if (!orgsResponse || orgsResponse.length === 0) {
270
+ throw new Error('No organizations found. Create one at supabase.com/dashboard first.');
148
271
  }
149
272
 
150
- organizationId = orgs[0].id;
273
+ organizationId = orgsResponse[0].id;
151
274
  }
152
275
 
153
- // Generate a random password for the database
154
- const dbPassword = Math.random().toString(36).slice(-16) +
155
- Math.random().toString(36).slice(-16).toUpperCase() +
156
- '!1';
276
+ spinner.text = 'Creating Supabase project...';
277
+
278
+ // Generate a cryptographically secure password
279
+ const dbPassword = generateSecurePassword();
157
280
 
158
281
  // Create the project
159
- const createResponse = await fetch('https://api.supabase.com/v1/projects', {
160
- method: 'POST',
161
- headers: {
162
- 'Authorization': `Bearer ${accessToken}`,
163
- 'Content-Type': 'application/json',
164
- },
165
- body: JSON.stringify({
166
- name: projectName,
167
- organization_id: organizationId,
168
- region: 'us-east-1',
169
- plan: 'free',
170
- db_pass: dbPassword,
282
+ const project = await withRetry(
283
+ () => fetch('https://api.supabase.com/v1/projects', {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Authorization': `Bearer ${accessToken}`,
287
+ 'Content-Type': 'application/json',
288
+ },
289
+ body: JSON.stringify({
290
+ name: projectName,
291
+ organization_id: organizationId,
292
+ region: region,
293
+ plan: 'free',
294
+ db_pass: dbPassword,
295
+ }),
296
+ }).then(async r => {
297
+ if (!r.ok) {
298
+ const error = await r.json().catch(() => ({}));
299
+ throw new Error(error.message || `Failed to create project (${r.status})`);
300
+ }
301
+ return r.json();
171
302
  }),
172
- });
173
-
174
- if (!createResponse.ok) {
175
- const error = await createResponse.json();
176
- throw new Error(error.message || 'Failed to create project');
177
- }
178
-
179
- const project = await createResponse.json();
303
+ {
304
+ onRetry: (attempt) => {
305
+ spinner.text = `Creating Supabase project (retry ${attempt})...`;
306
+ },
307
+ }
308
+ );
180
309
 
181
310
  spinner.succeed('Supabase project created!');
182
311
 
183
- // Wait for project to be ready (it takes a moment)
312
+ // Wait for project to be ready
184
313
  const waitSpinner = ora('Waiting for project to be ready...').start();
185
314
 
186
315
  let projectReady = false;
187
316
  let attempts = 0;
188
317
  let projectDetails: { api_url?: string; anon_key?: string } = {};
189
318
 
190
- while (!projectReady && attempts < 30) {
319
+ while (!projectReady && attempts < 60) { // Up to 2 minutes
191
320
  await new Promise(resolve => setTimeout(resolve, 2000));
192
321
 
193
- const statusResponse = await fetch(`https://api.supabase.com/v1/projects/${project.id}`, {
194
- headers: {
195
- 'Authorization': `Bearer ${accessToken}`,
196
- },
197
- });
322
+ try {
323
+ const statusResponse = await fetch(`https://api.supabase.com/v1/projects/${project.id}`, {
324
+ headers: { 'Authorization': `Bearer ${accessToken}` },
325
+ });
198
326
 
199
- if (statusResponse.ok) {
200
- projectDetails = await statusResponse.json();
201
- if (projectDetails.api_url) {
202
- projectReady = true;
327
+ if (statusResponse.ok) {
328
+ projectDetails = await statusResponse.json();
329
+ if (projectDetails.api_url) {
330
+ projectReady = true;
331
+ }
203
332
  }
333
+ } catch {
334
+ // Ignore errors during polling
204
335
  }
336
+
205
337
  attempts++;
338
+ waitSpinner.text = `Waiting for project to be ready... (${attempts * 2}s)`;
206
339
  }
207
340
 
208
341
  if (!projectReady) {
209
- waitSpinner.warn('Project created but may not be fully ready yet.');
342
+ waitSpinner.warn('Project created but may need a few more minutes to be fully ready.');
343
+ console.log(chalk.gray(' You can check status at the project URL.\n'));
210
344
  } else {
211
345
  waitSpinner.succeed('Project ready!');
212
346
  }
@@ -215,17 +349,20 @@ export async function createSupabaseProject(
215
349
  console.log(chalk.gray(` ${projectUrl}\n`));
216
350
 
217
351
  // Get the anon key
218
- const keysResponse = await fetch(`https://api.supabase.com/v1/projects/${project.id}/api-keys`, {
219
- headers: {
220
- 'Authorization': `Bearer ${accessToken}`,
221
- },
222
- });
223
-
224
352
  let anonKey = '';
225
- if (keysResponse.ok) {
226
- const keys = await keysResponse.json();
227
- const anonKeyObj = keys.find((k: { name: string }) => k.name === 'anon');
228
- anonKey = anonKeyObj?.api_key || '';
353
+ try {
354
+ const keysResponse = await fetch(`https://api.supabase.com/v1/projects/${project.id}/api-keys`, {
355
+ headers: { 'Authorization': `Bearer ${accessToken}` },
356
+ });
357
+
358
+ if (keysResponse.ok) {
359
+ const keys = await keysResponse.json();
360
+ const anonKeyObj = keys.find((k: { name: string }) => k.name === 'anon');
361
+ anonKey = anonKeyObj?.api_key || '';
362
+ }
363
+ } catch {
364
+ console.log(chalk.yellow(' Note: Could not fetch anon key automatically.'));
365
+ console.log(chalk.gray(' Get it from: Settings → API in your Supabase dashboard.\n'));
229
366
  }
230
367
 
231
368
  return {
@@ -233,22 +370,34 @@ export async function createSupabaseProject(
233
370
  projectUrl,
234
371
  apiUrl: projectDetails.api_url || `https://${project.id}.supabase.co`,
235
372
  anonKey,
373
+ dbPassword,
236
374
  };
237
375
  } catch (error) {
238
376
  spinner.fail('Failed to create Supabase project');
239
377
  const message = error instanceof Error ? error.message : String(error);
240
- console.log(chalk.red(` Error: ${message}\n`));
378
+
379
+ if (message.includes('limit')) {
380
+ console.log(chalk.red(` Error: ${message}`));
381
+ console.log(chalk.gray(' Free tier allows 2 projects. Delete old ones or upgrade.\n'));
382
+ } else {
383
+ console.log(chalk.red(` Error: ${message}`));
384
+ console.log(chalk.gray(' Check your access token and try again.\n'));
385
+ }
386
+
241
387
  return null;
242
388
  }
243
389
  }
244
390
 
245
391
  /**
246
- * Create a Vercel project
392
+ * Create a Vercel project with environment variables
247
393
  */
248
394
  export async function createVercelProject(
249
395
  projectName: string,
250
- gitRepoUrl?: string
251
- ): Promise<{ projectId: string; projectUrl: string } | null> {
396
+ options?: {
397
+ gitRepoUrl?: string;
398
+ envVars?: Record<string, string>;
399
+ }
400
+ ): Promise<{ projectId: string; projectUrl: string; envVarsSet: boolean } | null> {
252
401
  const accessToken = await ensureServiceKey('vercel',
253
402
  ' Get your token from: https://vercel.com/account/tokens'
254
403
  );
@@ -259,39 +408,93 @@ export async function createVercelProject(
259
408
 
260
409
  try {
261
410
  // Create the project
262
- const createResponse = await fetch('https://api.vercel.com/v10/projects', {
263
- method: 'POST',
264
- headers: {
265
- 'Authorization': `Bearer ${accessToken}`,
266
- 'Content-Type': 'application/json',
267
- },
268
- body: JSON.stringify({
269
- name: projectName,
270
- framework: 'nextjs',
271
- ...(gitRepoUrl && {
272
- gitRepository: {
273
- type: 'github',
274
- repo: gitRepoUrl.replace('https://github.com/', ''),
275
- },
276
- }),
277
- }),
278
- });
411
+ const projectBody: Record<string, unknown> = {
412
+ name: projectName,
413
+ framework: 'nextjs',
414
+ };
279
415
 
280
- if (!createResponse.ok) {
281
- const error = await createResponse.json();
282
- throw new Error(error.error?.message || 'Failed to create project');
416
+ // Link to GitHub repo if provided
417
+ if (options?.gitRepoUrl) {
418
+ projectBody.gitRepository = {
419
+ type: 'github',
420
+ repo: options.gitRepoUrl.replace('https://github.com/', ''),
421
+ };
283
422
  }
284
423
 
285
- const project = await createResponse.json();
424
+ const project = await withRetry(
425
+ () => fetch('https://api.vercel.com/v10/projects', {
426
+ method: 'POST',
427
+ headers: {
428
+ 'Authorization': `Bearer ${accessToken}`,
429
+ 'Content-Type': 'application/json',
430
+ },
431
+ body: JSON.stringify(projectBody),
432
+ }).then(async r => {
433
+ if (!r.ok) {
434
+ const error = await r.json().catch(() => ({}));
435
+ throw new Error(error.error?.message || `Failed to create project (${r.status})`);
436
+ }
437
+ return r.json();
438
+ }),
439
+ {
440
+ onRetry: (attempt) => {
441
+ spinner.text = `Creating Vercel project (retry ${attempt})...`;
442
+ },
443
+ }
444
+ );
286
445
 
287
446
  spinner.succeed('Vercel project created!');
288
447
 
289
448
  const projectUrl = `https://vercel.com/${project.accountId}/${project.name}`;
290
449
  console.log(chalk.gray(` ${projectUrl}\n`));
291
450
 
451
+ // Set environment variables if provided
452
+ let envVarsSet = false;
453
+ if (options?.envVars && Object.keys(options.envVars).length > 0) {
454
+ const envSpinner = ora('Setting environment variables...').start();
455
+
456
+ try {
457
+ // Vercel expects env vars in a specific format
458
+ const envVarPromises = Object.entries(options.envVars).map(([key, value]) =>
459
+ fetch(`https://api.vercel.com/v10/projects/${project.id}/env`, {
460
+ method: 'POST',
461
+ headers: {
462
+ 'Authorization': `Bearer ${accessToken}`,
463
+ 'Content-Type': 'application/json',
464
+ },
465
+ body: JSON.stringify({
466
+ key,
467
+ value,
468
+ type: key.startsWith('NEXT_PUBLIC_') ? 'plain' : 'encrypted',
469
+ target: ['production', 'preview', 'development'],
470
+ }),
471
+ }).then(r => {
472
+ if (!r.ok) {
473
+ console.log(chalk.yellow(` Warning: Could not set ${key}`));
474
+ }
475
+ return r.ok;
476
+ })
477
+ );
478
+
479
+ const results = await Promise.all(envVarPromises);
480
+ const successCount = results.filter(Boolean).length;
481
+
482
+ if (successCount === Object.keys(options.envVars).length) {
483
+ envSpinner.succeed(`Set ${successCount} environment variables!`);
484
+ envVarsSet = true;
485
+ } else {
486
+ envSpinner.warn(`Set ${successCount}/${Object.keys(options.envVars).length} environment variables`);
487
+ }
488
+ } catch (error) {
489
+ envSpinner.warn('Could not set some environment variables');
490
+ console.log(chalk.gray(' Set them manually in the Vercel dashboard.\n'));
491
+ }
492
+ }
493
+
292
494
  return {
293
495
  projectId: project.id,
294
496
  projectUrl,
497
+ envVarsSet,
295
498
  };
296
499
  } catch (error) {
297
500
  spinner.fail('Failed to create Vercel project');
@@ -299,9 +502,12 @@ export async function createVercelProject(
299
502
 
300
503
  if (message.includes('already exists')) {
301
504
  console.log(chalk.yellow(' Project already exists. Skipping.\n'));
505
+ console.log(chalk.gray(' You can link to existing project in Vercel dashboard.\n'));
302
506
  } else {
303
- console.log(chalk.red(` Error: ${message}\n`));
507
+ console.log(chalk.red(` Error: ${message}`));
508
+ console.log(chalk.gray(' Check your token and try again.\n'));
304
509
  }
510
+
305
511
  return null;
306
512
  }
307
513
  }
@@ -333,9 +539,24 @@ export async function provisionAll(
333
539
  result.supabase = supabase;
334
540
  }
335
541
 
336
- // 3. Vercel
542
+ // 3. Vercel (with environment variables from Supabase)
337
543
  console.log(chalk.white(' Step 3: Vercel Project\n'));
338
- const vercel = await createVercelProject(projectName, result.github?.repoUrl);
544
+
545
+ // Prepare env vars for Vercel
546
+ const vercelEnvVars: Record<string, string> = {};
547
+
548
+ if (result.supabase) {
549
+ vercelEnvVars['NEXT_PUBLIC_SUPABASE_URL'] = result.supabase.apiUrl;
550
+ if (result.supabase.anonKey) {
551
+ vercelEnvVars['NEXT_PUBLIC_SUPABASE_ANON_KEY'] = result.supabase.anonKey;
552
+ }
553
+ }
554
+
555
+ const vercel = await createVercelProject(projectName, {
556
+ gitRepoUrl: result.github?.repoUrl,
557
+ envVars: Object.keys(vercelEnvVars).length > 0 ? vercelEnvVars : undefined,
558
+ });
559
+
339
560
  if (vercel) {
340
561
  result.vercel = vercel;
341
562
  }
@@ -358,7 +579,8 @@ export async function provisionAll(
358
579
  }
359
580
 
360
581
  if (result.vercel) {
361
- console.log(chalk.green(' Vercel: ') + chalk.gray(result.vercel.projectUrl));
582
+ const envStatus = result.vercel.envVarsSet ? ' (env vars set!)' : '';
583
+ console.log(chalk.green(' ✅ Vercel: ') + chalk.gray(result.vercel.projectUrl) + chalk.green(envStatus));
362
584
  } else {
363
585
  console.log(chalk.yellow(' ⏭️ Vercel: Skipped'));
364
586
  }