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