@codebakers/cli 1.4.6 → 1.6.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/provision.d.ts +51 -0
- package/dist/commands/provision.js +309 -0
- package/dist/commands/scaffold.js +132 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +26 -0
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/src/commands/provision.ts +380 -0
- package/src/commands/scaffold.ts +144 -1
- package/src/config.ts +36 -0
- package/src/index.ts +1 -1
- package/src/mcp/server.ts +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ProvisionResult {
|
|
2
|
+
github?: {
|
|
3
|
+
repoUrl: string;
|
|
4
|
+
cloneUrl: string;
|
|
5
|
+
};
|
|
6
|
+
supabase?: {
|
|
7
|
+
projectId: string;
|
|
8
|
+
projectUrl: string;
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
anonKey: string;
|
|
11
|
+
};
|
|
12
|
+
vercel?: {
|
|
13
|
+
projectId: string;
|
|
14
|
+
projectUrl: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a GitHub repository
|
|
19
|
+
*/
|
|
20
|
+
export declare function createGitHubRepo(projectName: string, description?: string): Promise<{
|
|
21
|
+
repoUrl: string;
|
|
22
|
+
cloneUrl: string;
|
|
23
|
+
} | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Create a Supabase project
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSupabaseProject(projectName: string, organizationId?: string): Promise<{
|
|
28
|
+
projectId: string;
|
|
29
|
+
projectUrl: string;
|
|
30
|
+
apiUrl: string;
|
|
31
|
+
anonKey: string;
|
|
32
|
+
} | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Create a Vercel project
|
|
35
|
+
*/
|
|
36
|
+
export declare function createVercelProject(projectName: string, gitRepoUrl?: string): Promise<{
|
|
37
|
+
projectId: string;
|
|
38
|
+
projectUrl: string;
|
|
39
|
+
} | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Full provisioning flow - create all services
|
|
42
|
+
*/
|
|
43
|
+
export declare function provisionAll(projectName: string, description?: string): Promise<ProvisionResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Check which services have keys configured
|
|
46
|
+
*/
|
|
47
|
+
export declare function getConfiguredServices(): {
|
|
48
|
+
github: boolean;
|
|
49
|
+
supabase: boolean;
|
|
50
|
+
vercel: boolean;
|
|
51
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createGitHubRepo = createGitHubRepo;
|
|
7
|
+
exports.createSupabaseProject = createSupabaseProject;
|
|
8
|
+
exports.createVercelProject = createVercelProject;
|
|
9
|
+
exports.provisionAll = provisionAll;
|
|
10
|
+
exports.getConfiguredServices = getConfiguredServices;
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const ora_1 = __importDefault(require("ora"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const readline_1 = require("readline");
|
|
15
|
+
const config_js_1 = require("../config.js");
|
|
16
|
+
function prompt(question) {
|
|
17
|
+
const rl = (0, readline_1.createInterface)({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
});
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.trim());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a service key is configured, prompt if not
|
|
30
|
+
*/
|
|
31
|
+
async function ensureServiceKey(service, instructions) {
|
|
32
|
+
let key = (0, config_js_1.getServiceKey)(service);
|
|
33
|
+
if (!key) {
|
|
34
|
+
console.log(chalk_1.default.yellow(`\n No ${service} API key configured.\n`));
|
|
35
|
+
console.log(chalk_1.default.gray(instructions));
|
|
36
|
+
const inputKey = await prompt(`\n ${service} API key (or press Enter to skip): `);
|
|
37
|
+
if (inputKey) {
|
|
38
|
+
(0, config_js_1.setServiceKey)(service, inputKey);
|
|
39
|
+
key = inputKey;
|
|
40
|
+
console.log(chalk_1.default.green(` ✓ ${service} key saved\n`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(chalk_1.default.gray(` Skipping ${service} provisioning.\n`));
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a GitHub repository
|
|
51
|
+
*/
|
|
52
|
+
async function createGitHubRepo(projectName, description = '') {
|
|
53
|
+
// Check if gh CLI is available
|
|
54
|
+
try {
|
|
55
|
+
(0, child_process_1.execSync)('gh --version', { stdio: 'pipe' });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
console.log(chalk_1.default.yellow(' GitHub CLI (gh) not found. Install it from: https://cli.github.com\n'));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Check if authenticated
|
|
62
|
+
try {
|
|
63
|
+
(0, child_process_1.execSync)('gh auth status', { stdio: 'pipe' });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.log(chalk_1.default.yellow(' Not logged into GitHub CLI.\n'));
|
|
67
|
+
console.log(chalk_1.default.gray(' Run: gh auth login\n'));
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const spinner = (0, ora_1.default)('Creating GitHub repository...').start();
|
|
71
|
+
try {
|
|
72
|
+
// 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' });
|
|
74
|
+
// Get the repo URL
|
|
75
|
+
const repoUrl = (0, child_process_1.execSync)('gh repo view --json url -q .url', { encoding: 'utf-8' }).trim();
|
|
76
|
+
const cloneUrl = `${repoUrl}.git`;
|
|
77
|
+
spinner.succeed('GitHub repository created!');
|
|
78
|
+
console.log(chalk_1.default.gray(` ${repoUrl}\n`));
|
|
79
|
+
return { repoUrl, cloneUrl };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
spinner.fail('Failed to create GitHub repository');
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
if (message.includes('already exists')) {
|
|
85
|
+
console.log(chalk_1.default.yellow(' Repository already exists. Skipping.\n'));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(chalk_1.default.red(` Error: ${message}\n`));
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a Supabase project
|
|
95
|
+
*/
|
|
96
|
+
async function createSupabaseProject(projectName, organizationId) {
|
|
97
|
+
const accessToken = await ensureServiceKey('supabase', ' Get your access token from: https://supabase.com/dashboard/account/tokens');
|
|
98
|
+
if (!accessToken)
|
|
99
|
+
return null;
|
|
100
|
+
const spinner = (0, ora_1.default)('Creating Supabase project...').start();
|
|
101
|
+
try {
|
|
102
|
+
// Get organization if not provided
|
|
103
|
+
if (!organizationId) {
|
|
104
|
+
const orgsResponse = await fetch('https://api.supabase.com/v1/organizations', {
|
|
105
|
+
headers: {
|
|
106
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
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.');
|
|
115
|
+
}
|
|
116
|
+
organizationId = orgs[0].id;
|
|
117
|
+
}
|
|
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';
|
|
122
|
+
// Create the project
|
|
123
|
+
const createResponse = await fetch('https://api.supabase.com/v1/projects', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
name: projectName,
|
|
131
|
+
organization_id: organizationId,
|
|
132
|
+
region: 'us-east-1',
|
|
133
|
+
plan: 'free',
|
|
134
|
+
db_pass: dbPassword,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
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
|
+
spinner.succeed('Supabase project created!');
|
|
143
|
+
// Wait for project to be ready (it takes a moment)
|
|
144
|
+
const waitSpinner = (0, ora_1.default)('Waiting for project to be ready...').start();
|
|
145
|
+
let projectReady = false;
|
|
146
|
+
let attempts = 0;
|
|
147
|
+
let projectDetails = {};
|
|
148
|
+
while (!projectReady && attempts < 30) {
|
|
149
|
+
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;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
attempts++;
|
|
162
|
+
}
|
|
163
|
+
if (!projectReady) {
|
|
164
|
+
waitSpinner.warn('Project created but may not be fully ready yet.');
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
waitSpinner.succeed('Project ready!');
|
|
168
|
+
}
|
|
169
|
+
const projectUrl = `https://supabase.com/dashboard/project/${project.id}`;
|
|
170
|
+
console.log(chalk_1.default.gray(` ${projectUrl}\n`));
|
|
171
|
+
// 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
|
+
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 || '';
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
projectId: project.id,
|
|
185
|
+
projectUrl,
|
|
186
|
+
apiUrl: projectDetails.api_url || `https://${project.id}.supabase.co`,
|
|
187
|
+
anonKey,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
spinner.fail('Failed to create Supabase project');
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
console.log(chalk_1.default.red(` Error: ${message}\n`));
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create a Vercel project
|
|
199
|
+
*/
|
|
200
|
+
async function createVercelProject(projectName, gitRepoUrl) {
|
|
201
|
+
const accessToken = await ensureServiceKey('vercel', ' Get your token from: https://vercel.com/account/tokens');
|
|
202
|
+
if (!accessToken)
|
|
203
|
+
return null;
|
|
204
|
+
const spinner = (0, ora_1.default)('Creating Vercel project...').start();
|
|
205
|
+
try {
|
|
206
|
+
// Create the project
|
|
207
|
+
const createResponse = await fetch('https://api.vercel.com/v10/projects', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
211
|
+
'Content-Type': 'application/json',
|
|
212
|
+
},
|
|
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
|
+
}),
|
|
223
|
+
});
|
|
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
|
+
spinner.succeed('Vercel project created!');
|
|
230
|
+
const projectUrl = `https://vercel.com/${project.accountId}/${project.name}`;
|
|
231
|
+
console.log(chalk_1.default.gray(` ${projectUrl}\n`));
|
|
232
|
+
return {
|
|
233
|
+
projectId: project.id,
|
|
234
|
+
projectUrl,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
spinner.fail('Failed to create Vercel project');
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
if (message.includes('already exists')) {
|
|
241
|
+
console.log(chalk_1.default.yellow(' Project already exists. Skipping.\n'));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(chalk_1.default.red(` Error: ${message}\n`));
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Full provisioning flow - create all services
|
|
251
|
+
*/
|
|
252
|
+
async function provisionAll(projectName, description = '') {
|
|
253
|
+
console.log(chalk_1.default.blue('\n ══════════════════════════════════════════════════════════'));
|
|
254
|
+
console.log(chalk_1.default.white.bold(' 🚀 Auto-Provisioning Services\n'));
|
|
255
|
+
console.log(chalk_1.default.blue(' ══════════════════════════════════════════════════════════\n'));
|
|
256
|
+
const result = {};
|
|
257
|
+
// 1. GitHub
|
|
258
|
+
console.log(chalk_1.default.white(' Step 1: GitHub Repository\n'));
|
|
259
|
+
const github = await createGitHubRepo(projectName, description);
|
|
260
|
+
if (github) {
|
|
261
|
+
result.github = github;
|
|
262
|
+
}
|
|
263
|
+
// 2. Supabase
|
|
264
|
+
console.log(chalk_1.default.white(' Step 2: Supabase Project\n'));
|
|
265
|
+
const supabase = await createSupabaseProject(projectName);
|
|
266
|
+
if (supabase) {
|
|
267
|
+
result.supabase = supabase;
|
|
268
|
+
}
|
|
269
|
+
// 3. Vercel
|
|
270
|
+
console.log(chalk_1.default.white(' Step 3: Vercel Project\n'));
|
|
271
|
+
const vercel = await createVercelProject(projectName, result.github?.repoUrl);
|
|
272
|
+
if (vercel) {
|
|
273
|
+
result.vercel = vercel;
|
|
274
|
+
}
|
|
275
|
+
// Summary
|
|
276
|
+
console.log(chalk_1.default.blue(' ══════════════════════════════════════════════════════════'));
|
|
277
|
+
console.log(chalk_1.default.white.bold(' 📋 Provisioning Summary\n'));
|
|
278
|
+
console.log(chalk_1.default.blue(' ══════════════════════════════════════════════════════════\n'));
|
|
279
|
+
if (result.github) {
|
|
280
|
+
console.log(chalk_1.default.green(' ✅ GitHub: ') + chalk_1.default.gray(result.github.repoUrl));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log(chalk_1.default.yellow(' ⏭️ GitHub: Skipped'));
|
|
284
|
+
}
|
|
285
|
+
if (result.supabase) {
|
|
286
|
+
console.log(chalk_1.default.green(' ✅ Supabase: ') + chalk_1.default.gray(result.supabase.projectUrl));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log(chalk_1.default.yellow(' ⏭️ Supabase: Skipped'));
|
|
290
|
+
}
|
|
291
|
+
if (result.vercel) {
|
|
292
|
+
console.log(chalk_1.default.green(' ✅ Vercel: ') + chalk_1.default.gray(result.vercel.projectUrl));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.log(chalk_1.default.yellow(' ⏭️ Vercel: Skipped'));
|
|
296
|
+
}
|
|
297
|
+
console.log('');
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check which services have keys configured
|
|
302
|
+
*/
|
|
303
|
+
function getConfiguredServices() {
|
|
304
|
+
return {
|
|
305
|
+
github: false, // GitHub uses gh CLI auth, not stored key
|
|
306
|
+
supabase: !!(0, config_js_1.getServiceKey)('supabase'),
|
|
307
|
+
vercel: !!(0, config_js_1.getServiceKey)('vercel'),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -45,6 +45,41 @@ const path_1 = require("path");
|
|
|
45
45
|
const child_process_1 = require("child_process");
|
|
46
46
|
const templates = __importStar(require("../templates/nextjs-supabase.js"));
|
|
47
47
|
const config_js_1 = require("../config.js");
|
|
48
|
+
const provision_js_1 = require("./provision.js");
|
|
49
|
+
/**
|
|
50
|
+
* Fetch service keys from CodeBakers server
|
|
51
|
+
*/
|
|
52
|
+
async function fetchServerKeys() {
|
|
53
|
+
const apiKey = (0, config_js_1.getApiKey)();
|
|
54
|
+
if (!apiKey)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const apiUrl = (0, config_js_1.getApiUrl)();
|
|
58
|
+
const response = await fetch(`${apiUrl}/api/cli/service-keys`, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${apiKey}`,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
return await response.json();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Server unreachable or error
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sync server keys to local storage
|
|
74
|
+
*/
|
|
75
|
+
function syncKeysToLocal(keys) {
|
|
76
|
+
if (keys.github)
|
|
77
|
+
(0, config_js_1.setServiceKey)('github', keys.github);
|
|
78
|
+
if (keys.supabase)
|
|
79
|
+
(0, config_js_1.setServiceKey)('supabase', keys.supabase);
|
|
80
|
+
if (keys.vercel)
|
|
81
|
+
(0, config_js_1.setServiceKey)('vercel', keys.vercel);
|
|
82
|
+
}
|
|
48
83
|
// Cursor IDE configuration templates
|
|
49
84
|
const CURSORRULES_TEMPLATE = `# CODEBAKERS CURSOR RULES
|
|
50
85
|
# Zero-friction AI assistance - everything is automatic
|
|
@@ -507,6 +542,103 @@ async function scaffold() {
|
|
|
507
542
|
else {
|
|
508
543
|
console.log(chalk_1.default.yellow(' ⚠️ Not logged in - run `codebakers setup` first to get patterns\n'));
|
|
509
544
|
}
|
|
545
|
+
// Ask about auto-provisioning
|
|
546
|
+
console.log(chalk_1.default.white('\n Would you like to auto-provision your infrastructure?\n'));
|
|
547
|
+
console.log(chalk_1.default.gray(' This can create GitHub repo, Supabase database, and Vercel project automatically.'));
|
|
548
|
+
console.log(chalk_1.default.gray(' You\'ll need API keys for each service (one-time setup).\n'));
|
|
549
|
+
const wantProvision = await confirm(' Auto-provision services?');
|
|
550
|
+
let provisionResult = {};
|
|
551
|
+
if (wantProvision) {
|
|
552
|
+
// Check for saved keys in CodeBakers back office
|
|
553
|
+
const serverKeys = await fetchServerKeys();
|
|
554
|
+
const hasServerKeys = serverKeys && (serverKeys.github || serverKeys.supabase || serverKeys.vercel);
|
|
555
|
+
const localGithub = (0, config_js_1.getServiceKey)('github');
|
|
556
|
+
const localSupabase = (0, config_js_1.getServiceKey)('supabase');
|
|
557
|
+
const localVercel = (0, config_js_1.getServiceKey)('vercel');
|
|
558
|
+
const hasLocalKeys = localGithub || localSupabase || localVercel;
|
|
559
|
+
if (hasServerKeys || hasLocalKeys) {
|
|
560
|
+
// Show which keys are available
|
|
561
|
+
console.log(chalk_1.default.white('\n Available service keys:\n'));
|
|
562
|
+
if (hasServerKeys) {
|
|
563
|
+
console.log(chalk_1.default.gray(' From CodeBakers account:'));
|
|
564
|
+
if (serverKeys?.github)
|
|
565
|
+
console.log(chalk_1.default.green(' ✓ GitHub'));
|
|
566
|
+
if (serverKeys?.supabase)
|
|
567
|
+
console.log(chalk_1.default.green(' ✓ Supabase'));
|
|
568
|
+
if (serverKeys?.vercel)
|
|
569
|
+
console.log(chalk_1.default.green(' ✓ Vercel'));
|
|
570
|
+
}
|
|
571
|
+
if (hasLocalKeys) {
|
|
572
|
+
console.log(chalk_1.default.gray(' Stored locally:'));
|
|
573
|
+
if (localGithub)
|
|
574
|
+
console.log(chalk_1.default.green(' ✓ GitHub'));
|
|
575
|
+
if (localSupabase)
|
|
576
|
+
console.log(chalk_1.default.green(' ✓ Supabase'));
|
|
577
|
+
if (localVercel)
|
|
578
|
+
console.log(chalk_1.default.green(' ✓ Vercel'));
|
|
579
|
+
}
|
|
580
|
+
console.log('');
|
|
581
|
+
// Ask which keys to use
|
|
582
|
+
console.log(chalk_1.default.white(' Which keys would you like to use?\n'));
|
|
583
|
+
console.log(chalk_1.default.gray(' 1. ') + chalk_1.default.cyan('Use saved keys') + chalk_1.default.gray(' - Use keys from your account/local storage'));
|
|
584
|
+
console.log(chalk_1.default.gray(' 2. ') + chalk_1.default.cyan('Enter new keys') + chalk_1.default.gray(' - For a client project or different account'));
|
|
585
|
+
console.log(chalk_1.default.gray(' 3. ') + chalk_1.default.cyan('Skip') + chalk_1.default.gray(' - Don\'t provision, I\'ll do it manually\n'));
|
|
586
|
+
let keyChoice = '';
|
|
587
|
+
while (!['1', '2', '3'].includes(keyChoice)) {
|
|
588
|
+
keyChoice = await prompt(' Enter 1, 2, or 3: ');
|
|
589
|
+
}
|
|
590
|
+
if (keyChoice === '3') {
|
|
591
|
+
console.log(chalk_1.default.gray('\n Skipping auto-provisioning.\n'));
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
if (keyChoice === '1' && hasServerKeys) {
|
|
595
|
+
// Sync server keys to local storage for this session
|
|
596
|
+
syncKeysToLocal(serverKeys);
|
|
597
|
+
console.log(chalk_1.default.green('\n ✓ Using saved keys from CodeBakers account\n'));
|
|
598
|
+
}
|
|
599
|
+
else if (keyChoice === '2') {
|
|
600
|
+
// Clear local keys so provision.ts will prompt for new ones
|
|
601
|
+
console.log(chalk_1.default.gray('\n You\'ll be prompted to enter keys for each service.\n'));
|
|
602
|
+
}
|
|
603
|
+
// Initialize git first if not already
|
|
604
|
+
try {
|
|
605
|
+
(0, child_process_1.execSync)('git init', { cwd, stdio: 'pipe' });
|
|
606
|
+
(0, child_process_1.execSync)('git add .', { cwd, stdio: 'pipe' });
|
|
607
|
+
(0, child_process_1.execSync)('git commit -m "Initial commit from CodeBakers scaffold"', { cwd, stdio: 'pipe' });
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// Git might already be initialized or have issues
|
|
611
|
+
}
|
|
612
|
+
provisionResult = await (0, provision_js_1.provisionAll)(projectName, `${projectName} - Built with CodeBakers`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
// No saved keys - proceed with provisioning (will prompt for keys)
|
|
617
|
+
console.log(chalk_1.default.gray('\n No saved keys found. You\'ll be prompted to enter keys for each service.\n'));
|
|
618
|
+
console.log(chalk_1.default.gray(' Tip: Save keys in your CodeBakers dashboard to auto-provision future projects!\n'));
|
|
619
|
+
// Initialize git first if not already
|
|
620
|
+
try {
|
|
621
|
+
(0, child_process_1.execSync)('git init', { cwd, stdio: 'pipe' });
|
|
622
|
+
(0, child_process_1.execSync)('git add .', { cwd, stdio: 'pipe' });
|
|
623
|
+
(0, child_process_1.execSync)('git commit -m "Initial commit from CodeBakers scaffold"', { cwd, stdio: 'pipe' });
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
// Git might already be initialized or have issues
|
|
627
|
+
}
|
|
628
|
+
provisionResult = await (0, provision_js_1.provisionAll)(projectName, `${projectName} - Built with CodeBakers`);
|
|
629
|
+
}
|
|
630
|
+
// Update .env.local with Supabase credentials if available
|
|
631
|
+
if (provisionResult.supabase) {
|
|
632
|
+
const envPath = (0, path_1.join)(cwd, '.env.local');
|
|
633
|
+
let envContent = (0, fs_1.existsSync)(envPath) ? (0, fs_1.readFileSync)(envPath, 'utf-8') : '';
|
|
634
|
+
// Replace placeholder values with actual credentials
|
|
635
|
+
envContent = envContent
|
|
636
|
+
.replace('your-project-id.supabase.co', provisionResult.supabase.apiUrl.replace('https://', ''))
|
|
637
|
+
.replace('your-anon-key', provisionResult.supabase.anonKey || 'your-anon-key');
|
|
638
|
+
(0, fs_1.writeFileSync)(envPath, envContent);
|
|
639
|
+
console.log(chalk_1.default.green(' ✅ Updated .env.local with Supabase credentials!\n'));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
510
642
|
// Success message
|
|
511
643
|
console.log(chalk_1.default.green(`
|
|
512
644
|
╔═══════════════════════════════════════════════════════════╗
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced';
|
|
2
|
+
interface ServiceKeys {
|
|
3
|
+
github: string | null;
|
|
4
|
+
supabase: string | null;
|
|
5
|
+
vercel: string | null;
|
|
6
|
+
}
|
|
2
7
|
export declare function getApiKey(): string | null;
|
|
3
8
|
export declare function setApiKey(key: string): void;
|
|
4
9
|
export declare function clearApiKey(): void;
|
|
@@ -6,3 +11,9 @@ export declare function getApiUrl(): string;
|
|
|
6
11
|
export declare function setApiUrl(url: string): void;
|
|
7
12
|
export declare function getExperienceLevel(): ExperienceLevel;
|
|
8
13
|
export declare function setExperienceLevel(level: ExperienceLevel): void;
|
|
14
|
+
export type ServiceName = 'github' | 'supabase' | 'vercel';
|
|
15
|
+
export declare function getServiceKey(service: ServiceName): string | null;
|
|
16
|
+
export declare function setServiceKey(service: ServiceName, key: string): void;
|
|
17
|
+
export declare function clearServiceKey(service: ServiceName): void;
|
|
18
|
+
export declare function getAllServiceKeys(): ServiceKeys;
|
|
19
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,10 @@ exports.getApiUrl = getApiUrl;
|
|
|
10
10
|
exports.setApiUrl = setApiUrl;
|
|
11
11
|
exports.getExperienceLevel = getExperienceLevel;
|
|
12
12
|
exports.setExperienceLevel = setExperienceLevel;
|
|
13
|
+
exports.getServiceKey = getServiceKey;
|
|
14
|
+
exports.setServiceKey = setServiceKey;
|
|
15
|
+
exports.clearServiceKey = clearServiceKey;
|
|
16
|
+
exports.getAllServiceKeys = getAllServiceKeys;
|
|
13
17
|
const conf_1 = __importDefault(require("conf"));
|
|
14
18
|
const config = new conf_1.default({
|
|
15
19
|
projectName: 'codebakers',
|
|
@@ -17,6 +21,11 @@ const config = new conf_1.default({
|
|
|
17
21
|
apiKey: null,
|
|
18
22
|
apiUrl: 'https://codebakers.ai',
|
|
19
23
|
experienceLevel: 'intermediate',
|
|
24
|
+
serviceKeys: {
|
|
25
|
+
github: null,
|
|
26
|
+
supabase: null,
|
|
27
|
+
vercel: null,
|
|
28
|
+
},
|
|
20
29
|
},
|
|
21
30
|
});
|
|
22
31
|
function getApiKey() {
|
|
@@ -40,3 +49,20 @@ function getExperienceLevel() {
|
|
|
40
49
|
function setExperienceLevel(level) {
|
|
41
50
|
config.set('experienceLevel', level);
|
|
42
51
|
}
|
|
52
|
+
function getServiceKey(service) {
|
|
53
|
+
const keys = config.get('serviceKeys');
|
|
54
|
+
return keys[service];
|
|
55
|
+
}
|
|
56
|
+
function setServiceKey(service, key) {
|
|
57
|
+
const keys = config.get('serviceKeys');
|
|
58
|
+
keys[service] = key;
|
|
59
|
+
config.set('serviceKeys', keys);
|
|
60
|
+
}
|
|
61
|
+
function clearServiceKey(service) {
|
|
62
|
+
const keys = config.get('serviceKeys');
|
|
63
|
+
keys[service] = null;
|
|
64
|
+
config.set('serviceKeys', keys);
|
|
65
|
+
}
|
|
66
|
+
function getAllServiceKeys() {
|
|
67
|
+
return config.get('serviceKeys');
|
|
68
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ const program = new commander_1.Command();
|
|
|
53
53
|
program
|
|
54
54
|
.name('codebakers')
|
|
55
55
|
.description('CodeBakers CLI - Production patterns for AI-assisted development')
|
|
56
|
-
.version('1.
|
|
56
|
+
.version('1.6.0');
|
|
57
57
|
// Primary command - one-time setup
|
|
58
58
|
program
|
|
59
59
|
.command('setup')
|
package/dist/mcp/server.js
CHANGED
|
@@ -1131,7 +1131,7 @@ phase: development
|
|
|
1131
1131
|
## Connection Status
|
|
1132
1132
|
- **MCP Server:** Running
|
|
1133
1133
|
- **API Connected:** Yes
|
|
1134
|
-
- **Version:** 1.
|
|
1134
|
+
- **Version:** 1.5.0
|
|
1135
1135
|
|
|
1136
1136
|
## Current Settings
|
|
1137
1137
|
- **Experience Level:** ${level.charAt(0).toUpperCase() + level.slice(1)}
|
package/package.json
CHANGED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { execSync, exec } from 'child_process';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { getServiceKey, setServiceKey, type ServiceName } from '../config.js';
|
|
6
|
+
|
|
7
|
+
function prompt(question: string): Promise<string> {
|
|
8
|
+
const rl = createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProvisionResult {
|
|
22
|
+
github?: {
|
|
23
|
+
repoUrl: string;
|
|
24
|
+
cloneUrl: string;
|
|
25
|
+
};
|
|
26
|
+
supabase?: {
|
|
27
|
+
projectId: string;
|
|
28
|
+
projectUrl: string;
|
|
29
|
+
apiUrl: string;
|
|
30
|
+
anonKey: string;
|
|
31
|
+
};
|
|
32
|
+
vercel?: {
|
|
33
|
+
projectId: string;
|
|
34
|
+
projectUrl: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a service key is configured, prompt if not
|
|
40
|
+
*/
|
|
41
|
+
async function ensureServiceKey(service: ServiceName, instructions: string): Promise<string | null> {
|
|
42
|
+
let key = getServiceKey(service);
|
|
43
|
+
|
|
44
|
+
if (!key) {
|
|
45
|
+
console.log(chalk.yellow(`\n No ${service} API key configured.\n`));
|
|
46
|
+
console.log(chalk.gray(instructions));
|
|
47
|
+
|
|
48
|
+
const inputKey = await prompt(`\n ${service} API key (or press Enter to skip): `);
|
|
49
|
+
|
|
50
|
+
if (inputKey) {
|
|
51
|
+
setServiceKey(service, inputKey);
|
|
52
|
+
key = inputKey;
|
|
53
|
+
console.log(chalk.green(` ✓ ${service} key saved\n`));
|
|
54
|
+
} else {
|
|
55
|
+
console.log(chalk.gray(` Skipping ${service} provisioning.\n`));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return key;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a GitHub repository
|
|
65
|
+
*/
|
|
66
|
+
export async function createGitHubRepo(
|
|
67
|
+
projectName: string,
|
|
68
|
+
description: string = ''
|
|
69
|
+
): Promise<{ repoUrl: string; cloneUrl: string } | null> {
|
|
70
|
+
// Check if gh CLI is available
|
|
71
|
+
try {
|
|
72
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
73
|
+
} catch {
|
|
74
|
+
console.log(chalk.yellow(' GitHub CLI (gh) not found. Install it from: https://cli.github.com\n'));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if authenticated
|
|
79
|
+
try {
|
|
80
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
81
|
+
} catch {
|
|
82
|
+
console.log(chalk.yellow(' Not logged into GitHub CLI.\n'));
|
|
83
|
+
console.log(chalk.gray(' Run: gh auth login\n'));
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const spinner = ora('Creating GitHub repository...').start();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Create the repo
|
|
91
|
+
const result = execSync(
|
|
92
|
+
`gh repo create ${projectName} --public --description "${description}" --source . --remote origin --push`,
|
|
93
|
+
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Get the repo URL
|
|
97
|
+
const repoUrl = execSync('gh repo view --json url -q .url', { encoding: 'utf-8' }).trim();
|
|
98
|
+
const cloneUrl = `${repoUrl}.git`;
|
|
99
|
+
|
|
100
|
+
spinner.succeed('GitHub repository created!');
|
|
101
|
+
console.log(chalk.gray(` ${repoUrl}\n`));
|
|
102
|
+
|
|
103
|
+
return { repoUrl, cloneUrl };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
spinner.fail('Failed to create GitHub repository');
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
|
|
108
|
+
if (message.includes('already exists')) {
|
|
109
|
+
console.log(chalk.yellow(' Repository already exists. Skipping.\n'));
|
|
110
|
+
} else {
|
|
111
|
+
console.log(chalk.red(` Error: ${message}\n`));
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a Supabase project
|
|
119
|
+
*/
|
|
120
|
+
export async function createSupabaseProject(
|
|
121
|
+
projectName: string,
|
|
122
|
+
organizationId?: string
|
|
123
|
+
): Promise<{ projectId: string; projectUrl: string; apiUrl: string; anonKey: string } | null> {
|
|
124
|
+
const accessToken = await ensureServiceKey('supabase',
|
|
125
|
+
' Get your access token from: https://supabase.com/dashboard/account/tokens'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!accessToken) return null;
|
|
129
|
+
|
|
130
|
+
const spinner = ora('Creating Supabase project...').start();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Get organization if not provided
|
|
134
|
+
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
|
+
}
|
|
144
|
+
|
|
145
|
+
const orgs = await orgsResponse.json();
|
|
146
|
+
if (orgs.length === 0) {
|
|
147
|
+
throw new Error('No organizations found. Create one at supabase.com first.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
organizationId = orgs[0].id;
|
|
151
|
+
}
|
|
152
|
+
|
|
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';
|
|
157
|
+
|
|
158
|
+
// 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,
|
|
171
|
+
}),
|
|
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();
|
|
180
|
+
|
|
181
|
+
spinner.succeed('Supabase project created!');
|
|
182
|
+
|
|
183
|
+
// Wait for project to be ready (it takes a moment)
|
|
184
|
+
const waitSpinner = ora('Waiting for project to be ready...').start();
|
|
185
|
+
|
|
186
|
+
let projectReady = false;
|
|
187
|
+
let attempts = 0;
|
|
188
|
+
let projectDetails: { api_url?: string; anon_key?: string } = {};
|
|
189
|
+
|
|
190
|
+
while (!projectReady && attempts < 30) {
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
192
|
+
|
|
193
|
+
const statusResponse = await fetch(`https://api.supabase.com/v1/projects/${project.id}`, {
|
|
194
|
+
headers: {
|
|
195
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (statusResponse.ok) {
|
|
200
|
+
projectDetails = await statusResponse.json();
|
|
201
|
+
if (projectDetails.api_url) {
|
|
202
|
+
projectReady = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
attempts++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!projectReady) {
|
|
209
|
+
waitSpinner.warn('Project created but may not be fully ready yet.');
|
|
210
|
+
} else {
|
|
211
|
+
waitSpinner.succeed('Project ready!');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const projectUrl = `https://supabase.com/dashboard/project/${project.id}`;
|
|
215
|
+
console.log(chalk.gray(` ${projectUrl}\n`));
|
|
216
|
+
|
|
217
|
+
// 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
|
+
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 || '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
projectId: project.id,
|
|
233
|
+
projectUrl,
|
|
234
|
+
apiUrl: projectDetails.api_url || `https://${project.id}.supabase.co`,
|
|
235
|
+
anonKey,
|
|
236
|
+
};
|
|
237
|
+
} catch (error) {
|
|
238
|
+
spinner.fail('Failed to create Supabase project');
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
console.log(chalk.red(` Error: ${message}\n`));
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a Vercel project
|
|
247
|
+
*/
|
|
248
|
+
export async function createVercelProject(
|
|
249
|
+
projectName: string,
|
|
250
|
+
gitRepoUrl?: string
|
|
251
|
+
): Promise<{ projectId: string; projectUrl: string } | null> {
|
|
252
|
+
const accessToken = await ensureServiceKey('vercel',
|
|
253
|
+
' Get your token from: https://vercel.com/account/tokens'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (!accessToken) return null;
|
|
257
|
+
|
|
258
|
+
const spinner = ora('Creating Vercel project...').start();
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// 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
|
+
});
|
|
279
|
+
|
|
280
|
+
if (!createResponse.ok) {
|
|
281
|
+
const error = await createResponse.json();
|
|
282
|
+
throw new Error(error.error?.message || 'Failed to create project');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const project = await createResponse.json();
|
|
286
|
+
|
|
287
|
+
spinner.succeed('Vercel project created!');
|
|
288
|
+
|
|
289
|
+
const projectUrl = `https://vercel.com/${project.accountId}/${project.name}`;
|
|
290
|
+
console.log(chalk.gray(` ${projectUrl}\n`));
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
projectId: project.id,
|
|
294
|
+
projectUrl,
|
|
295
|
+
};
|
|
296
|
+
} catch (error) {
|
|
297
|
+
spinner.fail('Failed to create Vercel project');
|
|
298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
+
|
|
300
|
+
if (message.includes('already exists')) {
|
|
301
|
+
console.log(chalk.yellow(' Project already exists. Skipping.\n'));
|
|
302
|
+
} else {
|
|
303
|
+
console.log(chalk.red(` Error: ${message}\n`));
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Full provisioning flow - create all services
|
|
311
|
+
*/
|
|
312
|
+
export async function provisionAll(
|
|
313
|
+
projectName: string,
|
|
314
|
+
description: string = ''
|
|
315
|
+
): Promise<ProvisionResult> {
|
|
316
|
+
console.log(chalk.blue('\n ══════════════════════════════════════════════════════════'));
|
|
317
|
+
console.log(chalk.white.bold(' 🚀 Auto-Provisioning Services\n'));
|
|
318
|
+
console.log(chalk.blue(' ══════════════════════════════════════════════════════════\n'));
|
|
319
|
+
|
|
320
|
+
const result: ProvisionResult = {};
|
|
321
|
+
|
|
322
|
+
// 1. GitHub
|
|
323
|
+
console.log(chalk.white(' Step 1: GitHub Repository\n'));
|
|
324
|
+
const github = await createGitHubRepo(projectName, description);
|
|
325
|
+
if (github) {
|
|
326
|
+
result.github = github;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 2. Supabase
|
|
330
|
+
console.log(chalk.white(' Step 2: Supabase Project\n'));
|
|
331
|
+
const supabase = await createSupabaseProject(projectName);
|
|
332
|
+
if (supabase) {
|
|
333
|
+
result.supabase = supabase;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 3. Vercel
|
|
337
|
+
console.log(chalk.white(' Step 3: Vercel Project\n'));
|
|
338
|
+
const vercel = await createVercelProject(projectName, result.github?.repoUrl);
|
|
339
|
+
if (vercel) {
|
|
340
|
+
result.vercel = vercel;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Summary
|
|
344
|
+
console.log(chalk.blue(' ══════════════════════════════════════════════════════════'));
|
|
345
|
+
console.log(chalk.white.bold(' 📋 Provisioning Summary\n'));
|
|
346
|
+
console.log(chalk.blue(' ══════════════════════════════════════════════════════════\n'));
|
|
347
|
+
|
|
348
|
+
if (result.github) {
|
|
349
|
+
console.log(chalk.green(' ✅ GitHub: ') + chalk.gray(result.github.repoUrl));
|
|
350
|
+
} else {
|
|
351
|
+
console.log(chalk.yellow(' ⏭️ GitHub: Skipped'));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (result.supabase) {
|
|
355
|
+
console.log(chalk.green(' ✅ Supabase: ') + chalk.gray(result.supabase.projectUrl));
|
|
356
|
+
} else {
|
|
357
|
+
console.log(chalk.yellow(' ⏭️ Supabase: Skipped'));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (result.vercel) {
|
|
361
|
+
console.log(chalk.green(' ✅ Vercel: ') + chalk.gray(result.vercel.projectUrl));
|
|
362
|
+
} else {
|
|
363
|
+
console.log(chalk.yellow(' ⏭️ Vercel: Skipped'));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log('');
|
|
367
|
+
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check which services have keys configured
|
|
373
|
+
*/
|
|
374
|
+
export function getConfiguredServices(): { github: boolean; supabase: boolean; vercel: boolean } {
|
|
375
|
+
return {
|
|
376
|
+
github: false, // GitHub uses gh CLI auth, not stored key
|
|
377
|
+
supabase: !!getServiceKey('supabase'),
|
|
378
|
+
vercel: !!getServiceKey('vercel'),
|
|
379
|
+
};
|
|
380
|
+
}
|
package/src/commands/scaffold.ts
CHANGED
|
@@ -5,7 +5,47 @@ import { writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync } from
|
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import * as templates from '../templates/nextjs-supabase.js';
|
|
8
|
-
import { getApiKey, getApiUrl } from '../config.js';
|
|
8
|
+
import { getApiKey, getApiUrl, setServiceKey, getServiceKey } from '../config.js';
|
|
9
|
+
import { provisionAll, type ProvisionResult } from './provision.js';
|
|
10
|
+
|
|
11
|
+
interface ServerServiceKeys {
|
|
12
|
+
github: string | null;
|
|
13
|
+
supabase: string | null;
|
|
14
|
+
vercel: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch service keys from CodeBakers server
|
|
19
|
+
*/
|
|
20
|
+
async function fetchServerKeys(): Promise<ServerServiceKeys | null> {
|
|
21
|
+
const apiKey = getApiKey();
|
|
22
|
+
if (!apiKey) return null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const apiUrl = getApiUrl();
|
|
26
|
+
const response = await fetch(`${apiUrl}/api/cli/service-keys`, {
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${apiKey}`,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (response.ok) {
|
|
33
|
+
return await response.json();
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Server unreachable or error
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sync server keys to local storage
|
|
43
|
+
*/
|
|
44
|
+
function syncKeysToLocal(keys: ServerServiceKeys): void {
|
|
45
|
+
if (keys.github) setServiceKey('github', keys.github);
|
|
46
|
+
if (keys.supabase) setServiceKey('supabase', keys.supabase);
|
|
47
|
+
if (keys.vercel) setServiceKey('vercel', keys.vercel);
|
|
48
|
+
}
|
|
9
49
|
|
|
10
50
|
// Cursor IDE configuration templates
|
|
11
51
|
const CURSORRULES_TEMPLATE = `# CODEBAKERS CURSOR RULES
|
|
@@ -522,6 +562,109 @@ export async function scaffold(): Promise<void> {
|
|
|
522
562
|
console.log(chalk.yellow(' ⚠️ Not logged in - run `codebakers setup` first to get patterns\n'));
|
|
523
563
|
}
|
|
524
564
|
|
|
565
|
+
// Ask about auto-provisioning
|
|
566
|
+
console.log(chalk.white('\n Would you like to auto-provision your infrastructure?\n'));
|
|
567
|
+
console.log(chalk.gray(' This can create GitHub repo, Supabase database, and Vercel project automatically.'));
|
|
568
|
+
console.log(chalk.gray(' You\'ll need API keys for each service (one-time setup).\n'));
|
|
569
|
+
|
|
570
|
+
const wantProvision = await confirm(' Auto-provision services?');
|
|
571
|
+
let provisionResult: ProvisionResult = {};
|
|
572
|
+
|
|
573
|
+
if (wantProvision) {
|
|
574
|
+
// Check for saved keys in CodeBakers back office
|
|
575
|
+
const serverKeys = await fetchServerKeys();
|
|
576
|
+
const hasServerKeys = serverKeys && (serverKeys.github || serverKeys.supabase || serverKeys.vercel);
|
|
577
|
+
const localGithub = getServiceKey('github');
|
|
578
|
+
const localSupabase = getServiceKey('supabase');
|
|
579
|
+
const localVercel = getServiceKey('vercel');
|
|
580
|
+
const hasLocalKeys = localGithub || localSupabase || localVercel;
|
|
581
|
+
|
|
582
|
+
if (hasServerKeys || hasLocalKeys) {
|
|
583
|
+
// Show which keys are available
|
|
584
|
+
console.log(chalk.white('\n Available service keys:\n'));
|
|
585
|
+
|
|
586
|
+
if (hasServerKeys) {
|
|
587
|
+
console.log(chalk.gray(' From CodeBakers account:'));
|
|
588
|
+
if (serverKeys?.github) console.log(chalk.green(' ✓ GitHub'));
|
|
589
|
+
if (serverKeys?.supabase) console.log(chalk.green(' ✓ Supabase'));
|
|
590
|
+
if (serverKeys?.vercel) console.log(chalk.green(' ✓ Vercel'));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (hasLocalKeys) {
|
|
594
|
+
console.log(chalk.gray(' Stored locally:'));
|
|
595
|
+
if (localGithub) console.log(chalk.green(' ✓ GitHub'));
|
|
596
|
+
if (localSupabase) console.log(chalk.green(' ✓ Supabase'));
|
|
597
|
+
if (localVercel) console.log(chalk.green(' ✓ Vercel'));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
console.log('');
|
|
601
|
+
|
|
602
|
+
// Ask which keys to use
|
|
603
|
+
console.log(chalk.white(' Which keys would you like to use?\n'));
|
|
604
|
+
console.log(chalk.gray(' 1. ') + chalk.cyan('Use saved keys') + chalk.gray(' - Use keys from your account/local storage'));
|
|
605
|
+
console.log(chalk.gray(' 2. ') + chalk.cyan('Enter new keys') + chalk.gray(' - For a client project or different account'));
|
|
606
|
+
console.log(chalk.gray(' 3. ') + chalk.cyan('Skip') + chalk.gray(' - Don\'t provision, I\'ll do it manually\n'));
|
|
607
|
+
|
|
608
|
+
let keyChoice = '';
|
|
609
|
+
while (!['1', '2', '3'].includes(keyChoice)) {
|
|
610
|
+
keyChoice = await prompt(' Enter 1, 2, or 3: ');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (keyChoice === '3') {
|
|
614
|
+
console.log(chalk.gray('\n Skipping auto-provisioning.\n'));
|
|
615
|
+
} else {
|
|
616
|
+
if (keyChoice === '1' && hasServerKeys) {
|
|
617
|
+
// Sync server keys to local storage for this session
|
|
618
|
+
syncKeysToLocal(serverKeys!);
|
|
619
|
+
console.log(chalk.green('\n ✓ Using saved keys from CodeBakers account\n'));
|
|
620
|
+
} else if (keyChoice === '2') {
|
|
621
|
+
// Clear local keys so provision.ts will prompt for new ones
|
|
622
|
+
console.log(chalk.gray('\n You\'ll be prompted to enter keys for each service.\n'));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Initialize git first if not already
|
|
626
|
+
try {
|
|
627
|
+
execSync('git init', { cwd, stdio: 'pipe' });
|
|
628
|
+
execSync('git add .', { cwd, stdio: 'pipe' });
|
|
629
|
+
execSync('git commit -m "Initial commit from CodeBakers scaffold"', { cwd, stdio: 'pipe' });
|
|
630
|
+
} catch {
|
|
631
|
+
// Git might already be initialized or have issues
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
provisionResult = await provisionAll(projectName, `${projectName} - Built with CodeBakers`);
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
// No saved keys - proceed with provisioning (will prompt for keys)
|
|
638
|
+
console.log(chalk.gray('\n No saved keys found. You\'ll be prompted to enter keys for each service.\n'));
|
|
639
|
+
console.log(chalk.gray(' Tip: Save keys in your CodeBakers dashboard to auto-provision future projects!\n'));
|
|
640
|
+
|
|
641
|
+
// Initialize git first if not already
|
|
642
|
+
try {
|
|
643
|
+
execSync('git init', { cwd, stdio: 'pipe' });
|
|
644
|
+
execSync('git add .', { cwd, stdio: 'pipe' });
|
|
645
|
+
execSync('git commit -m "Initial commit from CodeBakers scaffold"', { cwd, stdio: 'pipe' });
|
|
646
|
+
} catch {
|
|
647
|
+
// Git might already be initialized or have issues
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
provisionResult = await provisionAll(projectName, `${projectName} - Built with CodeBakers`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Update .env.local with Supabase credentials if available
|
|
654
|
+
if (provisionResult.supabase) {
|
|
655
|
+
const envPath = join(cwd, '.env.local');
|
|
656
|
+
let envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
657
|
+
|
|
658
|
+
// Replace placeholder values with actual credentials
|
|
659
|
+
envContent = envContent
|
|
660
|
+
.replace('your-project-id.supabase.co', provisionResult.supabase.apiUrl.replace('https://', ''))
|
|
661
|
+
.replace('your-anon-key', provisionResult.supabase.anonKey || 'your-anon-key');
|
|
662
|
+
|
|
663
|
+
writeFileSync(envPath, envContent);
|
|
664
|
+
console.log(chalk.green(' ✅ Updated .env.local with Supabase credentials!\n'));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
525
668
|
// Success message
|
|
526
669
|
console.log(chalk.green(`
|
|
527
670
|
╔═══════════════════════════════════════════════════════════╗
|
package/src/config.ts
CHANGED
|
@@ -2,10 +2,17 @@ import Conf from 'conf';
|
|
|
2
2
|
|
|
3
3
|
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced';
|
|
4
4
|
|
|
5
|
+
interface ServiceKeys {
|
|
6
|
+
github: string | null;
|
|
7
|
+
supabase: string | null;
|
|
8
|
+
vercel: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
interface ConfigSchema {
|
|
6
12
|
apiKey: string | null;
|
|
7
13
|
apiUrl: string;
|
|
8
14
|
experienceLevel: ExperienceLevel;
|
|
15
|
+
serviceKeys: ServiceKeys;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const config = new Conf<ConfigSchema>({
|
|
@@ -14,6 +21,11 @@ const config = new Conf<ConfigSchema>({
|
|
|
14
21
|
apiKey: null,
|
|
15
22
|
apiUrl: 'https://codebakers.ai',
|
|
16
23
|
experienceLevel: 'intermediate',
|
|
24
|
+
serviceKeys: {
|
|
25
|
+
github: null,
|
|
26
|
+
supabase: null,
|
|
27
|
+
vercel: null,
|
|
28
|
+
},
|
|
17
29
|
},
|
|
18
30
|
});
|
|
19
31
|
|
|
@@ -44,3 +56,27 @@ export function getExperienceLevel(): ExperienceLevel {
|
|
|
44
56
|
export function setExperienceLevel(level: ExperienceLevel): void {
|
|
45
57
|
config.set('experienceLevel', level);
|
|
46
58
|
}
|
|
59
|
+
|
|
60
|
+
// Service API Keys
|
|
61
|
+
export type ServiceName = 'github' | 'supabase' | 'vercel';
|
|
62
|
+
|
|
63
|
+
export function getServiceKey(service: ServiceName): string | null {
|
|
64
|
+
const keys = config.get('serviceKeys');
|
|
65
|
+
return keys[service];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function setServiceKey(service: ServiceName, key: string): void {
|
|
69
|
+
const keys = config.get('serviceKeys');
|
|
70
|
+
keys[service] = key;
|
|
71
|
+
config.set('serviceKeys', keys);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function clearServiceKey(service: ServiceName): void {
|
|
75
|
+
const keys = config.get('serviceKeys');
|
|
76
|
+
keys[service] = null;
|
|
77
|
+
config.set('serviceKeys', keys);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getAllServiceKeys(): ServiceKeys {
|
|
81
|
+
return config.get('serviceKeys');
|
|
82
|
+
}
|
package/src/index.ts
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -1285,7 +1285,7 @@ phase: development
|
|
|
1285
1285
|
## Connection Status
|
|
1286
1286
|
- **MCP Server:** Running
|
|
1287
1287
|
- **API Connected:** Yes
|
|
1288
|
-
- **Version:** 1.
|
|
1288
|
+
- **Version:** 1.5.0
|
|
1289
1289
|
|
|
1290
1290
|
## Current Settings
|
|
1291
1291
|
- **Experience Level:** ${level.charAt(0).toUpperCase() + level.slice(1)}
|