@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.
- package/dist/commands/audit.d.ts +19 -0
- package/dist/commands/audit.js +730 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +176 -0
- package/dist/commands/doctor.js +59 -4
- package/dist/commands/heal.d.ts +41 -0
- package/dist/commands/heal.js +734 -0
- package/dist/commands/login.js +12 -16
- package/dist/commands/provision.d.ts +55 -3
- package/dist/commands/provision.js +243 -74
- package/dist/commands/scaffold.js +221 -41
- package/dist/commands/setup.js +60 -19
- package/dist/commands/upgrade.d.ts +4 -0
- package/dist/commands/upgrade.js +90 -0
- package/dist/config.d.ts +61 -5
- package/dist/config.js +268 -5
- package/dist/index.js +44 -3
- package/dist/lib/api.d.ts +45 -0
- package/dist/lib/api.js +159 -0
- package/dist/mcp/server.js +146 -0
- package/package.json +1 -1
- package/src/commands/audit.ts +827 -0
- package/src/commands/config.ts +216 -0
- package/src/commands/doctor.ts +69 -4
- package/src/commands/heal.ts +889 -0
- package/src/commands/login.ts +14 -18
- package/src/commands/provision.ts +323 -101
- package/src/commands/scaffold.ts +257 -43
- package/src/commands/setup.ts +65 -20
- package/src/commands/upgrade.ts +110 -0
- package/src/config.ts +320 -11
- package/src/index.ts +48 -3
- package/src/lib/api.ts +183 -0
- package/src/mcp/server.ts +160 -0
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
28
|
-
console.log(chalk_1.default.red('\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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
110
|
-
throw new Error('
|
|
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 =
|
|
203
|
+
organizationId = orgsResponse[0].id;
|
|
117
204
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 <
|
|
239
|
+
while (!projectReady && attempts < 60) { // Up to 2 minutes
|
|
149
240
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
'Authorization': `Bearer ${accessToken}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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'));
|