codebakers 1.0.45 → 2.0.1
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/README.md +275 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4260 -0
- package/install.bat +9 -0
- package/package.json +71 -115
- package/src/channels/discord.ts +5 -0
- package/src/channels/slack.ts +5 -0
- package/src/channels/sms.ts +4 -0
- package/src/channels/telegram.ts +5 -0
- package/src/channels/whatsapp.ts +7 -0
- package/src/commands/check.ts +365 -0
- package/src/commands/code.ts +684 -0
- package/src/commands/connect.ts +12 -0
- package/src/commands/deploy.ts +414 -0
- package/src/commands/design.ts +298 -0
- package/src/commands/fix.ts +20 -0
- package/src/commands/gateway.ts +604 -0
- package/src/commands/generate.ts +178 -0
- package/src/commands/init.ts +574 -0
- package/src/commands/learn.ts +36 -0
- package/src/commands/security.ts +102 -0
- package/src/commands/setup.ts +448 -0
- package/src/commands/status.ts +56 -0
- package/src/index.ts +278 -0
- package/src/patterns/loader.ts +337 -0
- package/src/services/github.ts +61 -0
- package/src/services/supabase.ts +147 -0
- package/src/services/vercel.ts +61 -0
- package/src/utils/claude-md.ts +287 -0
- package/src/utils/config.ts +282 -0
- package/src/utils/updates.ts +27 -0
- package/tsconfig.json +17 -10
- package/.vscodeignore +0 -18
- package/LICENSE +0 -21
- package/codebakers-1.0.0.vsix +0 -0
- package/codebakers-1.0.10.vsix +0 -0
- package/codebakers-1.0.11.vsix +0 -0
- package/codebakers-1.0.12.vsix +0 -0
- package/codebakers-1.0.13.vsix +0 -0
- package/codebakers-1.0.14.vsix +0 -0
- package/codebakers-1.0.15.vsix +0 -0
- package/codebakers-1.0.16.vsix +0 -0
- package/codebakers-1.0.17.vsix +0 -0
- package/codebakers-1.0.18.vsix +0 -0
- package/codebakers-1.0.19.vsix +0 -0
- package/codebakers-1.0.20.vsix +0 -0
- package/codebakers-1.0.21.vsix +0 -0
- package/codebakers-1.0.22.vsix +0 -0
- package/codebakers-1.0.23.vsix +0 -0
- package/codebakers-1.0.24.vsix +0 -0
- package/codebakers-1.0.25.vsix +0 -0
- package/codebakers-1.0.26.vsix +0 -0
- package/codebakers-1.0.27.vsix +0 -0
- package/codebakers-1.0.28.vsix +0 -0
- package/codebakers-1.0.29.vsix +0 -0
- package/codebakers-1.0.30.vsix +0 -0
- package/codebakers-1.0.31.vsix +0 -0
- package/codebakers-1.0.32.vsix +0 -0
- package/codebakers-1.0.35.vsix +0 -0
- package/codebakers-1.0.36.vsix +0 -0
- package/codebakers-1.0.37.vsix +0 -0
- package/codebakers-1.0.38.vsix +0 -0
- package/codebakers-1.0.39.vsix +0 -0
- package/codebakers-1.0.40.vsix +0 -0
- package/codebakers-1.0.41.vsix +0 -0
- package/codebakers-1.0.42.vsix +0 -0
- package/codebakers-1.0.43.vsix +0 -0
- package/codebakers-1.0.44.vsix +0 -0
- package/codebakers-1.0.45.vsix +0 -0
- package/dist/extension.js +0 -1394
- package/esbuild.js +0 -63
- package/media/icon.png +0 -0
- package/media/icon.svg +0 -7
- package/nul +0 -1
- package/preview.html +0 -547
- package/src/ChatPanelProvider.ts +0 -1815
- package/src/ChatViewProvider.ts +0 -749
- package/src/CodeBakersClient.ts +0 -1146
- package/src/CodeValidator.ts +0 -645
- package/src/FileOperations.ts +0 -410
- package/src/ProjectContext.ts +0 -526
- package/src/extension.ts +0 -332
package/dist/index.js
ADDED
|
@@ -0,0 +1,4260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p13 from "@clack/prompts";
|
|
6
|
+
import chalk14 from "chalk";
|
|
7
|
+
import boxen from "boxen";
|
|
8
|
+
import gradient from "gradient-string";
|
|
9
|
+
|
|
10
|
+
// src/utils/config.ts
|
|
11
|
+
import Conf from "conf";
|
|
12
|
+
import * as fs from "fs-extra";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import os from "os";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var ConfigSchema = z.object({
|
|
17
|
+
version: z.string().default("1.0.0"),
|
|
18
|
+
credentials: z.object({
|
|
19
|
+
github: z.object({
|
|
20
|
+
token: z.string().optional(),
|
|
21
|
+
username: z.string().optional()
|
|
22
|
+
}).optional(),
|
|
23
|
+
vercel: z.object({
|
|
24
|
+
token: z.string().optional(),
|
|
25
|
+
teamId: z.string().optional()
|
|
26
|
+
}).optional(),
|
|
27
|
+
supabase: z.object({
|
|
28
|
+
accessToken: z.string().optional(),
|
|
29
|
+
orgId: z.string().optional()
|
|
30
|
+
}).optional(),
|
|
31
|
+
anthropic: z.object({
|
|
32
|
+
apiKey: z.string().optional()
|
|
33
|
+
}).optional(),
|
|
34
|
+
openai: z.object({
|
|
35
|
+
apiKey: z.string().optional()
|
|
36
|
+
}).optional(),
|
|
37
|
+
stripe: z.object({
|
|
38
|
+
secretKey: z.string().optional(),
|
|
39
|
+
publishableKey: z.string().optional(),
|
|
40
|
+
webhookSecret: z.string().optional()
|
|
41
|
+
}).optional(),
|
|
42
|
+
twilio: z.object({
|
|
43
|
+
accountSid: z.string().optional(),
|
|
44
|
+
authToken: z.string().optional(),
|
|
45
|
+
phoneNumber: z.string().optional()
|
|
46
|
+
}).optional(),
|
|
47
|
+
vapi: z.object({
|
|
48
|
+
apiKey: z.string().optional()
|
|
49
|
+
}).optional(),
|
|
50
|
+
resend: z.object({
|
|
51
|
+
apiKey: z.string().optional()
|
|
52
|
+
}).optional(),
|
|
53
|
+
elevenLabs: z.object({
|
|
54
|
+
apiKey: z.string().optional()
|
|
55
|
+
}).optional(),
|
|
56
|
+
microsoft: z.object({
|
|
57
|
+
clientId: z.string().optional(),
|
|
58
|
+
clientSecret: z.string().optional(),
|
|
59
|
+
tenantId: z.string().optional()
|
|
60
|
+
}).optional(),
|
|
61
|
+
google: z.object({
|
|
62
|
+
clientId: z.string().optional(),
|
|
63
|
+
clientSecret: z.string().optional()
|
|
64
|
+
}).optional()
|
|
65
|
+
}).default({}),
|
|
66
|
+
preferences: z.object({
|
|
67
|
+
defaultFramework: z.string().optional(),
|
|
68
|
+
defaultUI: z.string().optional(),
|
|
69
|
+
defaultPackages: z.array(z.string()).optional(),
|
|
70
|
+
deployToPreviewFirst: z.boolean().default(true)
|
|
71
|
+
}).default({}),
|
|
72
|
+
learning: z.object({
|
|
73
|
+
enabled: z.boolean().default(true),
|
|
74
|
+
shortcuts: z.record(z.string()).default({}),
|
|
75
|
+
preferences: z.record(z.any()).default({}),
|
|
76
|
+
rejections: z.array(z.string()).default([]),
|
|
77
|
+
workflows: z.record(z.array(z.string())).default({})
|
|
78
|
+
}).default({}),
|
|
79
|
+
projects: z.array(z.object({
|
|
80
|
+
name: z.string(),
|
|
81
|
+
path: z.string(),
|
|
82
|
+
github: z.string().optional(),
|
|
83
|
+
vercel: z.string().optional(),
|
|
84
|
+
supabase: z.string().optional(),
|
|
85
|
+
createdAt: z.string()
|
|
86
|
+
})).default([]),
|
|
87
|
+
channels: z.object({
|
|
88
|
+
whatsapp: z.object({
|
|
89
|
+
enabled: z.boolean().default(false),
|
|
90
|
+
phoneNumber: z.string().optional()
|
|
91
|
+
}).optional(),
|
|
92
|
+
telegram: z.object({
|
|
93
|
+
enabled: z.boolean().default(false),
|
|
94
|
+
botToken: z.string().optional()
|
|
95
|
+
}).optional(),
|
|
96
|
+
discord: z.object({
|
|
97
|
+
enabled: z.boolean().default(false),
|
|
98
|
+
botToken: z.string().optional()
|
|
99
|
+
}).optional(),
|
|
100
|
+
slack: z.object({
|
|
101
|
+
enabled: z.boolean().default(false),
|
|
102
|
+
botToken: z.string().optional()
|
|
103
|
+
}).optional()
|
|
104
|
+
}).default({})
|
|
105
|
+
});
|
|
106
|
+
var Config = class {
|
|
107
|
+
conf;
|
|
108
|
+
configDir;
|
|
109
|
+
constructor() {
|
|
110
|
+
this.configDir = path.join(os.homedir(), ".codebakers");
|
|
111
|
+
fs.ensureDirSync(this.configDir);
|
|
112
|
+
fs.ensureDirSync(path.join(this.configDir, "patterns"));
|
|
113
|
+
fs.ensureDirSync(path.join(this.configDir, "templates"));
|
|
114
|
+
fs.ensureDirSync(path.join(this.configDir, "learning"));
|
|
115
|
+
this.conf = new Conf({
|
|
116
|
+
projectName: "codebakers",
|
|
117
|
+
cwd: this.configDir,
|
|
118
|
+
schema: {
|
|
119
|
+
version: { type: "string", default: "1.0.0" },
|
|
120
|
+
credentials: { type: "object", default: {} },
|
|
121
|
+
preferences: { type: "object", default: {} },
|
|
122
|
+
learning: { type: "object", default: { enabled: true, shortcuts: {}, preferences: {}, rejections: [], workflows: {} } },
|
|
123
|
+
projects: { type: "array", default: [] },
|
|
124
|
+
channels: { type: "object", default: {} }
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Check if initial setup is complete
|
|
129
|
+
isConfigured() {
|
|
130
|
+
const creds = this.conf.get("credentials") || {};
|
|
131
|
+
return !!(creds.github?.token && creds.vercel?.token && creds.supabase?.accessToken && creds.anthropic?.apiKey);
|
|
132
|
+
}
|
|
133
|
+
// Check if current directory is a CodeBakers project
|
|
134
|
+
isInProject() {
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
const codebakersDir = path.join(cwd, ".codebakers");
|
|
137
|
+
const claudeFile = path.join(cwd, "CLAUDE.md");
|
|
138
|
+
return fs.existsSync(codebakersDir) || fs.existsSync(claudeFile);
|
|
139
|
+
}
|
|
140
|
+
// Get current project config
|
|
141
|
+
getProjectConfig() {
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
const configPath = path.join(cwd, ".codebakers", "config.json");
|
|
144
|
+
if (fs.existsSync(configPath)) {
|
|
145
|
+
return fs.readJsonSync(configPath);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
// Credentials
|
|
150
|
+
getCredentials(service) {
|
|
151
|
+
const creds = this.conf.get("credentials") || {};
|
|
152
|
+
return creds[service];
|
|
153
|
+
}
|
|
154
|
+
setCredentials(service, credentials) {
|
|
155
|
+
const current = this.conf.get("credentials") || {};
|
|
156
|
+
this.conf.set("credentials", {
|
|
157
|
+
...current,
|
|
158
|
+
[service]: { ...current[service], ...credentials }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Preferences
|
|
162
|
+
getPreference(key) {
|
|
163
|
+
const prefs = this.conf.get("preferences") || {};
|
|
164
|
+
return prefs[key];
|
|
165
|
+
}
|
|
166
|
+
setPreference(key, value) {
|
|
167
|
+
const current = this.conf.get("preferences") || {};
|
|
168
|
+
this.conf.set("preferences", { ...current, [key]: value });
|
|
169
|
+
}
|
|
170
|
+
// Learning
|
|
171
|
+
getLearning() {
|
|
172
|
+
return this.conf.get("learning") || { enabled: true, shortcuts: {}, preferences: {}, rejections: [], workflows: {} };
|
|
173
|
+
}
|
|
174
|
+
addShortcut(shortcut, expansion) {
|
|
175
|
+
const learning = this.getLearning();
|
|
176
|
+
learning.shortcuts[shortcut] = expansion;
|
|
177
|
+
this.conf.set("learning", learning);
|
|
178
|
+
}
|
|
179
|
+
addRejection(item) {
|
|
180
|
+
const learning = this.getLearning();
|
|
181
|
+
if (!learning.rejections.includes(item)) {
|
|
182
|
+
learning.rejections.push(item);
|
|
183
|
+
this.conf.set("learning", learning);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
addWorkflow(name, steps) {
|
|
187
|
+
const learning = this.getLearning();
|
|
188
|
+
learning.workflows[name] = steps;
|
|
189
|
+
this.conf.set("learning", learning);
|
|
190
|
+
}
|
|
191
|
+
learnPreference(key, value) {
|
|
192
|
+
const learning = this.getLearning();
|
|
193
|
+
learning.preferences[key] = value;
|
|
194
|
+
this.conf.set("learning", learning);
|
|
195
|
+
}
|
|
196
|
+
forgetPreference(key) {
|
|
197
|
+
const learning = this.getLearning();
|
|
198
|
+
delete learning.preferences[key];
|
|
199
|
+
this.conf.set("learning", learning);
|
|
200
|
+
}
|
|
201
|
+
resetLearning() {
|
|
202
|
+
this.conf.set("learning", { enabled: true, shortcuts: {}, preferences: {}, rejections: [], workflows: {} });
|
|
203
|
+
}
|
|
204
|
+
// Projects
|
|
205
|
+
getProjects() {
|
|
206
|
+
return this.conf.get("projects") || [];
|
|
207
|
+
}
|
|
208
|
+
addProject(project) {
|
|
209
|
+
const projects = this.getProjects();
|
|
210
|
+
projects.push(project);
|
|
211
|
+
this.conf.set("projects", projects);
|
|
212
|
+
}
|
|
213
|
+
// Channels
|
|
214
|
+
getChannelConfig(channel) {
|
|
215
|
+
const channels = this.conf.get("channels") || {};
|
|
216
|
+
return channels[channel];
|
|
217
|
+
}
|
|
218
|
+
setChannelConfig(channel, config) {
|
|
219
|
+
const current = this.conf.get("channels") || {};
|
|
220
|
+
this.conf.set("channels", { ...current, [channel]: config });
|
|
221
|
+
}
|
|
222
|
+
// Config directory access
|
|
223
|
+
getConfigDir() {
|
|
224
|
+
return this.configDir;
|
|
225
|
+
}
|
|
226
|
+
getPatternsDir() {
|
|
227
|
+
return path.join(this.configDir, "patterns");
|
|
228
|
+
}
|
|
229
|
+
getTemplatesDir() {
|
|
230
|
+
return path.join(this.configDir, "templates");
|
|
231
|
+
}
|
|
232
|
+
getLearningDir() {
|
|
233
|
+
return path.join(this.configDir, "learning");
|
|
234
|
+
}
|
|
235
|
+
// Export/Import
|
|
236
|
+
exportConfig() {
|
|
237
|
+
return {
|
|
238
|
+
version: this.conf.get("version") || "1.0.0",
|
|
239
|
+
credentials: {},
|
|
240
|
+
// Don't export credentials
|
|
241
|
+
preferences: this.conf.get("preferences") || {},
|
|
242
|
+
learning: this.getLearning(),
|
|
243
|
+
projects: this.getProjects(),
|
|
244
|
+
channels: {}
|
|
245
|
+
// Don't export channel tokens
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
importConfig(config) {
|
|
249
|
+
if (config.preferences) {
|
|
250
|
+
this.conf.set("preferences", config.preferences);
|
|
251
|
+
}
|
|
252
|
+
if (config.learning) {
|
|
253
|
+
this.conf.set("learning", config.learning);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/utils/updates.ts
|
|
259
|
+
import chalk from "chalk";
|
|
260
|
+
var VERSION = "1.0.0";
|
|
261
|
+
async function checkForUpdates() {
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch("https://registry.npmjs.org/codebakers/latest", {
|
|
264
|
+
signal: AbortSignal.timeout(3e3)
|
|
265
|
+
});
|
|
266
|
+
if (!response.ok) return;
|
|
267
|
+
const data = await response.json();
|
|
268
|
+
const latestVersion = data.version;
|
|
269
|
+
if (latestVersion && latestVersion !== VERSION) {
|
|
270
|
+
console.log(chalk.yellow(`
|
|
271
|
+
\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
|
|
272
|
+
\u2502 Update available: ${VERSION} \u2192 ${latestVersion.padEnd(10)} \u2502
|
|
273
|
+
\u2502 Run: npm install -g codebakers \u2502
|
|
274
|
+
\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F
|
|
275
|
+
`));
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/commands/setup.ts
|
|
282
|
+
import * as p from "@clack/prompts";
|
|
283
|
+
import chalk2 from "chalk";
|
|
284
|
+
import open from "open";
|
|
285
|
+
async function setupCommand() {
|
|
286
|
+
const config = new Config();
|
|
287
|
+
p.intro(chalk2.bgCyan.black(" CodeBakers Setup "));
|
|
288
|
+
if (config.isConfigured()) {
|
|
289
|
+
const action = await p.select({
|
|
290
|
+
message: "CodeBakers is already configured. What do you want to do?",
|
|
291
|
+
options: [
|
|
292
|
+
{ value: "view", label: "\u{1F440} View connected services" },
|
|
293
|
+
{ value: "add", label: "\u2795 Add another service" },
|
|
294
|
+
{ value: "update", label: "\u{1F504} Update credentials" },
|
|
295
|
+
{ value: "reset", label: "\u{1F5D1}\uFE0F Reset all configuration" },
|
|
296
|
+
{ value: "back", label: "\u2190 Back" }
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
if (p.isCancel(action) || action === "back") {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (action === "view") {
|
|
303
|
+
showConnectedServices(config);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (action === "reset") {
|
|
307
|
+
const confirm6 = await p.confirm({
|
|
308
|
+
message: "Are you sure? This will remove all credentials and settings."
|
|
309
|
+
});
|
|
310
|
+
if (confirm6) {
|
|
311
|
+
p.outro(chalk2.yellow("Configuration reset. Run `codebakers setup` again."));
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (action === "add") {
|
|
316
|
+
await addService(config);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
p.note(
|
|
321
|
+
`Let's connect your accounts. This only takes a few minutes.
|
|
322
|
+
|
|
323
|
+
${chalk2.dim("Your credentials are stored locally in ~/.codebakers/")}
|
|
324
|
+
${chalk2.dim("and are never sent to our servers.")}`,
|
|
325
|
+
"Welcome to CodeBakers!"
|
|
326
|
+
);
|
|
327
|
+
const requiredServices = [
|
|
328
|
+
{ name: "GitHub", key: "github", required: true },
|
|
329
|
+
{ name: "Vercel", key: "vercel", required: true },
|
|
330
|
+
{ name: "Supabase", key: "supabase", required: true },
|
|
331
|
+
{ name: "Anthropic (Claude)", key: "anthropic", required: true }
|
|
332
|
+
];
|
|
333
|
+
const optionalServices = [
|
|
334
|
+
{ name: "OpenAI", key: "openai" },
|
|
335
|
+
{ name: "Stripe", key: "stripe" },
|
|
336
|
+
{ name: "Twilio", key: "twilio" },
|
|
337
|
+
{ name: "VAPI", key: "vapi" },
|
|
338
|
+
{ name: "Resend", key: "resend" },
|
|
339
|
+
{ name: "ElevenLabs", key: "elevenLabs" },
|
|
340
|
+
{ name: "Microsoft Graph", key: "microsoft" },
|
|
341
|
+
{ name: "Google APIs", key: "google" }
|
|
342
|
+
];
|
|
343
|
+
p.log.step("Connecting required services...");
|
|
344
|
+
for (const service of requiredServices) {
|
|
345
|
+
await connectService(config, service.key, service.name, true);
|
|
346
|
+
}
|
|
347
|
+
const addOptional = await p.confirm({
|
|
348
|
+
message: "Do you want to connect optional services? (Stripe, Twilio, etc.)",
|
|
349
|
+
initialValue: false
|
|
350
|
+
});
|
|
351
|
+
if (addOptional && !p.isCancel(addOptional)) {
|
|
352
|
+
const selected = await p.multiselect({
|
|
353
|
+
message: "Select services to connect:",
|
|
354
|
+
options: optionalServices.map((s) => ({
|
|
355
|
+
value: s.key,
|
|
356
|
+
label: s.name
|
|
357
|
+
})),
|
|
358
|
+
required: false
|
|
359
|
+
});
|
|
360
|
+
if (!p.isCancel(selected)) {
|
|
361
|
+
for (const serviceKey of selected) {
|
|
362
|
+
const service = optionalServices.find((s) => s.key === serviceKey);
|
|
363
|
+
if (service) {
|
|
364
|
+
await connectService(config, service.key, service.name, false);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
p.log.step("Installing CodeBakers patterns...");
|
|
370
|
+
await installPatterns(config);
|
|
371
|
+
p.outro(chalk2.green("\u2713 Setup complete! Run `codebakers init` to create your first project."));
|
|
372
|
+
}
|
|
373
|
+
async function connectService(config, serviceKey, serviceName, required) {
|
|
374
|
+
const spinner11 = p.spinner();
|
|
375
|
+
switch (serviceKey) {
|
|
376
|
+
case "github": {
|
|
377
|
+
p.log.info(`${chalk2.bold("GitHub")} - Opens browser for OAuth authorization`);
|
|
378
|
+
const proceed = await p.confirm({
|
|
379
|
+
message: "Open browser to authorize GitHub?",
|
|
380
|
+
initialValue: true
|
|
381
|
+
});
|
|
382
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
383
|
+
if (required) {
|
|
384
|
+
p.log.warn("GitHub is required. Skipping for now.");
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
p.log.info(chalk2.dim("Opening browser..."));
|
|
389
|
+
await open("https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&scope=repo,user");
|
|
390
|
+
const token = await p.text({
|
|
391
|
+
message: "Paste your GitHub token (or press Enter if OAuth completed):",
|
|
392
|
+
placeholder: "ghp_..."
|
|
393
|
+
});
|
|
394
|
+
if (!p.isCancel(token) && token) {
|
|
395
|
+
config.setCredentials("github", { token });
|
|
396
|
+
p.log.success("GitHub connected!");
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "vercel": {
|
|
402
|
+
p.log.info(`${chalk2.bold("Vercel")} - Opens browser for OAuth authorization`);
|
|
403
|
+
const proceed = await p.confirm({
|
|
404
|
+
message: "Open browser to authorize Vercel?",
|
|
405
|
+
initialValue: true
|
|
406
|
+
});
|
|
407
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
408
|
+
if (required) {
|
|
409
|
+
p.log.warn("Vercel is required. Skipping for now.");
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
p.log.info(chalk2.dim("Opening browser..."));
|
|
414
|
+
await open("https://vercel.com/account/tokens");
|
|
415
|
+
const token = await p.text({
|
|
416
|
+
message: "Paste your Vercel token:",
|
|
417
|
+
placeholder: "vercel_..."
|
|
418
|
+
});
|
|
419
|
+
if (!p.isCancel(token) && token) {
|
|
420
|
+
config.setCredentials("vercel", { token });
|
|
421
|
+
p.log.success("Vercel connected!");
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case "supabase": {
|
|
427
|
+
p.log.info(`${chalk2.bold("Supabase")} - Opens browser for OAuth authorization`);
|
|
428
|
+
const proceed = await p.confirm({
|
|
429
|
+
message: "Open browser to authorize Supabase?",
|
|
430
|
+
initialValue: true
|
|
431
|
+
});
|
|
432
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
433
|
+
if (required) {
|
|
434
|
+
p.log.warn("Supabase is required. Skipping for now.");
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
p.log.info(chalk2.dim("Opening browser..."));
|
|
439
|
+
await open("https://supabase.com/dashboard/account/tokens");
|
|
440
|
+
const token = await p.text({
|
|
441
|
+
message: "Paste your Supabase access token:",
|
|
442
|
+
placeholder: "sbp_..."
|
|
443
|
+
});
|
|
444
|
+
if (!p.isCancel(token) && token) {
|
|
445
|
+
config.setCredentials("supabase", { accessToken: token });
|
|
446
|
+
p.log.success("Supabase connected!");
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "anthropic": {
|
|
452
|
+
p.log.info(`${chalk2.bold("Anthropic (Claude)")} - Powers the AI coding agent`);
|
|
453
|
+
const openBrowser = await p.confirm({
|
|
454
|
+
message: "Open browser to get API key?",
|
|
455
|
+
initialValue: true
|
|
456
|
+
});
|
|
457
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
458
|
+
await open("https://console.anthropic.com/settings/keys");
|
|
459
|
+
}
|
|
460
|
+
const apiKey = await p.text({
|
|
461
|
+
message: "Paste your Anthropic API key:",
|
|
462
|
+
placeholder: "sk-ant-...",
|
|
463
|
+
validate: (value) => {
|
|
464
|
+
if (!value && required) return "API key is required";
|
|
465
|
+
if (value && !value.startsWith("sk-ant-")) return "Invalid API key format";
|
|
466
|
+
return void 0;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
if (!p.isCancel(apiKey) && apiKey) {
|
|
470
|
+
config.setCredentials("anthropic", { apiKey });
|
|
471
|
+
p.log.success("Anthropic connected!");
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "openai": {
|
|
477
|
+
const openBrowser = await p.confirm({
|
|
478
|
+
message: "Open browser to get OpenAI API key?",
|
|
479
|
+
initialValue: true
|
|
480
|
+
});
|
|
481
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
482
|
+
await open("https://platform.openai.com/api-keys");
|
|
483
|
+
}
|
|
484
|
+
const apiKey = await p.text({
|
|
485
|
+
message: "Paste your OpenAI API key:",
|
|
486
|
+
placeholder: "sk-..."
|
|
487
|
+
});
|
|
488
|
+
if (!p.isCancel(apiKey) && apiKey) {
|
|
489
|
+
config.setCredentials("openai", { apiKey });
|
|
490
|
+
p.log.success("OpenAI connected!");
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "stripe": {
|
|
496
|
+
const openBrowser = await p.confirm({
|
|
497
|
+
message: "Open browser to get Stripe API keys?",
|
|
498
|
+
initialValue: true
|
|
499
|
+
});
|
|
500
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
501
|
+
await open("https://dashboard.stripe.com/apikeys");
|
|
502
|
+
}
|
|
503
|
+
const secretKey = await p.text({
|
|
504
|
+
message: "Paste your Stripe secret key:",
|
|
505
|
+
placeholder: "sk_live_... or sk_test_..."
|
|
506
|
+
});
|
|
507
|
+
if (!p.isCancel(secretKey) && secretKey) {
|
|
508
|
+
config.setCredentials("stripe", { secretKey });
|
|
509
|
+
p.log.success("Stripe connected!");
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "twilio": {
|
|
515
|
+
const openBrowser = await p.confirm({
|
|
516
|
+
message: "Open browser to get Twilio credentials?",
|
|
517
|
+
initialValue: true
|
|
518
|
+
});
|
|
519
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
520
|
+
await open("https://console.twilio.com/");
|
|
521
|
+
}
|
|
522
|
+
const accountSid = await p.text({
|
|
523
|
+
message: "Paste your Twilio Account SID:",
|
|
524
|
+
placeholder: "AC..."
|
|
525
|
+
});
|
|
526
|
+
const authToken = await p.text({
|
|
527
|
+
message: "Paste your Twilio Auth Token:",
|
|
528
|
+
placeholder: "..."
|
|
529
|
+
});
|
|
530
|
+
if (!p.isCancel(accountSid) && !p.isCancel(authToken) && accountSid && authToken) {
|
|
531
|
+
config.setCredentials("twilio", {
|
|
532
|
+
accountSid,
|
|
533
|
+
authToken
|
|
534
|
+
});
|
|
535
|
+
p.log.success("Twilio connected!");
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case "vapi": {
|
|
541
|
+
const openBrowser = await p.confirm({
|
|
542
|
+
message: "Open browser to get VAPI API key?",
|
|
543
|
+
initialValue: true
|
|
544
|
+
});
|
|
545
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
546
|
+
await open("https://dashboard.vapi.ai/");
|
|
547
|
+
}
|
|
548
|
+
const apiKey = await p.text({
|
|
549
|
+
message: "Paste your VAPI API key:",
|
|
550
|
+
placeholder: "..."
|
|
551
|
+
});
|
|
552
|
+
if (!p.isCancel(apiKey) && apiKey) {
|
|
553
|
+
config.setCredentials("vapi", { apiKey });
|
|
554
|
+
p.log.success("VAPI connected!");
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "resend": {
|
|
560
|
+
const openBrowser = await p.confirm({
|
|
561
|
+
message: "Open browser to get Resend API key?",
|
|
562
|
+
initialValue: true
|
|
563
|
+
});
|
|
564
|
+
if (openBrowser && !p.isCancel(openBrowser)) {
|
|
565
|
+
await open("https://resend.com/api-keys");
|
|
566
|
+
}
|
|
567
|
+
const apiKey = await p.text({
|
|
568
|
+
message: "Paste your Resend API key:",
|
|
569
|
+
placeholder: "re_..."
|
|
570
|
+
});
|
|
571
|
+
if (!p.isCancel(apiKey) && apiKey) {
|
|
572
|
+
config.setCredentials("resend", { apiKey });
|
|
573
|
+
p.log.success("Resend connected!");
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
default:
|
|
579
|
+
p.log.warn(`Service ${serviceName} not yet implemented`);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
async function addService(config) {
|
|
585
|
+
const services = [
|
|
586
|
+
{ value: "github", label: "GitHub" },
|
|
587
|
+
{ value: "vercel", label: "Vercel" },
|
|
588
|
+
{ value: "supabase", label: "Supabase" },
|
|
589
|
+
{ value: "anthropic", label: "Anthropic (Claude)" },
|
|
590
|
+
{ value: "openai", label: "OpenAI" },
|
|
591
|
+
{ value: "stripe", label: "Stripe" },
|
|
592
|
+
{ value: "twilio", label: "Twilio" },
|
|
593
|
+
{ value: "vapi", label: "VAPI" },
|
|
594
|
+
{ value: "resend", label: "Resend" },
|
|
595
|
+
{ value: "elevenLabs", label: "ElevenLabs" },
|
|
596
|
+
{ value: "microsoft", label: "Microsoft Graph" },
|
|
597
|
+
{ value: "google", label: "Google APIs" }
|
|
598
|
+
];
|
|
599
|
+
const service = await p.select({
|
|
600
|
+
message: "Which service do you want to connect?",
|
|
601
|
+
options: services
|
|
602
|
+
});
|
|
603
|
+
if (!p.isCancel(service)) {
|
|
604
|
+
const serviceInfo = services.find((s) => s.value === service);
|
|
605
|
+
if (serviceInfo) {
|
|
606
|
+
await connectService(config, serviceInfo.value, serviceInfo.label, false);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function showConnectedServices(config) {
|
|
611
|
+
const services = [
|
|
612
|
+
{ key: "github", name: "GitHub" },
|
|
613
|
+
{ key: "vercel", name: "Vercel" },
|
|
614
|
+
{ key: "supabase", name: "Supabase" },
|
|
615
|
+
{ key: "anthropic", name: "Anthropic" },
|
|
616
|
+
{ key: "openai", name: "OpenAI" },
|
|
617
|
+
{ key: "stripe", name: "Stripe" },
|
|
618
|
+
{ key: "twilio", name: "Twilio" },
|
|
619
|
+
{ key: "vapi", name: "VAPI" },
|
|
620
|
+
{ key: "resend", name: "Resend" },
|
|
621
|
+
{ key: "elevenLabs", name: "ElevenLabs" },
|
|
622
|
+
{ key: "microsoft", name: "Microsoft" },
|
|
623
|
+
{ key: "google", name: "Google" }
|
|
624
|
+
];
|
|
625
|
+
console.log("\n" + chalk2.bold("Connected Services:") + "\n");
|
|
626
|
+
for (const service of services) {
|
|
627
|
+
const creds = config.getCredentials(service.key);
|
|
628
|
+
const isConnected = creds && Object.values(creds).some((v) => v);
|
|
629
|
+
const status = isConnected ? chalk2.green("\u2713 Connected") : chalk2.dim("\u25CB Not connected");
|
|
630
|
+
console.log(` ${service.name.padEnd(15)} ${status}`);
|
|
631
|
+
}
|
|
632
|
+
console.log("");
|
|
633
|
+
}
|
|
634
|
+
async function installPatterns(config) {
|
|
635
|
+
const patternsDir = config.getPatternsDir();
|
|
636
|
+
p.log.info(chalk2.dim(`Patterns installed to ${patternsDir}`));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/init.ts
|
|
640
|
+
import * as p2 from "@clack/prompts";
|
|
641
|
+
import chalk3 from "chalk";
|
|
642
|
+
import * as fs2 from "fs-extra";
|
|
643
|
+
import * as path2 from "path";
|
|
644
|
+
import { execa as execa2 } from "execa";
|
|
645
|
+
|
|
646
|
+
// src/services/github.ts
|
|
647
|
+
import { Octokit } from "@octokit/rest";
|
|
648
|
+
var GitHubService = class {
|
|
649
|
+
octokit;
|
|
650
|
+
config;
|
|
651
|
+
constructor(config) {
|
|
652
|
+
this.config = config;
|
|
653
|
+
const creds = config.getCredentials("github");
|
|
654
|
+
if (!creds?.token) {
|
|
655
|
+
throw new Error("GitHub not configured. Run `codebakers setup`.");
|
|
656
|
+
}
|
|
657
|
+
this.octokit = new Octokit({ auth: creds.token });
|
|
658
|
+
}
|
|
659
|
+
async createRepo(name, options = {}) {
|
|
660
|
+
const response = await this.octokit.repos.createForAuthenticatedUser({
|
|
661
|
+
name,
|
|
662
|
+
private: options.private ?? true,
|
|
663
|
+
auto_init: false
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
id: response.data.id,
|
|
667
|
+
name: response.data.name,
|
|
668
|
+
full_name: response.data.full_name,
|
|
669
|
+
html_url: response.data.html_url,
|
|
670
|
+
clone_url: response.data.clone_url
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
async getUser() {
|
|
674
|
+
const response = await this.octokit.users.getAuthenticated();
|
|
675
|
+
return {
|
|
676
|
+
login: response.data.login,
|
|
677
|
+
name: response.data.name
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async listRepos() {
|
|
681
|
+
const response = await this.octokit.repos.listForAuthenticatedUser({
|
|
682
|
+
sort: "updated",
|
|
683
|
+
per_page: 100
|
|
684
|
+
});
|
|
685
|
+
return response.data.map((repo) => ({
|
|
686
|
+
name: repo.name,
|
|
687
|
+
full_name: repo.full_name,
|
|
688
|
+
private: repo.private
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/services/vercel.ts
|
|
694
|
+
import { execa } from "execa";
|
|
695
|
+
var VercelService = class {
|
|
696
|
+
config;
|
|
697
|
+
token;
|
|
698
|
+
constructor(config) {
|
|
699
|
+
this.config = config;
|
|
700
|
+
const creds = config.getCredentials("vercel");
|
|
701
|
+
if (!creds?.token) {
|
|
702
|
+
throw new Error("Vercel not configured. Run `codebakers setup`.");
|
|
703
|
+
}
|
|
704
|
+
this.token = creds.token;
|
|
705
|
+
}
|
|
706
|
+
async createProject(name) {
|
|
707
|
+
const response = await fetch("https://api.vercel.com/v9/projects", {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
Authorization: `Bearer ${this.token}`,
|
|
711
|
+
"Content-Type": "application/json"
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify({ name, framework: "nextjs" })
|
|
714
|
+
});
|
|
715
|
+
if (!response.ok) {
|
|
716
|
+
const error = await response.json();
|
|
717
|
+
throw new Error(error.error?.message || "Failed to create project");
|
|
718
|
+
}
|
|
719
|
+
const data = await response.json();
|
|
720
|
+
return { id: data.id, name: data.name };
|
|
721
|
+
}
|
|
722
|
+
async deploy(projectPath, production = false) {
|
|
723
|
+
const args2 = ["vercel", "--yes"];
|
|
724
|
+
if (production) args2.push("--prod");
|
|
725
|
+
const result = await execa("npx", args2, {
|
|
726
|
+
cwd: projectPath,
|
|
727
|
+
env: { ...process.env, VERCEL_TOKEN: this.token }
|
|
728
|
+
});
|
|
729
|
+
const urlMatch = result.stdout.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
730
|
+
return { url: urlMatch ? urlMatch[0] : "https://vercel.app" };
|
|
731
|
+
}
|
|
732
|
+
async addDomain(projectName, domain) {
|
|
733
|
+
await fetch(`https://api.vercel.com/v10/projects/${projectName}/domains`, {
|
|
734
|
+
method: "POST",
|
|
735
|
+
headers: {
|
|
736
|
+
Authorization: `Bearer ${this.token}`,
|
|
737
|
+
"Content-Type": "application/json"
|
|
738
|
+
},
|
|
739
|
+
body: JSON.stringify({ name: domain })
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/services/supabase.ts
|
|
745
|
+
var SupabaseService = class {
|
|
746
|
+
config;
|
|
747
|
+
accessToken;
|
|
748
|
+
constructor(config) {
|
|
749
|
+
this.config = config;
|
|
750
|
+
const creds = config.getCredentials("supabase");
|
|
751
|
+
if (!creds?.accessToken) {
|
|
752
|
+
throw new Error("Supabase not configured. Run `codebakers setup`.");
|
|
753
|
+
}
|
|
754
|
+
this.accessToken = creds.accessToken;
|
|
755
|
+
}
|
|
756
|
+
async createProject(name) {
|
|
757
|
+
const dbPassword = this.generatePassword();
|
|
758
|
+
const response = await fetch("https://api.supabase.com/v1/projects", {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: {
|
|
761
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
762
|
+
"Content-Type": "application/json"
|
|
763
|
+
},
|
|
764
|
+
body: JSON.stringify({
|
|
765
|
+
name,
|
|
766
|
+
organization_id: await this.getDefaultOrgId(),
|
|
767
|
+
region: "us-west-1",
|
|
768
|
+
plan: "free",
|
|
769
|
+
db_pass: dbPassword
|
|
770
|
+
})
|
|
771
|
+
});
|
|
772
|
+
if (!response.ok) {
|
|
773
|
+
const error = await response.json();
|
|
774
|
+
throw new Error(error.message || "Failed to create Supabase project");
|
|
775
|
+
}
|
|
776
|
+
const data = await response.json();
|
|
777
|
+
await this.waitForProject(data.id);
|
|
778
|
+
const keys = await this.getApiKeys(data.id);
|
|
779
|
+
return {
|
|
780
|
+
id: data.id,
|
|
781
|
+
name: data.name,
|
|
782
|
+
api_url: `https://${data.id}.supabase.co`,
|
|
783
|
+
anon_key: keys.anon_key
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
async getDefaultOrgId() {
|
|
787
|
+
const response = await fetch("https://api.supabase.com/v1/organizations", {
|
|
788
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
789
|
+
});
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
throw new Error("Failed to get organizations");
|
|
792
|
+
}
|
|
793
|
+
const orgs = await response.json();
|
|
794
|
+
if (orgs.length === 0) {
|
|
795
|
+
throw new Error("No Supabase organization found");
|
|
796
|
+
}
|
|
797
|
+
return orgs[0].id;
|
|
798
|
+
}
|
|
799
|
+
async waitForProject(projectId, maxWait = 12e4) {
|
|
800
|
+
const startTime = Date.now();
|
|
801
|
+
while (Date.now() - startTime < maxWait) {
|
|
802
|
+
const response = await fetch(
|
|
803
|
+
`https://api.supabase.com/v1/projects/${projectId}`,
|
|
804
|
+
{ headers: { Authorization: `Bearer ${this.accessToken}` } }
|
|
805
|
+
);
|
|
806
|
+
if (response.ok) {
|
|
807
|
+
const project = await response.json();
|
|
808
|
+
if (project.status === "ACTIVE_HEALTHY") {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
813
|
+
}
|
|
814
|
+
throw new Error("Timeout waiting for Supabase project");
|
|
815
|
+
}
|
|
816
|
+
async getApiKeys(projectId) {
|
|
817
|
+
const response = await fetch(
|
|
818
|
+
`https://api.supabase.com/v1/projects/${projectId}/api-keys`,
|
|
819
|
+
{ headers: { Authorization: `Bearer ${this.accessToken}` } }
|
|
820
|
+
);
|
|
821
|
+
if (!response.ok) {
|
|
822
|
+
throw new Error("Failed to get API keys");
|
|
823
|
+
}
|
|
824
|
+
const keys = await response.json();
|
|
825
|
+
const anonKey = keys.find((k) => k.name === "anon");
|
|
826
|
+
const serviceKey = keys.find((k) => k.name === "service_role");
|
|
827
|
+
return {
|
|
828
|
+
anon_key: anonKey?.api_key || "",
|
|
829
|
+
service_role_key: serviceKey?.api_key || ""
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
generatePassword() {
|
|
833
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
|
|
834
|
+
let password = "";
|
|
835
|
+
for (let i = 0; i < 24; i++) {
|
|
836
|
+
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
837
|
+
}
|
|
838
|
+
return password;
|
|
839
|
+
}
|
|
840
|
+
async listProjects() {
|
|
841
|
+
const response = await fetch("https://api.supabase.com/v1/projects", {
|
|
842
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
843
|
+
});
|
|
844
|
+
if (!response.ok) {
|
|
845
|
+
throw new Error("Failed to list projects");
|
|
846
|
+
}
|
|
847
|
+
const projects = await response.json();
|
|
848
|
+
return projects.map((p14) => ({
|
|
849
|
+
id: p14.id,
|
|
850
|
+
name: p14.name,
|
|
851
|
+
region: p14.region
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/utils/claude-md.ts
|
|
857
|
+
function generateClaudeMd(config) {
|
|
858
|
+
return `# ${config.name.toUpperCase()} - CLAUDE.md
|
|
859
|
+
|
|
860
|
+
Generated by CodeBakers CLI
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
# CORE DEVELOPMENT STANDARDS
|
|
865
|
+
|
|
866
|
+
## \u{1F9E0} MANDATORY THINKING PROTOCOL
|
|
867
|
+
|
|
868
|
+
**BEFORE WRITING ANY CODE**, complete this mental checklist:
|
|
869
|
+
|
|
870
|
+
\`\`\`
|
|
871
|
+
\u25A2 What is the user actually asking for?
|
|
872
|
+
\u25A2 What are ALL the components needed (UI, API, DB, types)?
|
|
873
|
+
\u25A2 What are the edge cases?
|
|
874
|
+
\u25A2 What error handling is required?
|
|
875
|
+
\u25A2 What loading/empty states are needed?
|
|
876
|
+
\u25A2 How does this integrate with existing code?
|
|
877
|
+
\u25A2 What security implications exist?
|
|
878
|
+
\u25A2 What could break in production?
|
|
879
|
+
\`\`\`
|
|
880
|
+
|
|
881
|
+
**NEVER skip this step.**
|
|
882
|
+
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
## \u{1F6AB} ABSOLUTE PROHIBITIONS
|
|
886
|
+
|
|
887
|
+
These will NEVER appear in your code under ANY circumstances:
|
|
888
|
+
|
|
889
|
+
\`\`\`typescript
|
|
890
|
+
// \u274C BANNED FOREVER - NON-FUNCTIONAL CODE
|
|
891
|
+
onClick={handleClick} // where handleClick doesn't exist
|
|
892
|
+
onSubmit={handleSubmit} // where handleSubmit doesn't exist
|
|
893
|
+
href="/some-page" // where the page doesn't exist
|
|
894
|
+
|
|
895
|
+
// \u274C BANNED FOREVER - INCOMPLETE CODE
|
|
896
|
+
TODO: // No TODOs ever
|
|
897
|
+
FIXME: // No FIXMEs ever
|
|
898
|
+
// ... // No placeholder comments
|
|
899
|
+
throw new Error('Not implemented')
|
|
900
|
+
|
|
901
|
+
// \u274C BANNED FOREVER - DEBUG CODE
|
|
902
|
+
console.log('test') // No debug logs in final code
|
|
903
|
+
debugger; // No debugger statements
|
|
904
|
+
|
|
905
|
+
// \u274C BANNED FOREVER - TYPE SAFETY VIOLATIONS
|
|
906
|
+
any // No 'any' types
|
|
907
|
+
@ts-ignore // No ignoring TypeScript
|
|
908
|
+
as any // No casting to any
|
|
909
|
+
|
|
910
|
+
// \u274C BANNED FOREVER - SECURITY VIOLATIONS
|
|
911
|
+
eval() // No eval ever
|
|
912
|
+
innerHTML = // No direct innerHTML
|
|
913
|
+
dangerouslySetInnerHTML // Without DOMPurify
|
|
914
|
+
\`\`\`
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
## \u2705 MANDATORY PATTERNS
|
|
919
|
+
|
|
920
|
+
### Every Button MUST Have:
|
|
921
|
+
|
|
922
|
+
\`\`\`typescript
|
|
923
|
+
// \u2705 CORRECT - Fully functional button
|
|
924
|
+
<Button
|
|
925
|
+
onClick={handleAction}
|
|
926
|
+
disabled={isLoading || isDisabled}
|
|
927
|
+
aria-label="Descriptive action name"
|
|
928
|
+
aria-busy={isLoading}
|
|
929
|
+
>
|
|
930
|
+
{isLoading ? (
|
|
931
|
+
<>
|
|
932
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
933
|
+
Processing...
|
|
934
|
+
</>
|
|
935
|
+
) : (
|
|
936
|
+
'Action Name'
|
|
937
|
+
)}
|
|
938
|
+
</Button>
|
|
939
|
+
|
|
940
|
+
// The handler MUST exist and be complete:
|
|
941
|
+
const handleAction = async () => {
|
|
942
|
+
setIsLoading(true);
|
|
943
|
+
try {
|
|
944
|
+
await performAction();
|
|
945
|
+
toast.success('Action completed successfully');
|
|
946
|
+
} catch (error) {
|
|
947
|
+
const message = error instanceof Error ? error.message : 'Action failed';
|
|
948
|
+
toast.error(message);
|
|
949
|
+
} finally {
|
|
950
|
+
setIsLoading(false);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
\`\`\`
|
|
954
|
+
|
|
955
|
+
### Every Form MUST Have:
|
|
956
|
+
|
|
957
|
+
\`\`\`typescript
|
|
958
|
+
'use client';
|
|
959
|
+
|
|
960
|
+
import { useState } from 'react';
|
|
961
|
+
import { z } from 'zod';
|
|
962
|
+
import { useForm } from 'react-hook-form';
|
|
963
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
964
|
+
import { toast } from 'sonner';
|
|
965
|
+
|
|
966
|
+
const formSchema = z.object({
|
|
967
|
+
email: z.string().email('Please enter a valid email'),
|
|
968
|
+
name: z.string().min(1, 'Name is required').max(100),
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
type FormData = z.infer<typeof formSchema>;
|
|
972
|
+
|
|
973
|
+
export function ContactForm({ onSuccess }: { onSuccess?: () => void }) {
|
|
974
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
975
|
+
|
|
976
|
+
const form = useForm<FormData>({
|
|
977
|
+
resolver: zodResolver(formSchema),
|
|
978
|
+
defaultValues: { email: '', name: '' },
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const onSubmit = async (data: FormData) => {
|
|
982
|
+
setIsSubmitting(true);
|
|
983
|
+
try {
|
|
984
|
+
const response = await fetch('/api/contact', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'Content-Type': 'application/json' },
|
|
987
|
+
body: JSON.stringify(data),
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
if (!response.ok) {
|
|
991
|
+
const error = await response.json();
|
|
992
|
+
throw new Error(error.message || 'Failed to send');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
toast.success('Message sent!');
|
|
996
|
+
form.reset();
|
|
997
|
+
onSuccess?.();
|
|
998
|
+
} catch (error) {
|
|
999
|
+
const message = error instanceof Error ? error.message : 'Something went wrong';
|
|
1000
|
+
toast.error(message);
|
|
1001
|
+
} finally {
|
|
1002
|
+
setIsSubmitting(false);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
return (
|
|
1007
|
+
<Form {...form}>
|
|
1008
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
1009
|
+
{/* Form fields with FormField, FormLabel, FormControl, FormMessage */}
|
|
1010
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
1011
|
+
{isSubmitting ? 'Sending...' : 'Send Message'}
|
|
1012
|
+
</Button>
|
|
1013
|
+
</form>
|
|
1014
|
+
</Form>
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
\`\`\`
|
|
1018
|
+
|
|
1019
|
+
### Every List MUST Have:
|
|
1020
|
+
|
|
1021
|
+
\`\`\`typescript
|
|
1022
|
+
interface ItemListProps {
|
|
1023
|
+
items: Item[];
|
|
1024
|
+
isLoading: boolean;
|
|
1025
|
+
error: string | null;
|
|
1026
|
+
onRetry?: () => void;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function ItemList({ items, isLoading, error, onRetry }: ItemListProps) {
|
|
1030
|
+
// Loading state
|
|
1031
|
+
if (isLoading) {
|
|
1032
|
+
return (
|
|
1033
|
+
<div className="flex items-center justify-center p-8">
|
|
1034
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
1035
|
+
</div>
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Error state
|
|
1040
|
+
if (error) {
|
|
1041
|
+
return (
|
|
1042
|
+
<div className="flex flex-col items-center gap-4 p-8 text-center">
|
|
1043
|
+
<AlertCircle className="h-12 w-12 text-destructive" />
|
|
1044
|
+
<p className="text-destructive">{error}</p>
|
|
1045
|
+
{onRetry && <Button variant="outline" onClick={onRetry}>Try Again</Button>}
|
|
1046
|
+
</div>
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Empty state
|
|
1051
|
+
if (items.length === 0) {
|
|
1052
|
+
return (
|
|
1053
|
+
<div className="flex flex-col items-center gap-4 p-8 text-center">
|
|
1054
|
+
<Package className="h-12 w-12 text-muted-foreground" />
|
|
1055
|
+
<div>
|
|
1056
|
+
<h3 className="font-medium">No items found</h3>
|
|
1057
|
+
<p className="text-sm text-muted-foreground">Get started by creating your first item.</p>
|
|
1058
|
+
</div>
|
|
1059
|
+
</div>
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Success state with data
|
|
1064
|
+
return (
|
|
1065
|
+
<ul className="divide-y divide-border">
|
|
1066
|
+
{items.map((item) => (
|
|
1067
|
+
<li key={item.id} className="p-4">
|
|
1068
|
+
{item.name}
|
|
1069
|
+
</li>
|
|
1070
|
+
))}
|
|
1071
|
+
</ul>
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
\`\`\`
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
## \u{1F4C1} PROJECT STRUCTURE
|
|
1079
|
+
|
|
1080
|
+
\`\`\`
|
|
1081
|
+
${config.name}/
|
|
1082
|
+
\u251C\u2500\u2500 src/
|
|
1083
|
+
\u2502 \u251C\u2500\u2500 app/ # Next.js App Router
|
|
1084
|
+
\u2502 \u2502 \u251C\u2500\u2500 (auth)/ # Routes requiring authentication
|
|
1085
|
+
\u2502 \u2502 \u251C\u2500\u2500 (public)/ # Public routes
|
|
1086
|
+
\u2502 \u2502 \u251C\u2500\u2500 api/ # API routes
|
|
1087
|
+
\u2502 \u2502 \u2514\u2500\u2500 layout.tsx # Root layout
|
|
1088
|
+
\u2502 \u251C\u2500\u2500 components/
|
|
1089
|
+
\u2502 \u2502 \u251C\u2500\u2500 ui/ # shadcn/ui components
|
|
1090
|
+
\u2502 \u2502 \u251C\u2500\u2500 forms/ # Form components
|
|
1091
|
+
\u2502 \u2502 \u2514\u2500\u2500 [feature]/ # Feature-specific
|
|
1092
|
+
\u2502 \u251C\u2500\u2500 lib/
|
|
1093
|
+
\u2502 \u2502 \u251C\u2500\u2500 supabase/ # Supabase clients
|
|
1094
|
+
\u2502 \u2502 \u251C\u2500\u2500 utils.ts # Utilities
|
|
1095
|
+
\u2502 \u2502 \u2514\u2500\u2500 validations.ts # Zod schemas
|
|
1096
|
+
\u2502 \u251C\u2500\u2500 hooks/ # Custom hooks
|
|
1097
|
+
\u2502 \u251C\u2500\u2500 types/ # TypeScript types
|
|
1098
|
+
\u2502 \u251C\u2500\u2500 stores/ # Zustand stores
|
|
1099
|
+
\u2502 \u2514\u2500\u2500 services/ # Business logic
|
|
1100
|
+
\u251C\u2500\u2500 tests/ # Playwright tests
|
|
1101
|
+
\u251C\u2500\u2500 .codebakers/ # CodeBakers config
|
|
1102
|
+
\u2514\u2500\u2500 CLAUDE.md # This file
|
|
1103
|
+
\`\`\`
|
|
1104
|
+
|
|
1105
|
+
---
|
|
1106
|
+
|
|
1107
|
+
## \u{1F510} SECURITY REQUIREMENTS
|
|
1108
|
+
|
|
1109
|
+
1. **Never expose secrets** - Use environment variables
|
|
1110
|
+
2. **Validate all inputs** - Use Zod on both client and server
|
|
1111
|
+
3. **Enable RLS** - Every Supabase table must have Row Level Security
|
|
1112
|
+
4. **Sanitize user content** - Use DOMPurify for any user HTML
|
|
1113
|
+
5. **Use HTTPS** - Never use HTTP in production
|
|
1114
|
+
6. **Rate limit APIs** - Protect public endpoints
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
## \u{1F9EA} TESTING REQUIREMENTS
|
|
1119
|
+
|
|
1120
|
+
After building ANY feature, automatically test it:
|
|
1121
|
+
|
|
1122
|
+
1. Run \`codebakers check\` to verify patterns
|
|
1123
|
+
2. Check TypeScript: \`npx tsc --noEmit\`
|
|
1124
|
+
3. Run tests: \`npm test\`
|
|
1125
|
+
4. Fix any failures before saying "done"
|
|
1126
|
+
|
|
1127
|
+
---
|
|
1128
|
+
|
|
1129
|
+
**END OF CLAUDE.md**
|
|
1130
|
+
|
|
1131
|
+
Generated by CodeBakers CLI v1.0.0
|
|
1132
|
+
`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/commands/init.ts
|
|
1136
|
+
async function initCommand(options = {}) {
|
|
1137
|
+
const config = new Config();
|
|
1138
|
+
if (!config.isConfigured()) {
|
|
1139
|
+
p2.log.error("Please run `codebakers setup` first.");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
p2.intro(chalk3.bgCyan.black(" Create New Project "));
|
|
1143
|
+
let projectName = options.name;
|
|
1144
|
+
if (!projectName) {
|
|
1145
|
+
const name = await p2.text({
|
|
1146
|
+
message: "Project name:",
|
|
1147
|
+
placeholder: "my-app",
|
|
1148
|
+
validate: (value) => {
|
|
1149
|
+
if (!value) return "Project name is required";
|
|
1150
|
+
if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
|
|
1151
|
+
return void 0;
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
if (p2.isCancel(name)) return;
|
|
1155
|
+
projectName = name;
|
|
1156
|
+
}
|
|
1157
|
+
const projectType = await p2.select({
|
|
1158
|
+
message: "What are you building?",
|
|
1159
|
+
options: [
|
|
1160
|
+
{ value: "web", label: "\u{1F310} Web app", hint: "Next.js \u2192 Vercel" },
|
|
1161
|
+
{ value: "mobile", label: "\u{1F4F1} Mobile app", hint: "Expo \u2192 App stores" },
|
|
1162
|
+
{ value: "desktop", label: "\u{1F5A5}\uFE0F Desktop app", hint: "Tauri \u2192 Windows/Mac/Linux" },
|
|
1163
|
+
{ value: "fullstack", label: "\u{1F4E6} Full stack", hint: "Web + Mobile + Desktop" }
|
|
1164
|
+
]
|
|
1165
|
+
});
|
|
1166
|
+
if (p2.isCancel(projectType)) return;
|
|
1167
|
+
let framework = "nextjs";
|
|
1168
|
+
if (projectType === "web" || projectType === "fullstack") {
|
|
1169
|
+
const selected = await p2.select({
|
|
1170
|
+
message: "Framework:",
|
|
1171
|
+
options: [
|
|
1172
|
+
{ value: "nextjs", label: "Next.js 14+", hint: config.getPreference("defaultFramework") === "nextjs" ? "(your usual choice)" : "" },
|
|
1173
|
+
{ value: "remix", label: "Remix" },
|
|
1174
|
+
{ value: "vite", label: "Vite + React" },
|
|
1175
|
+
{ value: "astro", label: "Astro" }
|
|
1176
|
+
]
|
|
1177
|
+
});
|
|
1178
|
+
if (p2.isCancel(selected)) return;
|
|
1179
|
+
framework = selected;
|
|
1180
|
+
config.learnPreference("defaultFramework", framework);
|
|
1181
|
+
}
|
|
1182
|
+
const uiLibrary = await p2.select({
|
|
1183
|
+
message: "UI Library:",
|
|
1184
|
+
options: [
|
|
1185
|
+
{ value: "shadcn", label: "shadcn/ui", hint: config.getPreference("defaultUI") === "shadcn" ? "(your usual choice)" : "Recommended" },
|
|
1186
|
+
{ value: "chakra", label: "Chakra UI" },
|
|
1187
|
+
{ value: "mantine", label: "Mantine" },
|
|
1188
|
+
{ value: "mui", label: "Material UI" },
|
|
1189
|
+
{ value: "headless", label: "Headless UI" },
|
|
1190
|
+
{ value: "tailwind", label: "Tailwind only" }
|
|
1191
|
+
]
|
|
1192
|
+
});
|
|
1193
|
+
if (p2.isCancel(uiLibrary)) return;
|
|
1194
|
+
config.learnPreference("defaultUI", uiLibrary);
|
|
1195
|
+
const packages = await p2.multiselect({
|
|
1196
|
+
message: "Additional packages:",
|
|
1197
|
+
options: [
|
|
1198
|
+
{ value: "typescript", label: "TypeScript", hint: "Always recommended" },
|
|
1199
|
+
{ value: "tailwind", label: "Tailwind CSS" },
|
|
1200
|
+
{ value: "zod", label: "Zod", hint: "Validation" },
|
|
1201
|
+
{ value: "tanstack-query", label: "TanStack Query", hint: "Data fetching" },
|
|
1202
|
+
{ value: "zustand", label: "Zustand", hint: "State management" },
|
|
1203
|
+
{ value: "react-hook-form", label: "React Hook Form" },
|
|
1204
|
+
{ value: "lucide", label: "Lucide Icons" },
|
|
1205
|
+
{ value: "trpc", label: "tRPC" }
|
|
1206
|
+
],
|
|
1207
|
+
initialValues: ["typescript", "tailwind", "zod", "zustand", "lucide"]
|
|
1208
|
+
});
|
|
1209
|
+
if (p2.isCancel(packages)) return;
|
|
1210
|
+
const services = await p2.multiselect({
|
|
1211
|
+
message: "Services to provision:",
|
|
1212
|
+
options: [
|
|
1213
|
+
{ value: "github", label: "GitHub", hint: "Repository" },
|
|
1214
|
+
{ value: "vercel", label: "Vercel", hint: "Hosting" },
|
|
1215
|
+
{ value: "supabase", label: "Supabase", hint: "Database + Auth" },
|
|
1216
|
+
{ value: "stripe", label: "Stripe", hint: "Payments" },
|
|
1217
|
+
{ value: "resend", label: "Resend", hint: "Email" },
|
|
1218
|
+
{ value: "vapi", label: "VAPI", hint: "Voice AI" }
|
|
1219
|
+
],
|
|
1220
|
+
initialValues: ["github", "vercel", "supabase"]
|
|
1221
|
+
});
|
|
1222
|
+
if (p2.isCancel(services)) return;
|
|
1223
|
+
let domain;
|
|
1224
|
+
const domainChoice = await p2.select({
|
|
1225
|
+
message: "Domain:",
|
|
1226
|
+
options: [
|
|
1227
|
+
{ value: "subdomain", label: "Subdomain", hint: `${projectName}.yourdomain.com` },
|
|
1228
|
+
{ value: "vercel", label: "Vercel subdomain", hint: `${projectName}.vercel.app` },
|
|
1229
|
+
{ value: "purchase", label: "Purchase new domain" },
|
|
1230
|
+
{ value: "skip", label: "Skip for now" }
|
|
1231
|
+
]
|
|
1232
|
+
});
|
|
1233
|
+
if (p2.isCancel(domainChoice)) return;
|
|
1234
|
+
if (domainChoice === "subdomain") {
|
|
1235
|
+
const sub = await p2.text({
|
|
1236
|
+
message: "Subdomain:",
|
|
1237
|
+
placeholder: projectName,
|
|
1238
|
+
initialValue: projectName
|
|
1239
|
+
});
|
|
1240
|
+
if (!p2.isCancel(sub)) {
|
|
1241
|
+
domain = `${sub}.yourdomain.com`;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const projectConfig = {
|
|
1245
|
+
name: projectName,
|
|
1246
|
+
type: projectType,
|
|
1247
|
+
framework,
|
|
1248
|
+
ui: uiLibrary,
|
|
1249
|
+
packages,
|
|
1250
|
+
services,
|
|
1251
|
+
domain
|
|
1252
|
+
};
|
|
1253
|
+
p2.note(
|
|
1254
|
+
`Name: ${projectName}
|
|
1255
|
+
Type: ${projectType}
|
|
1256
|
+
Framework: ${framework}
|
|
1257
|
+
UI: ${uiLibrary}
|
|
1258
|
+
Packages: ${packages.join(", ")}
|
|
1259
|
+
Services: ${services.join(", ")}
|
|
1260
|
+
Domain: ${domain || "Vercel default"}`,
|
|
1261
|
+
"Configuration"
|
|
1262
|
+
);
|
|
1263
|
+
const confirmed = await p2.confirm({
|
|
1264
|
+
message: "Create project?",
|
|
1265
|
+
initialValue: true
|
|
1266
|
+
});
|
|
1267
|
+
if (!confirmed || p2.isCancel(confirmed)) {
|
|
1268
|
+
p2.cancel("Project creation cancelled.");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const spinner11 = p2.spinner();
|
|
1272
|
+
const projectPath = path2.join(process.cwd(), projectName);
|
|
1273
|
+
try {
|
|
1274
|
+
spinner11.start("Creating local project...");
|
|
1275
|
+
await createLocalProject(projectPath, projectConfig);
|
|
1276
|
+
spinner11.stop("Local project created");
|
|
1277
|
+
spinner11.start("Installing dependencies...");
|
|
1278
|
+
await execa2("pnpm", ["install"], { cwd: projectPath });
|
|
1279
|
+
spinner11.stop("Dependencies installed");
|
|
1280
|
+
if (services.includes("github")) {
|
|
1281
|
+
spinner11.start("Creating GitHub repository...");
|
|
1282
|
+
const github = new GitHubService(config);
|
|
1283
|
+
const repo = await github.createRepo(projectName, { private: true });
|
|
1284
|
+
spinner11.stop(`GitHub repo created: ${repo.html_url}`);
|
|
1285
|
+
await execa2("git", ["init"], { cwd: projectPath });
|
|
1286
|
+
await execa2("git", ["add", "."], { cwd: projectPath });
|
|
1287
|
+
await execa2("git", ["commit", "-m", "Initial commit by CodeBakers"], { cwd: projectPath });
|
|
1288
|
+
await execa2("git", ["remote", "add", "origin", repo.clone_url], { cwd: projectPath });
|
|
1289
|
+
await execa2("git", ["push", "-u", "origin", "main"], { cwd: projectPath });
|
|
1290
|
+
}
|
|
1291
|
+
if (services.includes("supabase")) {
|
|
1292
|
+
spinner11.start("Creating Supabase project...");
|
|
1293
|
+
const supabase = new SupabaseService(config);
|
|
1294
|
+
const project = await supabase.createProject(projectName);
|
|
1295
|
+
spinner11.stop(`Supabase project created: ${project.name}`);
|
|
1296
|
+
await fs2.writeJson(
|
|
1297
|
+
path2.join(projectPath, ".codebakers", "supabase.json"),
|
|
1298
|
+
{ projectId: project.id, projectUrl: project.api_url },
|
|
1299
|
+
{ spaces: 2 }
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (services.includes("vercel")) {
|
|
1303
|
+
spinner11.start("Creating Vercel project...");
|
|
1304
|
+
const vercel = new VercelService(config);
|
|
1305
|
+
const project = await vercel.createProject(projectName);
|
|
1306
|
+
spinner11.stop(`Vercel project created`);
|
|
1307
|
+
if (domain) {
|
|
1308
|
+
spinner11.start(`Configuring domain: ${domain}...`);
|
|
1309
|
+
await vercel.addDomain(projectName, domain);
|
|
1310
|
+
spinner11.stop("Domain configured");
|
|
1311
|
+
}
|
|
1312
|
+
spinner11.start("Deploying to Vercel...");
|
|
1313
|
+
const deployment = await vercel.deploy(projectPath);
|
|
1314
|
+
spinner11.stop(`Deployed: ${deployment.url}`);
|
|
1315
|
+
}
|
|
1316
|
+
spinner11.start("Generating CLAUDE.md...");
|
|
1317
|
+
const claudeMd = generateClaudeMd(projectConfig);
|
|
1318
|
+
await fs2.writeFile(path2.join(projectPath, "CLAUDE.md"), claudeMd);
|
|
1319
|
+
spinner11.stop("CLAUDE.md generated");
|
|
1320
|
+
spinner11.start("Setting up CodeBakers enforcement...");
|
|
1321
|
+
await setupGitHooks(projectPath);
|
|
1322
|
+
spinner11.stop("CodeBakers enforcement configured");
|
|
1323
|
+
config.addProject({
|
|
1324
|
+
name: projectName,
|
|
1325
|
+
path: projectPath,
|
|
1326
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1327
|
+
});
|
|
1328
|
+
p2.outro(chalk3.green(`
|
|
1329
|
+
\u2713 Project created!
|
|
1330
|
+
|
|
1331
|
+
${chalk3.bold("Your project is ready:")}
|
|
1332
|
+
${chalk3.cyan(`cd ${projectName}`)}
|
|
1333
|
+
${chalk3.cyan("codebakers code")}
|
|
1334
|
+
|
|
1335
|
+
${chalk3.dim("Shortcuts:")}
|
|
1336
|
+
${chalk3.cyan("codebakers")} \u2014 Interactive menu
|
|
1337
|
+
${chalk3.cyan("codebakers code")} \u2014 AI coding agent
|
|
1338
|
+
${chalk3.cyan("codebakers check")} \u2014 Pattern enforcement
|
|
1339
|
+
${chalk3.cyan("codebakers deploy")} \u2014 Deploy to production
|
|
1340
|
+
`));
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
spinner11.stop("Error occurred");
|
|
1343
|
+
p2.log.error(`Failed to create project: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1344
|
+
const cleanup = await p2.confirm({
|
|
1345
|
+
message: "Clean up partially created project?"
|
|
1346
|
+
});
|
|
1347
|
+
if (cleanup && !p2.isCancel(cleanup)) {
|
|
1348
|
+
await fs2.remove(projectPath);
|
|
1349
|
+
p2.log.info("Cleaned up.");
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async function createLocalProject(projectPath, config) {
|
|
1354
|
+
const framework = config.framework;
|
|
1355
|
+
const ui = config.ui;
|
|
1356
|
+
const packages = config.packages;
|
|
1357
|
+
await fs2.ensureDir(projectPath);
|
|
1358
|
+
await fs2.ensureDir(path2.join(projectPath, ".codebakers"));
|
|
1359
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "app"));
|
|
1360
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "components", "ui"));
|
|
1361
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "components", "features"));
|
|
1362
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "lib"));
|
|
1363
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "hooks"));
|
|
1364
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "types"));
|
|
1365
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "stores"));
|
|
1366
|
+
await fs2.ensureDir(path2.join(projectPath, "src", "services"));
|
|
1367
|
+
const packageJson = {
|
|
1368
|
+
name: path2.basename(projectPath),
|
|
1369
|
+
version: "0.1.0",
|
|
1370
|
+
private: true,
|
|
1371
|
+
scripts: {
|
|
1372
|
+
dev: "next dev",
|
|
1373
|
+
build: "next build",
|
|
1374
|
+
start: "next start",
|
|
1375
|
+
lint: "next lint",
|
|
1376
|
+
typecheck: "tsc --noEmit",
|
|
1377
|
+
"codebakers:check": "codebakers check"
|
|
1378
|
+
},
|
|
1379
|
+
dependencies: {
|
|
1380
|
+
next: "^14.2.0",
|
|
1381
|
+
react: "^18.3.0",
|
|
1382
|
+
"react-dom": "^18.3.0"
|
|
1383
|
+
},
|
|
1384
|
+
devDependencies: {
|
|
1385
|
+
typescript: "^5.5.0",
|
|
1386
|
+
"@types/node": "^22.0.0",
|
|
1387
|
+
"@types/react": "^18.3.0",
|
|
1388
|
+
"@types/react-dom": "^18.3.0"
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
if (packages.includes("tailwind")) {
|
|
1392
|
+
packageJson.devDependencies["tailwindcss"] = "^3.4.0";
|
|
1393
|
+
packageJson.devDependencies["postcss"] = "^8.4.0";
|
|
1394
|
+
packageJson.devDependencies["autoprefixer"] = "^10.4.0";
|
|
1395
|
+
}
|
|
1396
|
+
if (packages.includes("zod")) {
|
|
1397
|
+
packageJson.dependencies["zod"] = "^3.23.0";
|
|
1398
|
+
}
|
|
1399
|
+
if (packages.includes("zustand")) {
|
|
1400
|
+
packageJson.dependencies["zustand"] = "^4.5.0";
|
|
1401
|
+
}
|
|
1402
|
+
if (packages.includes("tanstack-query")) {
|
|
1403
|
+
packageJson.dependencies["@tanstack/react-query"] = "^5.50.0";
|
|
1404
|
+
}
|
|
1405
|
+
if (packages.includes("react-hook-form")) {
|
|
1406
|
+
packageJson.dependencies["react-hook-form"] = "^7.52.0";
|
|
1407
|
+
packageJson.dependencies["@hookform/resolvers"] = "^3.9.0";
|
|
1408
|
+
}
|
|
1409
|
+
if (packages.includes("lucide")) {
|
|
1410
|
+
packageJson.dependencies["lucide-react"] = "^0.400.0";
|
|
1411
|
+
}
|
|
1412
|
+
if (ui === "shadcn") {
|
|
1413
|
+
packageJson.dependencies["class-variance-authority"] = "^0.7.0";
|
|
1414
|
+
packageJson.dependencies["clsx"] = "^2.1.0";
|
|
1415
|
+
packageJson.dependencies["tailwind-merge"] = "^2.4.0";
|
|
1416
|
+
packageJson.dependencies["sonner"] = "^1.5.0";
|
|
1417
|
+
}
|
|
1418
|
+
packageJson.dependencies["@supabase/supabase-js"] = "^2.45.0";
|
|
1419
|
+
packageJson.dependencies["@supabase/ssr"] = "^0.4.0";
|
|
1420
|
+
await fs2.writeJson(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
1421
|
+
await createConfigFiles(projectPath, config);
|
|
1422
|
+
await fs2.writeJson(
|
|
1423
|
+
path2.join(projectPath, ".codebakers", "config.json"),
|
|
1424
|
+
{
|
|
1425
|
+
version: "1.0.0",
|
|
1426
|
+
framework,
|
|
1427
|
+
ui,
|
|
1428
|
+
packages,
|
|
1429
|
+
patterns: ["00-core", "01-database", "02-auth", "03-api", "04-frontend"],
|
|
1430
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1431
|
+
},
|
|
1432
|
+
{ spaces: 2 }
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
async function createConfigFiles(projectPath, config) {
|
|
1436
|
+
const tsConfig = {
|
|
1437
|
+
compilerOptions: {
|
|
1438
|
+
target: "ES2022",
|
|
1439
|
+
lib: ["dom", "dom.iterable", "ES2022"],
|
|
1440
|
+
allowJs: true,
|
|
1441
|
+
skipLibCheck: true,
|
|
1442
|
+
strict: true,
|
|
1443
|
+
noEmit: true,
|
|
1444
|
+
esModuleInterop: true,
|
|
1445
|
+
module: "esnext",
|
|
1446
|
+
moduleResolution: "bundler",
|
|
1447
|
+
resolveJsonModule: true,
|
|
1448
|
+
isolatedModules: true,
|
|
1449
|
+
jsx: "preserve",
|
|
1450
|
+
incremental: true,
|
|
1451
|
+
plugins: [{ name: "next" }],
|
|
1452
|
+
paths: { "@/*": ["./src/*"] }
|
|
1453
|
+
},
|
|
1454
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1455
|
+
exclude: ["node_modules"]
|
|
1456
|
+
};
|
|
1457
|
+
await fs2.writeJson(path2.join(projectPath, "tsconfig.json"), tsConfig, { spaces: 2 });
|
|
1458
|
+
const nextConfig = `/** @type {import('next').NextConfig} */
|
|
1459
|
+
const nextConfig = {
|
|
1460
|
+
experimental: {
|
|
1461
|
+
typedRoutes: true,
|
|
1462
|
+
},
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
export default nextConfig;
|
|
1466
|
+
`;
|
|
1467
|
+
await fs2.writeFile(path2.join(projectPath, "next.config.mjs"), nextConfig);
|
|
1468
|
+
const tailwindConfig = `import type { Config } from 'tailwindcss';
|
|
1469
|
+
|
|
1470
|
+
const config: Config = {
|
|
1471
|
+
darkMode: ['class'],
|
|
1472
|
+
content: [
|
|
1473
|
+
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
1474
|
+
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
1475
|
+
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
1476
|
+
],
|
|
1477
|
+
theme: {
|
|
1478
|
+
extend: {
|
|
1479
|
+
colors: {
|
|
1480
|
+
border: 'hsl(var(--border))',
|
|
1481
|
+
input: 'hsl(var(--input))',
|
|
1482
|
+
ring: 'hsl(var(--ring))',
|
|
1483
|
+
background: 'hsl(var(--background))',
|
|
1484
|
+
foreground: 'hsl(var(--foreground))',
|
|
1485
|
+
primary: {
|
|
1486
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
1487
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
1488
|
+
},
|
|
1489
|
+
secondary: {
|
|
1490
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
1491
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
1492
|
+
},
|
|
1493
|
+
destructive: {
|
|
1494
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
1495
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
1496
|
+
},
|
|
1497
|
+
muted: {
|
|
1498
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
1499
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
1500
|
+
},
|
|
1501
|
+
accent: {
|
|
1502
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
1503
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
borderRadius: {
|
|
1507
|
+
lg: 'var(--radius)',
|
|
1508
|
+
md: 'calc(var(--radius) - 2px)',
|
|
1509
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
1510
|
+
},
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
plugins: [require('tailwindcss-animate')],
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
export default config;
|
|
1517
|
+
`;
|
|
1518
|
+
await fs2.writeFile(path2.join(projectPath, "tailwind.config.ts"), tailwindConfig);
|
|
1519
|
+
const envExample = `# Supabase
|
|
1520
|
+
NEXT_PUBLIC_SUPABASE_URL=
|
|
1521
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|
1522
|
+
SUPABASE_SERVICE_ROLE_KEY=
|
|
1523
|
+
|
|
1524
|
+
# Stripe (optional)
|
|
1525
|
+
STRIPE_SECRET_KEY=
|
|
1526
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
1527
|
+
STRIPE_WEBHOOK_SECRET=
|
|
1528
|
+
|
|
1529
|
+
# App
|
|
1530
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
1531
|
+
`;
|
|
1532
|
+
await fs2.writeFile(path2.join(projectPath, ".env.example"), envExample);
|
|
1533
|
+
await fs2.writeFile(path2.join(projectPath, ".env.local"), envExample);
|
|
1534
|
+
const gitignore = `# Dependencies
|
|
1535
|
+
node_modules/
|
|
1536
|
+
.pnp
|
|
1537
|
+
.pnp.js
|
|
1538
|
+
|
|
1539
|
+
# Build
|
|
1540
|
+
.next/
|
|
1541
|
+
out/
|
|
1542
|
+
build/
|
|
1543
|
+
dist/
|
|
1544
|
+
|
|
1545
|
+
# Environment
|
|
1546
|
+
.env
|
|
1547
|
+
.env.local
|
|
1548
|
+
.env.*.local
|
|
1549
|
+
|
|
1550
|
+
# IDE
|
|
1551
|
+
.vscode/
|
|
1552
|
+
.idea/
|
|
1553
|
+
*.swp
|
|
1554
|
+
*.swo
|
|
1555
|
+
|
|
1556
|
+
# OS
|
|
1557
|
+
.DS_Store
|
|
1558
|
+
Thumbs.db
|
|
1559
|
+
|
|
1560
|
+
# Logs
|
|
1561
|
+
npm-debug.log*
|
|
1562
|
+
yarn-debug.log*
|
|
1563
|
+
yarn-error.log*
|
|
1564
|
+
|
|
1565
|
+
# Testing
|
|
1566
|
+
coverage/
|
|
1567
|
+
.nyc_output/
|
|
1568
|
+
|
|
1569
|
+
# Misc
|
|
1570
|
+
*.pem
|
|
1571
|
+
`;
|
|
1572
|
+
await fs2.writeFile(path2.join(projectPath, ".gitignore"), gitignore);
|
|
1573
|
+
}
|
|
1574
|
+
async function setupGitHooks(projectPath) {
|
|
1575
|
+
const hooksDir = path2.join(projectPath, ".git", "hooks");
|
|
1576
|
+
if (!await fs2.pathExists(path2.join(projectPath, ".git"))) {
|
|
1577
|
+
await execa2("git", ["init"], { cwd: projectPath });
|
|
1578
|
+
}
|
|
1579
|
+
await fs2.ensureDir(hooksDir);
|
|
1580
|
+
const preCommitHook = `#!/bin/sh
|
|
1581
|
+
# CodeBakers pre-commit hook
|
|
1582
|
+
|
|
1583
|
+
echo "\u{1F50D} Running CodeBakers check..."
|
|
1584
|
+
|
|
1585
|
+
# Run pattern check
|
|
1586
|
+
codebakers check
|
|
1587
|
+
|
|
1588
|
+
# If check fails, abort commit
|
|
1589
|
+
if [ $? -ne 0 ]; then
|
|
1590
|
+
echo "\u274C CodeBakers check failed. Fix issues or use --no-verify to bypass."
|
|
1591
|
+
exit 1
|
|
1592
|
+
fi
|
|
1593
|
+
|
|
1594
|
+
echo "\u2713 CodeBakers check passed"
|
|
1595
|
+
`;
|
|
1596
|
+
await fs2.writeFile(path2.join(hooksDir, "pre-commit"), preCommitHook);
|
|
1597
|
+
await fs2.chmod(path2.join(hooksDir, "pre-commit"), "755");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/commands/code.ts
|
|
1601
|
+
import * as p4 from "@clack/prompts";
|
|
1602
|
+
import chalk5 from "chalk";
|
|
1603
|
+
import * as fs5 from "fs-extra";
|
|
1604
|
+
import * as path5 from "path";
|
|
1605
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1606
|
+
import { execa as execa3 } from "execa";
|
|
1607
|
+
|
|
1608
|
+
// src/patterns/loader.ts
|
|
1609
|
+
import * as fs3 from "fs-extra";
|
|
1610
|
+
import * as path3 from "path";
|
|
1611
|
+
var CORE_PATTERNS = `
|
|
1612
|
+
# CODEBAKERS CORE PATTERNS
|
|
1613
|
+
|
|
1614
|
+
## ABSOLUTE PROHIBITIONS
|
|
1615
|
+
|
|
1616
|
+
These will NEVER appear in your code:
|
|
1617
|
+
|
|
1618
|
+
\`\`\`typescript
|
|
1619
|
+
// \u274C BANNED - NON-FUNCTIONAL CODE
|
|
1620
|
+
onClick={handleClick} // where handleClick doesn't exist
|
|
1621
|
+
onSubmit={handleSubmit} // where handleSubmit doesn't exist
|
|
1622
|
+
href="/some-page" // where the page doesn't exist
|
|
1623
|
+
|
|
1624
|
+
// \u274C BANNED - INCOMPLETE CODE
|
|
1625
|
+
TODO: // No TODOs ever
|
|
1626
|
+
FIXME: // No FIXMEs ever
|
|
1627
|
+
// ... // No placeholder comments
|
|
1628
|
+
throw new Error('Not implemented')
|
|
1629
|
+
|
|
1630
|
+
// \u274C BANNED - DEBUG CODE
|
|
1631
|
+
console.log('test') // No debug logs
|
|
1632
|
+
debugger; // No debugger statements
|
|
1633
|
+
|
|
1634
|
+
// \u274C BANNED - TYPE SAFETY VIOLATIONS
|
|
1635
|
+
any // No 'any' types
|
|
1636
|
+
@ts-ignore // No ignoring TypeScript
|
|
1637
|
+
as any // No casting to any
|
|
1638
|
+
|
|
1639
|
+
// \u274C BANNED - SECURITY VIOLATIONS
|
|
1640
|
+
eval() // No eval ever
|
|
1641
|
+
innerHTML = // No direct innerHTML
|
|
1642
|
+
dangerouslySetInnerHTML // Without DOMPurify
|
|
1643
|
+
\`\`\`
|
|
1644
|
+
|
|
1645
|
+
## MANDATORY PATTERNS
|
|
1646
|
+
|
|
1647
|
+
### Every Button MUST Have:
|
|
1648
|
+
|
|
1649
|
+
\`\`\`typescript
|
|
1650
|
+
<Button
|
|
1651
|
+
onClick={handleAction}
|
|
1652
|
+
disabled={isLoading || isDisabled}
|
|
1653
|
+
aria-label="Descriptive action name"
|
|
1654
|
+
>
|
|
1655
|
+
{isLoading ? (
|
|
1656
|
+
<>
|
|
1657
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1658
|
+
Processing...
|
|
1659
|
+
</>
|
|
1660
|
+
) : (
|
|
1661
|
+
'Action Name'
|
|
1662
|
+
)}
|
|
1663
|
+
</Button>
|
|
1664
|
+
|
|
1665
|
+
// The handler MUST exist and be complete:
|
|
1666
|
+
const handleAction = async () => {
|
|
1667
|
+
setIsLoading(true);
|
|
1668
|
+
try {
|
|
1669
|
+
await performAction();
|
|
1670
|
+
toast.success('Action completed successfully');
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
const message = error instanceof Error ? error.message : 'Action failed';
|
|
1673
|
+
toast.error(message);
|
|
1674
|
+
} finally {
|
|
1675
|
+
setIsLoading(false);
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
\`\`\`
|
|
1679
|
+
|
|
1680
|
+
### Every Form MUST Have:
|
|
1681
|
+
|
|
1682
|
+
1. React Hook Form with Zod validation
|
|
1683
|
+
2. Loading state during submission
|
|
1684
|
+
3. Error messages for each field
|
|
1685
|
+
4. Success/error toast notifications
|
|
1686
|
+
5. Proper disabled state while submitting
|
|
1687
|
+
|
|
1688
|
+
### Every Async Operation MUST Have:
|
|
1689
|
+
|
|
1690
|
+
\`\`\`typescript
|
|
1691
|
+
const [data, setData] = useState<DataType | null>(null);
|
|
1692
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1693
|
+
const [error, setError] = useState<string | null>(null);
|
|
1694
|
+
|
|
1695
|
+
const fetchData = async () => {
|
|
1696
|
+
setIsLoading(true);
|
|
1697
|
+
setError(null);
|
|
1698
|
+
|
|
1699
|
+
try {
|
|
1700
|
+
const response = await fetch('/api/data');
|
|
1701
|
+
if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
|
|
1702
|
+
const result = await response.json();
|
|
1703
|
+
setData(result.data);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch';
|
|
1706
|
+
setError(message);
|
|
1707
|
+
toast.error(message);
|
|
1708
|
+
} finally {
|
|
1709
|
+
setIsLoading(false);
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
\`\`\`
|
|
1713
|
+
|
|
1714
|
+
### Every List MUST Have:
|
|
1715
|
+
|
|
1716
|
+
1. Loading state with spinner/skeleton
|
|
1717
|
+
2. Error state with retry button
|
|
1718
|
+
3. Empty state with helpful message
|
|
1719
|
+
4. Success state with data
|
|
1720
|
+
|
|
1721
|
+
## FILE STRUCTURE
|
|
1722
|
+
|
|
1723
|
+
\`\`\`
|
|
1724
|
+
src/
|
|
1725
|
+
\u251C\u2500\u2500 app/ # Next.js App Router
|
|
1726
|
+
\u2502 \u251C\u2500\u2500 (auth)/ # Routes requiring authentication
|
|
1727
|
+
\u2502 \u251C\u2500\u2500 (public)/ # Public routes
|
|
1728
|
+
\u2502 \u251C\u2500\u2500 (marketing)/ # Marketing pages
|
|
1729
|
+
\u2502 \u251C\u2500\u2500 api/ # API routes
|
|
1730
|
+
\u2502 \u2514\u2500\u2500 layout.tsx # Root layout
|
|
1731
|
+
\u251C\u2500\u2500 components/
|
|
1732
|
+
\u2502 \u251C\u2500\u2500 ui/ # shadcn/ui components
|
|
1733
|
+
\u2502 \u251C\u2500\u2500 forms/ # Form components
|
|
1734
|
+
\u2502 \u251C\u2500\u2500 layouts/ # Layout components
|
|
1735
|
+
\u2502 \u2514\u2500\u2500 [feature]/ # Feature-specific components
|
|
1736
|
+
\u251C\u2500\u2500 lib/
|
|
1737
|
+
\u2502 \u251C\u2500\u2500 supabase/ # Supabase clients
|
|
1738
|
+
\u2502 \u251C\u2500\u2500 utils.ts # Utility functions
|
|
1739
|
+
\u2502 \u2514\u2500\u2500 validations.ts # Zod schemas
|
|
1740
|
+
\u251C\u2500\u2500 hooks/ # Custom React hooks
|
|
1741
|
+
\u251C\u2500\u2500 types/ # TypeScript types
|
|
1742
|
+
\u251C\u2500\u2500 stores/ # Zustand stores
|
|
1743
|
+
\u2514\u2500\u2500 services/ # Business logic
|
|
1744
|
+
\`\`\`
|
|
1745
|
+
|
|
1746
|
+
## API ROUTE PATTERN
|
|
1747
|
+
|
|
1748
|
+
\`\`\`typescript
|
|
1749
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1750
|
+
import { z } from 'zod';
|
|
1751
|
+
import { createServerClient } from '@/lib/supabase/server';
|
|
1752
|
+
|
|
1753
|
+
const requestSchema = z.object({
|
|
1754
|
+
// Define request body schema
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
export async function POST(request: NextRequest) {
|
|
1758
|
+
try {
|
|
1759
|
+
// 1. Auth check
|
|
1760
|
+
const supabase = createServerClient();
|
|
1761
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
1762
|
+
|
|
1763
|
+
if (authError || !user) {
|
|
1764
|
+
return NextResponse.json(
|
|
1765
|
+
{ error: 'Unauthorized' },
|
|
1766
|
+
{ status: 401 }
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// 2. Validate input
|
|
1771
|
+
const body = await request.json();
|
|
1772
|
+
const result = requestSchema.safeParse(body);
|
|
1773
|
+
|
|
1774
|
+
if (!result.success) {
|
|
1775
|
+
return NextResponse.json(
|
|
1776
|
+
{ error: 'Validation failed', details: result.error.flatten() },
|
|
1777
|
+
{ status: 400 }
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// 3. Business logic
|
|
1782
|
+
const data = result.data;
|
|
1783
|
+
// ... do something
|
|
1784
|
+
|
|
1785
|
+
// 4. Return success
|
|
1786
|
+
return NextResponse.json({ data }, { status: 201 });
|
|
1787
|
+
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
console.error('[API] Error:', error);
|
|
1790
|
+
return NextResponse.json(
|
|
1791
|
+
{ error: 'Internal server error' },
|
|
1792
|
+
{ status: 500 }
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
\`\`\`
|
|
1797
|
+
|
|
1798
|
+
## SUPABASE RLS PATTERN
|
|
1799
|
+
|
|
1800
|
+
Every table MUST have Row Level Security enabled:
|
|
1801
|
+
|
|
1802
|
+
\`\`\`sql
|
|
1803
|
+
-- Enable RLS
|
|
1804
|
+
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
1805
|
+
|
|
1806
|
+
-- User can only see their own data
|
|
1807
|
+
CREATE POLICY "Users can view own data"
|
|
1808
|
+
ON your_table FOR SELECT
|
|
1809
|
+
TO authenticated
|
|
1810
|
+
USING (user_id = auth.uid());
|
|
1811
|
+
|
|
1812
|
+
-- User can only insert their own data
|
|
1813
|
+
CREATE POLICY "Users can insert own data"
|
|
1814
|
+
ON your_table FOR INSERT
|
|
1815
|
+
TO authenticated
|
|
1816
|
+
WITH CHECK (user_id = auth.uid());
|
|
1817
|
+
|
|
1818
|
+
-- User can only update their own data
|
|
1819
|
+
CREATE POLICY "Users can update own data"
|
|
1820
|
+
ON your_table FOR UPDATE
|
|
1821
|
+
TO authenticated
|
|
1822
|
+
USING (user_id = auth.uid())
|
|
1823
|
+
WITH CHECK (user_id = auth.uid());
|
|
1824
|
+
|
|
1825
|
+
-- User can only delete their own data
|
|
1826
|
+
CREATE POLICY "Users can delete own data"
|
|
1827
|
+
ON your_table FOR DELETE
|
|
1828
|
+
TO authenticated
|
|
1829
|
+
USING (user_id = auth.uid());
|
|
1830
|
+
\`\`\`
|
|
1831
|
+
|
|
1832
|
+
## STATE MANAGEMENT
|
|
1833
|
+
|
|
1834
|
+
Use Zustand for global state:
|
|
1835
|
+
|
|
1836
|
+
\`\`\`typescript
|
|
1837
|
+
import { create } from 'zustand';
|
|
1838
|
+
|
|
1839
|
+
interface AppState {
|
|
1840
|
+
user: User | null;
|
|
1841
|
+
isLoading: boolean;
|
|
1842
|
+
setUser: (user: User | null) => void;
|
|
1843
|
+
reset: () => void;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
1847
|
+
user: null,
|
|
1848
|
+
isLoading: false,
|
|
1849
|
+
setUser: (user) => set({ user }),
|
|
1850
|
+
reset: () => set({ user: null, isLoading: false }),
|
|
1851
|
+
}));
|
|
1852
|
+
\`\`\`
|
|
1853
|
+
|
|
1854
|
+
## ERROR HANDLING
|
|
1855
|
+
|
|
1856
|
+
Use typed error classes:
|
|
1857
|
+
|
|
1858
|
+
\`\`\`typescript
|
|
1859
|
+
export class AppError extends Error {
|
|
1860
|
+
constructor(
|
|
1861
|
+
message: string,
|
|
1862
|
+
public code: string,
|
|
1863
|
+
public statusCode: number = 500
|
|
1864
|
+
) {
|
|
1865
|
+
super(message);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
export class AuthenticationError extends AppError {
|
|
1870
|
+
constructor(message = 'Authentication required') {
|
|
1871
|
+
super(message, 'AUTH_ERROR', 401);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
export class ValidationError extends AppError {
|
|
1876
|
+
constructor(message = 'Validation failed', public details?: Record<string, string[]>) {
|
|
1877
|
+
super(message, 'VALIDATION_ERROR', 400);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
\`\`\`
|
|
1881
|
+
`;
|
|
1882
|
+
async function loadPatterns(config) {
|
|
1883
|
+
const projectDir = process.cwd();
|
|
1884
|
+
const patterns = [CORE_PATTERNS];
|
|
1885
|
+
const projectPatternDir = path3.join(projectDir, ".codebakers", "patterns");
|
|
1886
|
+
if (await fs3.pathExists(projectPatternDir)) {
|
|
1887
|
+
const files = await fs3.readdir(projectPatternDir);
|
|
1888
|
+
for (const file of files) {
|
|
1889
|
+
if (file.endsWith(".md")) {
|
|
1890
|
+
const content = await fs3.readFile(path3.join(projectPatternDir, file), "utf-8");
|
|
1891
|
+
patterns.push(content);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
const globalPatternDir = config.getPatternsDir();
|
|
1896
|
+
if (await fs3.pathExists(globalPatternDir)) {
|
|
1897
|
+
const files = await fs3.readdir(globalPatternDir);
|
|
1898
|
+
for (const file of files.slice(0, 10)) {
|
|
1899
|
+
if (file.endsWith(".md")) {
|
|
1900
|
+
const content = await fs3.readFile(path3.join(globalPatternDir, file), "utf-8");
|
|
1901
|
+
patterns.push(content.slice(0, 1e4));
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return patterns.join("\n\n---\n\n");
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/commands/check.ts
|
|
1909
|
+
import * as p3 from "@clack/prompts";
|
|
1910
|
+
import chalk4 from "chalk";
|
|
1911
|
+
import * as fs4 from "fs-extra";
|
|
1912
|
+
import * as path4 from "path";
|
|
1913
|
+
import glob from "fast-glob";
|
|
1914
|
+
var RULES = [
|
|
1915
|
+
{
|
|
1916
|
+
id: "no-any",
|
|
1917
|
+
name: "No any type",
|
|
1918
|
+
description: 'Avoid using the "any" type',
|
|
1919
|
+
severity: "error",
|
|
1920
|
+
pattern: /:\s*any\b(?!\s*\=\>)/g,
|
|
1921
|
+
message: 'Avoid using "any" type. Use proper TypeScript types.',
|
|
1922
|
+
autoFixable: false
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
id: "no-ts-ignore",
|
|
1926
|
+
name: "No @ts-ignore",
|
|
1927
|
+
description: "Avoid using @ts-ignore",
|
|
1928
|
+
severity: "error",
|
|
1929
|
+
pattern: /@ts-ignore/g,
|
|
1930
|
+
message: "@ts-ignore bypasses type checking. Fix the underlying issue.",
|
|
1931
|
+
autoFixable: false
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
id: "no-console-log",
|
|
1935
|
+
name: "No console.log",
|
|
1936
|
+
description: "Remove debug console.log statements",
|
|
1937
|
+
severity: "warning",
|
|
1938
|
+
pattern: /console\.log\(/g,
|
|
1939
|
+
message: "Remove console.log before committing. Use proper logging.",
|
|
1940
|
+
autoFixable: true
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
id: "no-todo",
|
|
1944
|
+
name: "No TODO comments",
|
|
1945
|
+
description: "Complete all TODOs before committing",
|
|
1946
|
+
severity: "warning",
|
|
1947
|
+
pattern: /\/\/\s*TODO:/gi,
|
|
1948
|
+
message: "Complete or remove TODO comments before committing.",
|
|
1949
|
+
autoFixable: false
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
id: "no-fixme",
|
|
1953
|
+
name: "No FIXME comments",
|
|
1954
|
+
description: "Fix all FIXME issues before committing",
|
|
1955
|
+
severity: "error",
|
|
1956
|
+
pattern: /\/\/\s*FIXME:/gi,
|
|
1957
|
+
message: "Address FIXME issues before committing.",
|
|
1958
|
+
autoFixable: false
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
id: "button-has-handler",
|
|
1962
|
+
name: "Button has onClick",
|
|
1963
|
+
description: "Buttons should have onClick handlers",
|
|
1964
|
+
severity: "error",
|
|
1965
|
+
pattern: /<Button[^>]*(?!onClick)[^>]*>/g,
|
|
1966
|
+
message: "Button must have an onClick handler.",
|
|
1967
|
+
autoFixable: false,
|
|
1968
|
+
validator: (content, match) => {
|
|
1969
|
+
const buttonTag = match[0];
|
|
1970
|
+
return !buttonTag.includes("onClick") && !buttonTag.includes('type="submit"');
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
id: "async-has-try-catch",
|
|
1975
|
+
name: "Async has try/catch",
|
|
1976
|
+
description: "Async functions should have error handling",
|
|
1977
|
+
severity: "error",
|
|
1978
|
+
pattern: /async\s+(?:function\s+\w+|\([^)]*\)\s*=>|\w+\s*=\s*async\s*\([^)]*\)\s*=>)\s*\{[^}]*\}/g,
|
|
1979
|
+
message: "Async function should have try/catch error handling.",
|
|
1980
|
+
autoFixable: false,
|
|
1981
|
+
validator: (content, match) => {
|
|
1982
|
+
const funcBody = match[0];
|
|
1983
|
+
return !funcBody.includes("try") && !funcBody.includes("catch");
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
id: "no-hardcoded-secrets",
|
|
1988
|
+
name: "No hardcoded secrets",
|
|
1989
|
+
description: "Secrets should be in environment variables",
|
|
1990
|
+
severity: "error",
|
|
1991
|
+
pattern: /(sk_live_|sk_test_|pk_live_|pk_test_|api_key\s*=\s*['"][^'"]+['"])/gi,
|
|
1992
|
+
message: "Possible hardcoded secret. Use environment variables.",
|
|
1993
|
+
autoFixable: false
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
id: "no-eval",
|
|
1997
|
+
name: "No eval()",
|
|
1998
|
+
description: "Never use eval() - security risk",
|
|
1999
|
+
severity: "error",
|
|
2000
|
+
pattern: /\beval\s*\(/g,
|
|
2001
|
+
message: "Never use eval(). It is a security vulnerability.",
|
|
2002
|
+
autoFixable: false
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
id: "no-innerhtml",
|
|
2006
|
+
name: "No innerHTML",
|
|
2007
|
+
description: "Avoid direct innerHTML assignment",
|
|
2008
|
+
severity: "error",
|
|
2009
|
+
pattern: /\.innerHTML\s*=/g,
|
|
2010
|
+
message: "Avoid innerHTML. Use proper React rendering or DOMPurify.",
|
|
2011
|
+
autoFixable: false
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
id: "loading-state",
|
|
2015
|
+
name: "Loading state in components",
|
|
2016
|
+
description: "Components with fetch should have loading states",
|
|
2017
|
+
severity: "warning",
|
|
2018
|
+
pattern: /useEffect\s*\([^)]*fetch\([^)]*\)/g,
|
|
2019
|
+
message: "Components fetching data should have loading states.",
|
|
2020
|
+
autoFixable: false,
|
|
2021
|
+
validator: (content) => {
|
|
2022
|
+
return content.includes("fetch(") && !content.includes("isLoading") && !content.includes("loading") && !content.includes("isPending");
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
id: "form-validation",
|
|
2027
|
+
name: "Form has validation",
|
|
2028
|
+
description: "Forms should use Zod validation",
|
|
2029
|
+
severity: "warning",
|
|
2030
|
+
pattern: /<form[^>]*onSubmit/gi,
|
|
2031
|
+
message: "Forms should use Zod schema validation.",
|
|
2032
|
+
autoFixable: false,
|
|
2033
|
+
validator: (content) => {
|
|
2034
|
+
return content.includes("<form") && !content.includes("zodResolver") && !content.includes("z.object");
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
id: "use-client-directive",
|
|
2039
|
+
name: "use client directive",
|
|
2040
|
+
description: 'Client components should have "use client" directive',
|
|
2041
|
+
severity: "error",
|
|
2042
|
+
pattern: /(useState|useEffect|useReducer|useContext|useRef|useCallback|useMemo)\s*\(/g,
|
|
2043
|
+
message: 'Components using hooks must have "use client" directive.',
|
|
2044
|
+
autoFixable: true,
|
|
2045
|
+
validator: (content) => {
|
|
2046
|
+
const hasHooks = /(useState|useEffect|useReducer|useContext|useRef|useCallback|useMemo)\s*\(/.test(content);
|
|
2047
|
+
const hasDirective = content.trim().startsWith("'use client'") || content.trim().startsWith('"use client"');
|
|
2048
|
+
return hasHooks && !hasDirective;
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
id: "key-prop-in-map",
|
|
2053
|
+
name: "Key prop in map",
|
|
2054
|
+
description: "Elements in .map() should have key prop",
|
|
2055
|
+
severity: "error",
|
|
2056
|
+
pattern: /\.map\s*\([^)]*\)\s*=>\s*(?:\(?\s*<[A-Z][^>]*(?!key=)[^>]*>)/g,
|
|
2057
|
+
message: "Elements in .map() must have a unique key prop.",
|
|
2058
|
+
autoFixable: false
|
|
2059
|
+
}
|
|
2060
|
+
];
|
|
2061
|
+
async function checkCommand(options = {}) {
|
|
2062
|
+
const config = new Config();
|
|
2063
|
+
if (!config.isInProject()) {
|
|
2064
|
+
p3.log.error("Not in a CodeBakers project.");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
p3.intro(chalk4.bgCyan.black(" CodeBakers Pattern Check "));
|
|
2068
|
+
const spinner11 = p3.spinner();
|
|
2069
|
+
spinner11.start("Analyzing code...");
|
|
2070
|
+
const result = await runPatternCheck(options.fix || false);
|
|
2071
|
+
spinner11.stop("Analysis complete");
|
|
2072
|
+
displayResults(result);
|
|
2073
|
+
if (result.violations.length > 0 && options.fix) {
|
|
2074
|
+
const fixable = result.violations.filter((v) => v.autoFixable);
|
|
2075
|
+
if (fixable.length > 0) {
|
|
2076
|
+
spinner11.start(`Auto-fixing ${fixable.length} violations...`);
|
|
2077
|
+
await autoFix(fixable);
|
|
2078
|
+
spinner11.stop("Auto-fix complete");
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const errors = result.violations.filter((v) => v.severity === "error");
|
|
2082
|
+
if (errors.length > 0) {
|
|
2083
|
+
p3.outro(chalk4.red(`\u274C ${errors.length} errors found. Fix before committing.`));
|
|
2084
|
+
process.exit(1);
|
|
2085
|
+
} else if (result.violations.length > 0) {
|
|
2086
|
+
p3.outro(chalk4.yellow(`\u26A0\uFE0F ${result.violations.length} warnings. Consider fixing.`));
|
|
2087
|
+
} else {
|
|
2088
|
+
p3.outro(chalk4.green("\u2713 All patterns satisfied!"));
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async function runPatternCheck(autoFix2) {
|
|
2092
|
+
const cwd = process.cwd();
|
|
2093
|
+
const violations = [];
|
|
2094
|
+
const files = await glob(["src/**/*.{ts,tsx,js,jsx}"], {
|
|
2095
|
+
cwd,
|
|
2096
|
+
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
2097
|
+
});
|
|
2098
|
+
for (const file of files) {
|
|
2099
|
+
const filePath = path4.join(cwd, file);
|
|
2100
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
2101
|
+
const lines = content.split("\n");
|
|
2102
|
+
for (const rule of RULES) {
|
|
2103
|
+
if (rule.validator) {
|
|
2104
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
2105
|
+
let match;
|
|
2106
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2107
|
+
if (rule.validator(content, match)) {
|
|
2108
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
2109
|
+
violations.push({
|
|
2110
|
+
file,
|
|
2111
|
+
line,
|
|
2112
|
+
rule: rule.id,
|
|
2113
|
+
message: rule.message,
|
|
2114
|
+
severity: rule.severity,
|
|
2115
|
+
autoFixable: rule.autoFixable
|
|
2116
|
+
});
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
2122
|
+
let match;
|
|
2123
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2124
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
2125
|
+
violations.push({
|
|
2126
|
+
file,
|
|
2127
|
+
line,
|
|
2128
|
+
rule: rule.id,
|
|
2129
|
+
message: rule.message,
|
|
2130
|
+
severity: rule.severity,
|
|
2131
|
+
autoFixable: rule.autoFixable
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return {
|
|
2138
|
+
violations,
|
|
2139
|
+
filesChecked: files.length,
|
|
2140
|
+
passed: violations.length === 0
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function displayResults(result) {
|
|
2144
|
+
console.log(chalk4.dim(`
|
|
2145
|
+
Checked ${result.filesChecked} files
|
|
2146
|
+
`));
|
|
2147
|
+
if (result.violations.length === 0) {
|
|
2148
|
+
console.log(chalk4.green(" \u2713 No violations found\n"));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2152
|
+
for (const v of result.violations) {
|
|
2153
|
+
if (!byFile.has(v.file)) {
|
|
2154
|
+
byFile.set(v.file, []);
|
|
2155
|
+
}
|
|
2156
|
+
byFile.get(v.file).push(v);
|
|
2157
|
+
}
|
|
2158
|
+
for (const [file, fileViolations] of byFile) {
|
|
2159
|
+
console.log(chalk4.bold(file));
|
|
2160
|
+
for (const v of fileViolations) {
|
|
2161
|
+
const icon = v.severity === "error" ? chalk4.red("\u2717") : chalk4.yellow("\u26A0");
|
|
2162
|
+
const fixable = v.autoFixable ? chalk4.dim(" (auto-fixable)") : "";
|
|
2163
|
+
console.log(` ${icon} Line ${v.line}: ${v.message}${fixable}`);
|
|
2164
|
+
}
|
|
2165
|
+
console.log("");
|
|
2166
|
+
}
|
|
2167
|
+
const errors = result.violations.filter((v) => v.severity === "error").length;
|
|
2168
|
+
const warnings = result.violations.filter((v) => v.severity === "warning").length;
|
|
2169
|
+
console.log(chalk4.bold("Summary:"));
|
|
2170
|
+
if (errors > 0) {
|
|
2171
|
+
console.log(chalk4.red(` ${errors} error(s)`));
|
|
2172
|
+
}
|
|
2173
|
+
if (warnings > 0) {
|
|
2174
|
+
console.log(chalk4.yellow(` ${warnings} warning(s)`));
|
|
2175
|
+
}
|
|
2176
|
+
console.log("");
|
|
2177
|
+
}
|
|
2178
|
+
async function autoFix(violations) {
|
|
2179
|
+
const cwd = process.cwd();
|
|
2180
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2181
|
+
for (const v of violations) {
|
|
2182
|
+
if (!byFile.has(v.file)) {
|
|
2183
|
+
byFile.set(v.file, []);
|
|
2184
|
+
}
|
|
2185
|
+
byFile.get(v.file).push(v);
|
|
2186
|
+
}
|
|
2187
|
+
for (const [file, fileViolations] of byFile) {
|
|
2188
|
+
const filePath = path4.join(cwd, file);
|
|
2189
|
+
let content = await fs4.readFile(filePath, "utf-8");
|
|
2190
|
+
for (const v of fileViolations) {
|
|
2191
|
+
switch (v.rule) {
|
|
2192
|
+
case "no-console-log":
|
|
2193
|
+
content = content.replace(/console\.log\([^)]*\);?\n?/g, "");
|
|
2194
|
+
break;
|
|
2195
|
+
case "use-client-directive":
|
|
2196
|
+
if (!content.startsWith("'use client'") && !content.startsWith('"use client"')) {
|
|
2197
|
+
content = "'use client';\n\n" + content;
|
|
2198
|
+
}
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
await fs4.writeFile(filePath, content);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/commands/code.ts
|
|
2207
|
+
async function codeCommand(prompt, options = {}) {
|
|
2208
|
+
const config = new Config();
|
|
2209
|
+
if (!config.isConfigured()) {
|
|
2210
|
+
p4.log.error("Please run `codebakers setup` first.");
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (!config.isInProject()) {
|
|
2214
|
+
p4.log.error("Not in a CodeBakers project. Run `codebakers init` first.");
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const anthropicCreds = config.getCredentials("anthropic");
|
|
2218
|
+
if (!anthropicCreds?.apiKey) {
|
|
2219
|
+
p4.log.error("Anthropic API key not configured. Run `codebakers setup`.");
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const anthropic = new Anthropic({
|
|
2223
|
+
apiKey: anthropicCreds.apiKey
|
|
2224
|
+
});
|
|
2225
|
+
const projectContext = await loadProjectContext();
|
|
2226
|
+
const patterns = await loadPatterns(config);
|
|
2227
|
+
const systemPrompt = buildSystemPrompt(projectContext, patterns);
|
|
2228
|
+
console.log(chalk5.cyan(`
|
|
2229
|
+
\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
|
|
2230
|
+
\u2502 \u{1F916} CodeBakers AI \u2502
|
|
2231
|
+
\u2502 \u2502
|
|
2232
|
+
\u2502 Project: ${projectContext.name.padEnd(38)}\u2502
|
|
2233
|
+
\u2502 Stack: ${`${projectContext.framework} + ${projectContext.ui}`.padEnd(40)}\u2502
|
|
2234
|
+
\u2502 \u2502
|
|
2235
|
+
\u2502 Type your request or "?" for help \u2502
|
|
2236
|
+
\u2502 Type "exit" or Ctrl+C to quit \u2502
|
|
2237
|
+
\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F
|
|
2238
|
+
`));
|
|
2239
|
+
const messages = [];
|
|
2240
|
+
if (prompt) {
|
|
2241
|
+
await processUserInput(prompt, messages, anthropic, systemPrompt, projectContext, config);
|
|
2242
|
+
}
|
|
2243
|
+
while (true) {
|
|
2244
|
+
const input = await p4.text({
|
|
2245
|
+
message: "",
|
|
2246
|
+
placeholder: "What do you want to build?"
|
|
2247
|
+
});
|
|
2248
|
+
if (p4.isCancel(input)) {
|
|
2249
|
+
p4.outro("Goodbye!");
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
const userInput = input.trim();
|
|
2253
|
+
if (!userInput) continue;
|
|
2254
|
+
if (userInput.toLowerCase() === "exit") {
|
|
2255
|
+
p4.outro("Goodbye!");
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
if (userInput === "?") {
|
|
2259
|
+
showHelp();
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
if (userInput.startsWith("/")) {
|
|
2263
|
+
await handleSlashCommand(userInput, projectContext, config);
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
await processUserInput(userInput, messages, anthropic, systemPrompt, projectContext, config);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async function processUserInput(userInput, messages, anthropic, systemPrompt, projectContext, config) {
|
|
2270
|
+
const spinner11 = p4.spinner();
|
|
2271
|
+
messages.push({ role: "user", content: userInput });
|
|
2272
|
+
const wizardResult = await checkForWizard(userInput);
|
|
2273
|
+
if (wizardResult) {
|
|
2274
|
+
messages[messages.length - 1].content = wizardResult;
|
|
2275
|
+
}
|
|
2276
|
+
try {
|
|
2277
|
+
spinner11.start("Thinking...");
|
|
2278
|
+
const response = await anthropic.messages.create({
|
|
2279
|
+
model: "claude-sonnet-4-20250514",
|
|
2280
|
+
max_tokens: 8192,
|
|
2281
|
+
system: systemPrompt,
|
|
2282
|
+
messages: messages.map((m) => ({
|
|
2283
|
+
role: m.role,
|
|
2284
|
+
content: m.content
|
|
2285
|
+
}))
|
|
2286
|
+
});
|
|
2287
|
+
spinner11.stop("");
|
|
2288
|
+
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
|
|
2289
|
+
messages.push({ role: "assistant", content: assistantMessage });
|
|
2290
|
+
const actions = parseActions(assistantMessage);
|
|
2291
|
+
if (actions.length > 0) {
|
|
2292
|
+
console.log(chalk5.cyan("\n\u{1F4CB} Plan:"));
|
|
2293
|
+
actions.forEach((action, i) => {
|
|
2294
|
+
console.log(chalk5.dim(` ${i + 1}. ${action.description}`));
|
|
2295
|
+
});
|
|
2296
|
+
console.log("");
|
|
2297
|
+
const proceed = await p4.confirm({
|
|
2298
|
+
message: "Execute this plan?",
|
|
2299
|
+
initialValue: true
|
|
2300
|
+
});
|
|
2301
|
+
if (proceed && !p4.isCancel(proceed)) {
|
|
2302
|
+
spinner11.start("Building...");
|
|
2303
|
+
for (const action of actions) {
|
|
2304
|
+
await executeAction(action, spinner11);
|
|
2305
|
+
}
|
|
2306
|
+
spinner11.stop("Build complete");
|
|
2307
|
+
console.log(chalk5.dim("\n\u{1F50D} Running CodeBakers check..."));
|
|
2308
|
+
const checkResult = await runPatternCheck(false);
|
|
2309
|
+
if (checkResult.violations.length > 0) {
|
|
2310
|
+
console.log(chalk5.yellow(`
|
|
2311
|
+
\u26A0\uFE0F ${checkResult.violations.length} pattern violations found`));
|
|
2312
|
+
const autoFix2 = await p4.confirm({
|
|
2313
|
+
message: "Auto-fix violations?",
|
|
2314
|
+
initialValue: true
|
|
2315
|
+
});
|
|
2316
|
+
if (autoFix2 && !p4.isCancel(autoFix2)) {
|
|
2317
|
+
spinner11.start("Auto-fixing...");
|
|
2318
|
+
await autoFixViolations(checkResult.violations, anthropic, systemPrompt);
|
|
2319
|
+
spinner11.stop("Violations fixed");
|
|
2320
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
console.log(chalk5.green("\u2713 All patterns satisfied"));
|
|
2323
|
+
}
|
|
2324
|
+
projectContext.recentChanges.push(...actions.map((a) => a.description));
|
|
2325
|
+
}
|
|
2326
|
+
} else {
|
|
2327
|
+
console.log("\n" + assistantMessage + "\n");
|
|
2328
|
+
}
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
spinner11.stop("Error");
|
|
2331
|
+
console.log(chalk5.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function buildSystemPrompt(context, patterns) {
|
|
2335
|
+
return `You are CodeBakers, an AI coding assistant that ALWAYS follows the user's patterns and rules.
|
|
2336
|
+
|
|
2337
|
+
PROJECT CONTEXT:
|
|
2338
|
+
- Name: ${context.name}
|
|
2339
|
+
- Framework: ${context.framework}
|
|
2340
|
+
- UI Library: ${context.ui}
|
|
2341
|
+
- Packages: ${context.packages.join(", ")}
|
|
2342
|
+
|
|
2343
|
+
EXISTING FILES:
|
|
2344
|
+
${context.existingFiles.slice(0, 50).join("\n")}
|
|
2345
|
+
|
|
2346
|
+
RECENT CHANGES:
|
|
2347
|
+
${context.recentChanges.slice(-10).join("\n")}
|
|
2348
|
+
|
|
2349
|
+
CODEBAKERS PATTERNS (MUST FOLLOW):
|
|
2350
|
+
${patterns}
|
|
2351
|
+
|
|
2352
|
+
INSTRUCTIONS:
|
|
2353
|
+
1. ALWAYS follow the patterns above - they are non-negotiable
|
|
2354
|
+
2. When creating code, output it in this format:
|
|
2355
|
+
|
|
2356
|
+
<<<ACTION:CREATE_FILE>>>
|
|
2357
|
+
PATH: src/path/to/file.tsx
|
|
2358
|
+
DESCRIPTION: Brief description
|
|
2359
|
+
<<<CONTENT>>>
|
|
2360
|
+
// file content here
|
|
2361
|
+
<<<END_CONTENT>>>
|
|
2362
|
+
<<<END_ACTION>>>
|
|
2363
|
+
|
|
2364
|
+
3. For editing files:
|
|
2365
|
+
|
|
2366
|
+
<<<ACTION:EDIT_FILE>>>
|
|
2367
|
+
PATH: src/path/to/file.tsx
|
|
2368
|
+
DESCRIPTION: What you're changing
|
|
2369
|
+
<<<FIND>>>
|
|
2370
|
+
// code to find
|
|
2371
|
+
<<<REPLACE>>>
|
|
2372
|
+
// replacement code
|
|
2373
|
+
<<<END_ACTION>>>
|
|
2374
|
+
|
|
2375
|
+
4. For running commands:
|
|
2376
|
+
|
|
2377
|
+
<<<ACTION:RUN_COMMAND>>>
|
|
2378
|
+
COMMAND: pnpm add some-package
|
|
2379
|
+
DESCRIPTION: Why running this
|
|
2380
|
+
<<<END_ACTION>>>
|
|
2381
|
+
|
|
2382
|
+
5. Every button MUST have a working onClick handler
|
|
2383
|
+
6. Every form MUST have proper validation with Zod
|
|
2384
|
+
7. Every async operation MUST have loading, error, and success states
|
|
2385
|
+
8. Every list MUST have empty state handling
|
|
2386
|
+
9. Use Zustand for state management, not prop drilling
|
|
2387
|
+
10. Put files in the correct folders as specified in patterns
|
|
2388
|
+
|
|
2389
|
+
If you're unsure about requirements, ask clarifying questions before coding.
|
|
2390
|
+
`;
|
|
2391
|
+
}
|
|
2392
|
+
async function loadProjectContext() {
|
|
2393
|
+
const cwd = process.cwd();
|
|
2394
|
+
const codebakersConfig = path5.join(cwd, ".codebakers", "config.json");
|
|
2395
|
+
let projectConfig = {};
|
|
2396
|
+
if (await fs5.pathExists(codebakersConfig)) {
|
|
2397
|
+
projectConfig = await fs5.readJson(codebakersConfig);
|
|
2398
|
+
}
|
|
2399
|
+
const glob3 = (await import("fast-glob")).default;
|
|
2400
|
+
const files = await glob3(["src/**/*.{ts,tsx,js,jsx}"], { cwd });
|
|
2401
|
+
return {
|
|
2402
|
+
name: path5.basename(cwd),
|
|
2403
|
+
framework: projectConfig.framework || "nextjs",
|
|
2404
|
+
ui: projectConfig.ui || "shadcn",
|
|
2405
|
+
packages: projectConfig.packages || [],
|
|
2406
|
+
existingFiles: files,
|
|
2407
|
+
recentChanges: []
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function parseActions(response) {
|
|
2411
|
+
const actions = [];
|
|
2412
|
+
const createFileRegex = /<<<ACTION:CREATE_FILE>>>\s*PATH:\s*(.+?)\s*DESCRIPTION:\s*(.+?)\s*<<<CONTENT>>>\s*([\s\S]+?)\s*<<<END_CONTENT>>>\s*<<<END_ACTION>>>/g;
|
|
2413
|
+
let match;
|
|
2414
|
+
while ((match = createFileRegex.exec(response)) !== null) {
|
|
2415
|
+
actions.push({
|
|
2416
|
+
type: "CREATE_FILE",
|
|
2417
|
+
path: match[1].trim(),
|
|
2418
|
+
description: match[2].trim(),
|
|
2419
|
+
content: match[3].trim()
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
const editFileRegex = /<<<ACTION:EDIT_FILE>>>\s*PATH:\s*(.+?)\s*DESCRIPTION:\s*(.+?)\s*<<<FIND>>>\s*([\s\S]+?)\s*<<<REPLACE>>>\s*([\s\S]+?)\s*<<<END_ACTION>>>/g;
|
|
2423
|
+
while ((match = editFileRegex.exec(response)) !== null) {
|
|
2424
|
+
actions.push({
|
|
2425
|
+
type: "EDIT_FILE",
|
|
2426
|
+
path: match[1].trim(),
|
|
2427
|
+
description: match[2].trim(),
|
|
2428
|
+
find: match[3].trim(),
|
|
2429
|
+
replace: match[4].trim()
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
const runCommandRegex = /<<<ACTION:RUN_COMMAND>>>\s*COMMAND:\s*(.+?)\s*DESCRIPTION:\s*(.+?)\s*<<<END_ACTION>>>/g;
|
|
2433
|
+
while ((match = runCommandRegex.exec(response)) !== null) {
|
|
2434
|
+
actions.push({
|
|
2435
|
+
type: "RUN_COMMAND",
|
|
2436
|
+
command: match[1].trim(),
|
|
2437
|
+
description: match[2].trim()
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return actions;
|
|
2441
|
+
}
|
|
2442
|
+
async function executeAction(action, spinner11) {
|
|
2443
|
+
const cwd = process.cwd();
|
|
2444
|
+
switch (action.type) {
|
|
2445
|
+
case "CREATE_FILE": {
|
|
2446
|
+
const filePath = path5.join(cwd, action.path);
|
|
2447
|
+
await fs5.ensureDir(path5.dirname(filePath));
|
|
2448
|
+
await fs5.writeFile(filePath, action.content);
|
|
2449
|
+
spinner11.message(`Created ${action.path}`);
|
|
2450
|
+
break;
|
|
2451
|
+
}
|
|
2452
|
+
case "EDIT_FILE": {
|
|
2453
|
+
const filePath = path5.join(cwd, action.path);
|
|
2454
|
+
if (await fs5.pathExists(filePath)) {
|
|
2455
|
+
let content = await fs5.readFile(filePath, "utf-8");
|
|
2456
|
+
if (action.find && content.includes(action.find)) {
|
|
2457
|
+
content = content.replace(action.find, action.replace || "");
|
|
2458
|
+
await fs5.writeFile(filePath, content);
|
|
2459
|
+
spinner11.message(`Edited ${action.path}`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
case "RUN_COMMAND": {
|
|
2465
|
+
spinner11.message(`Running: ${action.command}`);
|
|
2466
|
+
const [cmd, ...args2] = action.command.split(" ");
|
|
2467
|
+
await execa3(cmd, args2, { cwd, stdio: "pipe" });
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
case "DELETE_FILE": {
|
|
2471
|
+
const filePath = path5.join(cwd, action.path);
|
|
2472
|
+
if (await fs5.pathExists(filePath)) {
|
|
2473
|
+
await fs5.remove(filePath);
|
|
2474
|
+
spinner11.message(`Deleted ${action.path}`);
|
|
2475
|
+
}
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async function checkForWizard(input) {
|
|
2481
|
+
const lower = input.toLowerCase();
|
|
2482
|
+
if (lower.includes("add") || lower.includes("create") || lower.includes("build")) {
|
|
2483
|
+
if (lower.includes("auth") || lower.includes("login") || lower.includes("signup")) {
|
|
2484
|
+
return await runAuthWizard(input);
|
|
2485
|
+
}
|
|
2486
|
+
if (lower.includes("payment") || lower.includes("checkout") || lower.includes("stripe")) {
|
|
2487
|
+
return await runPaymentWizard(input);
|
|
2488
|
+
}
|
|
2489
|
+
if (lower.includes("form")) {
|
|
2490
|
+
return await runFormWizard(input);
|
|
2491
|
+
}
|
|
2492
|
+
if (lower.includes("dashboard")) {
|
|
2493
|
+
return await runDashboardWizard(input);
|
|
2494
|
+
}
|
|
2495
|
+
if (lower.includes("chat") || lower.includes("ai")) {
|
|
2496
|
+
return await runAIWizard(input);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
async function runAuthWizard(originalInput) {
|
|
2502
|
+
console.log(chalk5.cyan("\n\u{1F510} Auth Wizard"));
|
|
2503
|
+
const authType = await p4.select({
|
|
2504
|
+
message: "What type of authentication?",
|
|
2505
|
+
options: [
|
|
2506
|
+
{ value: "magic-link", label: "\u{1F517} Magic link (email)" },
|
|
2507
|
+
{ value: "password", label: "\u{1F511} Password (email + password)" },
|
|
2508
|
+
{ value: "social", label: "\u{1F310} Social login (Google, GitHub)" },
|
|
2509
|
+
{ value: "phone", label: "\u{1F4F1} Phone (SMS code)" }
|
|
2510
|
+
]
|
|
2511
|
+
});
|
|
2512
|
+
if (p4.isCancel(authType)) return originalInput;
|
|
2513
|
+
const pages = await p4.multiselect({
|
|
2514
|
+
message: "What pages do you need?",
|
|
2515
|
+
options: [
|
|
2516
|
+
{ value: "login", label: "Login page" },
|
|
2517
|
+
{ value: "signup", label: "Signup page" },
|
|
2518
|
+
{ value: "forgot", label: "Forgot password" },
|
|
2519
|
+
{ value: "reset", label: "Reset password" },
|
|
2520
|
+
{ value: "verify", label: "Email verification" }
|
|
2521
|
+
],
|
|
2522
|
+
initialValues: ["login", "signup"]
|
|
2523
|
+
});
|
|
2524
|
+
if (p4.isCancel(pages)) return originalInput;
|
|
2525
|
+
const redirect = await p4.text({
|
|
2526
|
+
message: "Where should users go after login?",
|
|
2527
|
+
placeholder: "/dashboard",
|
|
2528
|
+
initialValue: "/dashboard"
|
|
2529
|
+
});
|
|
2530
|
+
return `Create a complete authentication system with:
|
|
2531
|
+
- Auth type: ${authType}
|
|
2532
|
+
- Pages: ${pages.join(", ")}
|
|
2533
|
+
- Redirect after login: ${redirect}
|
|
2534
|
+
- Use Supabase Auth
|
|
2535
|
+
- Include loading states, error handling, form validation
|
|
2536
|
+
- Follow all CodeBakers patterns`;
|
|
2537
|
+
}
|
|
2538
|
+
async function runPaymentWizard(originalInput) {
|
|
2539
|
+
console.log(chalk5.cyan("\n\u{1F4B3} Payment Wizard"));
|
|
2540
|
+
const paymentType = await p4.select({
|
|
2541
|
+
message: "What type of payment flow?",
|
|
2542
|
+
options: [
|
|
2543
|
+
{ value: "one-time", label: "\u{1F4B5} One-time purchase" },
|
|
2544
|
+
{ value: "subscription", label: "\u{1F504} Subscription (recurring)" },
|
|
2545
|
+
{ value: "both", label: "\u{1F4E6} Both" }
|
|
2546
|
+
]
|
|
2547
|
+
});
|
|
2548
|
+
if (p4.isCancel(paymentType)) return originalInput;
|
|
2549
|
+
const features = await p4.multiselect({
|
|
2550
|
+
message: "What features?",
|
|
2551
|
+
options: [
|
|
2552
|
+
{ value: "checkout", label: "Checkout page" },
|
|
2553
|
+
{ value: "webhook", label: "Webhook handler" },
|
|
2554
|
+
{ value: "portal", label: "Customer portal" },
|
|
2555
|
+
{ value: "invoices", label: "Invoice history" }
|
|
2556
|
+
],
|
|
2557
|
+
initialValues: ["checkout", "webhook"]
|
|
2558
|
+
});
|
|
2559
|
+
if (p4.isCancel(features)) return originalInput;
|
|
2560
|
+
return `Create a complete Stripe payment system with:
|
|
2561
|
+
- Payment type: ${paymentType}
|
|
2562
|
+
- Features: ${features.join(", ")}
|
|
2563
|
+
- Use Stripe SDK
|
|
2564
|
+
- Include webhook signature verification
|
|
2565
|
+
- Handle all error cases
|
|
2566
|
+
- Follow all CodeBakers patterns`;
|
|
2567
|
+
}
|
|
2568
|
+
async function runFormWizard(originalInput) {
|
|
2569
|
+
console.log(chalk5.cyan("\n\u{1F4DD} Form Wizard"));
|
|
2570
|
+
const formName = await p4.text({
|
|
2571
|
+
message: "What is this form for?",
|
|
2572
|
+
placeholder: "Contact form"
|
|
2573
|
+
});
|
|
2574
|
+
if (p4.isCancel(formName)) return originalInput;
|
|
2575
|
+
return `Create a ${formName} form with:
|
|
2576
|
+
- React Hook Form for form handling
|
|
2577
|
+
- Zod validation schema
|
|
2578
|
+
- Loading state during submission
|
|
2579
|
+
- Error messages for each field
|
|
2580
|
+
- Success toast on completion
|
|
2581
|
+
- Proper accessibility (aria labels)
|
|
2582
|
+
- Follow all CodeBakers patterns`;
|
|
2583
|
+
}
|
|
2584
|
+
async function runDashboardWizard(originalInput) {
|
|
2585
|
+
console.log(chalk5.cyan("\n\u{1F4CA} Dashboard Wizard"));
|
|
2586
|
+
const components = await p4.multiselect({
|
|
2587
|
+
message: "What components?",
|
|
2588
|
+
options: [
|
|
2589
|
+
{ value: "stats", label: "Stats cards" },
|
|
2590
|
+
{ value: "chart", label: "Charts/graphs" },
|
|
2591
|
+
{ value: "table", label: "Data table" },
|
|
2592
|
+
{ value: "activity", label: "Activity feed" },
|
|
2593
|
+
{ value: "sidebar", label: "Sidebar navigation" }
|
|
2594
|
+
],
|
|
2595
|
+
initialValues: ["stats", "sidebar"]
|
|
2596
|
+
});
|
|
2597
|
+
if (p4.isCancel(components)) return originalInput;
|
|
2598
|
+
return `Create a dashboard with:
|
|
2599
|
+
- Components: ${components.join(", ")}
|
|
2600
|
+
- Responsive layout
|
|
2601
|
+
- Loading skeletons
|
|
2602
|
+
- Empty states
|
|
2603
|
+
- Error boundaries
|
|
2604
|
+
- Follow all CodeBakers patterns`;
|
|
2605
|
+
}
|
|
2606
|
+
async function runAIWizard(originalInput) {
|
|
2607
|
+
console.log(chalk5.cyan("\n\u{1F916} AI Feature Wizard"));
|
|
2608
|
+
const aiType = await p4.select({
|
|
2609
|
+
message: "What AI feature?",
|
|
2610
|
+
options: [
|
|
2611
|
+
{ value: "chat", label: "\u{1F4AC} Chat interface" },
|
|
2612
|
+
{ value: "generate", label: "\u{1F4DD} Content generation" },
|
|
2613
|
+
{ value: "analyze", label: "\u{1F50D} Data analysis" }
|
|
2614
|
+
]
|
|
2615
|
+
});
|
|
2616
|
+
if (p4.isCancel(aiType)) return originalInput;
|
|
2617
|
+
const streaming = await p4.confirm({
|
|
2618
|
+
message: "Use streaming responses?",
|
|
2619
|
+
initialValue: true
|
|
2620
|
+
});
|
|
2621
|
+
return `Create an AI ${aiType} feature with:
|
|
2622
|
+
- Streaming: ${streaming ? "Yes" : "No"}
|
|
2623
|
+
- Use Anthropic Claude API
|
|
2624
|
+
- Include conversation history
|
|
2625
|
+
- Loading indicators
|
|
2626
|
+
- Error handling
|
|
2627
|
+
- Stop generation button
|
|
2628
|
+
- Follow all CodeBakers patterns`;
|
|
2629
|
+
}
|
|
2630
|
+
async function handleSlashCommand(command, context, config) {
|
|
2631
|
+
const [cmd, ...args2] = command.slice(1).split(" ");
|
|
2632
|
+
switch (cmd.toLowerCase()) {
|
|
2633
|
+
case "deploy":
|
|
2634
|
+
console.log(chalk5.dim("Running deploy..."));
|
|
2635
|
+
break;
|
|
2636
|
+
case "check":
|
|
2637
|
+
console.log(chalk5.dim("Running pattern check..."));
|
|
2638
|
+
await runPatternCheck(false);
|
|
2639
|
+
break;
|
|
2640
|
+
case "status":
|
|
2641
|
+
console.log(chalk5.cyan(`
|
|
2642
|
+
Project: ${context.name}
|
|
2643
|
+
Framework: ${context.framework}
|
|
2644
|
+
UI: ${context.ui}
|
|
2645
|
+
Files: ${context.existingFiles.length}
|
|
2646
|
+
`));
|
|
2647
|
+
break;
|
|
2648
|
+
case "undo":
|
|
2649
|
+
console.log(chalk5.yellow("Undo not yet implemented"));
|
|
2650
|
+
break;
|
|
2651
|
+
case "help":
|
|
2652
|
+
showHelp();
|
|
2653
|
+
break;
|
|
2654
|
+
default:
|
|
2655
|
+
console.log(chalk5.dim(`Unknown command: ${cmd}`));
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
function showHelp() {
|
|
2659
|
+
console.log(chalk5.cyan(`
|
|
2660
|
+
Available commands:
|
|
2661
|
+
|
|
2662
|
+
/deploy Deploy to production
|
|
2663
|
+
/check Run pattern check
|
|
2664
|
+
/status Show project status
|
|
2665
|
+
/undo Undo last change
|
|
2666
|
+
/help Show this help
|
|
2667
|
+
|
|
2668
|
+
Tips:
|
|
2669
|
+
\u2022 Just describe what you want to build
|
|
2670
|
+
\u2022 I'll ask clarifying questions if needed
|
|
2671
|
+
\u2022 All code follows your CodeBakers patterns
|
|
2672
|
+
\u2022 Patterns are auto-checked after every change
|
|
2673
|
+
`));
|
|
2674
|
+
}
|
|
2675
|
+
async function autoFixViolations(violations, anthropic, systemPrompt) {
|
|
2676
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2677
|
+
for (const v of violations) {
|
|
2678
|
+
if (!byFile.has(v.file)) {
|
|
2679
|
+
byFile.set(v.file, []);
|
|
2680
|
+
}
|
|
2681
|
+
byFile.get(v.file).push(v);
|
|
2682
|
+
}
|
|
2683
|
+
for (const [file, fileViolations] of byFile) {
|
|
2684
|
+
const content = await fs5.readFile(path5.join(process.cwd(), file), "utf-8");
|
|
2685
|
+
const fixPrompt = `Fix these violations in ${file}:
|
|
2686
|
+
|
|
2687
|
+
${fileViolations.map((v) => `- ${v.rule}: ${v.message}`).join("\n")}
|
|
2688
|
+
|
|
2689
|
+
Current file content:
|
|
2690
|
+
\`\`\`
|
|
2691
|
+
${content}
|
|
2692
|
+
\`\`\`
|
|
2693
|
+
|
|
2694
|
+
Output only the fixed file content, no explanation.`;
|
|
2695
|
+
const response = await anthropic.messages.create({
|
|
2696
|
+
model: "claude-sonnet-4-20250514",
|
|
2697
|
+
max_tokens: 8192,
|
|
2698
|
+
system: systemPrompt,
|
|
2699
|
+
messages: [{ role: "user", content: fixPrompt }]
|
|
2700
|
+
});
|
|
2701
|
+
const fixedContent = response.content[0].type === "text" ? response.content[0].text : "";
|
|
2702
|
+
const codeMatch = fixedContent.match(/```(?:tsx?|jsx?)?\n([\s\S]+?)\n```/);
|
|
2703
|
+
const finalContent = codeMatch ? codeMatch[1] : fixedContent;
|
|
2704
|
+
await fs5.writeFile(path5.join(process.cwd(), file), finalContent);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// src/commands/deploy.ts
|
|
2709
|
+
import * as p5 from "@clack/prompts";
|
|
2710
|
+
import chalk6 from "chalk";
|
|
2711
|
+
import { execa as execa4 } from "execa";
|
|
2712
|
+
import * as fs6 from "fs-extra";
|
|
2713
|
+
import * as path6 from "path";
|
|
2714
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
2715
|
+
async function deployCommand(options = {}) {
|
|
2716
|
+
const config = new Config();
|
|
2717
|
+
if (!config.isInProject()) {
|
|
2718
|
+
p5.log.error("Not in a CodeBakers project.");
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
p5.intro(chalk6.bgCyan.black(" Deploy to Production "));
|
|
2722
|
+
const spinner11 = p5.spinner();
|
|
2723
|
+
if (options.check !== false) {
|
|
2724
|
+
spinner11.start("Running CodeBakers check...");
|
|
2725
|
+
const checkResult = await runPatternCheck(false);
|
|
2726
|
+
if (!checkResult.passed) {
|
|
2727
|
+
spinner11.stop("");
|
|
2728
|
+
const errors = checkResult.violations.filter((v) => v.severity === "error");
|
|
2729
|
+
if (errors.length > 0) {
|
|
2730
|
+
p5.log.error(`${errors.length} pattern violations found. Fix before deploying.`);
|
|
2731
|
+
const showViolations = await p5.confirm({
|
|
2732
|
+
message: "Show violations?",
|
|
2733
|
+
initialValue: true
|
|
2734
|
+
});
|
|
2735
|
+
if (showViolations && !p5.isCancel(showViolations)) {
|
|
2736
|
+
for (const v of errors) {
|
|
2737
|
+
console.log(chalk6.red(` \u2717 ${v.file}:${v.line} - ${v.message}`));
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const autoFix2 = await p5.confirm({
|
|
2741
|
+
message: "Attempt auto-fix with AI?",
|
|
2742
|
+
initialValue: true
|
|
2743
|
+
});
|
|
2744
|
+
if (!autoFix2 || p5.isCancel(autoFix2)) {
|
|
2745
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
spinner11.start("Auto-fixing with AI...");
|
|
2749
|
+
await autoFixWithAI(config, errors);
|
|
2750
|
+
spinner11.stop("Auto-fix complete");
|
|
2751
|
+
const recheck = await runPatternCheck(false);
|
|
2752
|
+
if (!recheck.passed) {
|
|
2753
|
+
p5.log.error("Some violations remain. Manual fix required.");
|
|
2754
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
spinner11.stop("Pattern check passed");
|
|
2760
|
+
}
|
|
2761
|
+
spinner11.start("Running TypeScript check...");
|
|
2762
|
+
try {
|
|
2763
|
+
await execa4("npx", ["tsc", "--noEmit"], { cwd: process.cwd() });
|
|
2764
|
+
spinner11.stop("TypeScript check passed");
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
spinner11.stop("");
|
|
2767
|
+
p5.log.error("TypeScript errors found.");
|
|
2768
|
+
const fix = await p5.confirm({
|
|
2769
|
+
message: "Attempt to fix TypeScript errors?",
|
|
2770
|
+
initialValue: true
|
|
2771
|
+
});
|
|
2772
|
+
if (fix && !p5.isCancel(fix)) {
|
|
2773
|
+
spinner11.start("Fixing TypeScript errors...");
|
|
2774
|
+
const fixed = await fixTypeScriptErrors(config);
|
|
2775
|
+
spinner11.stop(fixed ? "TypeScript errors fixed" : "Could not auto-fix all errors");
|
|
2776
|
+
if (!fixed) {
|
|
2777
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
} else {
|
|
2781
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
spinner11.start("Building project...");
|
|
2786
|
+
try {
|
|
2787
|
+
await execa4("pnpm", ["build"], { cwd: process.cwd() });
|
|
2788
|
+
spinner11.stop("Build successful");
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
spinner11.stop("");
|
|
2791
|
+
p5.log.error("Build failed.");
|
|
2792
|
+
const errorOutput = error instanceof Error ? error.message : "Unknown error";
|
|
2793
|
+
console.log(chalk6.dim(errorOutput));
|
|
2794
|
+
const fix = await p5.confirm({
|
|
2795
|
+
message: "Attempt to fix build errors with AI?",
|
|
2796
|
+
initialValue: true
|
|
2797
|
+
});
|
|
2798
|
+
if (fix && !p5.isCancel(fix)) {
|
|
2799
|
+
spinner11.start("Fixing build errors...");
|
|
2800
|
+
const fixed = await fixBuildErrors(config, errorOutput);
|
|
2801
|
+
spinner11.stop(fixed ? "Build errors fixed" : "Could not auto-fix");
|
|
2802
|
+
if (fixed) {
|
|
2803
|
+
spinner11.start("Retrying build...");
|
|
2804
|
+
try {
|
|
2805
|
+
await execa4("pnpm", ["build"], { cwd: process.cwd() });
|
|
2806
|
+
spinner11.stop("Build successful");
|
|
2807
|
+
} catch {
|
|
2808
|
+
spinner11.stop("Build still failing");
|
|
2809
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
} else {
|
|
2813
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
} else {
|
|
2817
|
+
p5.outro(chalk6.red("Deploy cancelled."));
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
spinner11.start("Checking for uncommitted changes...");
|
|
2822
|
+
const { stdout: gitStatus } = await execa4("git", ["status", "--porcelain"], { cwd: process.cwd() });
|
|
2823
|
+
if (gitStatus.trim()) {
|
|
2824
|
+
spinner11.stop("");
|
|
2825
|
+
const commit = await p5.confirm({
|
|
2826
|
+
message: "You have uncommitted changes. Commit before deploying?",
|
|
2827
|
+
initialValue: true
|
|
2828
|
+
});
|
|
2829
|
+
if (commit && !p5.isCancel(commit)) {
|
|
2830
|
+
const message = await p5.text({
|
|
2831
|
+
message: "Commit message:",
|
|
2832
|
+
placeholder: "Pre-deploy changes",
|
|
2833
|
+
initialValue: "Pre-deploy changes"
|
|
2834
|
+
});
|
|
2835
|
+
if (!p5.isCancel(message)) {
|
|
2836
|
+
await execa4("git", ["add", "."], { cwd: process.cwd() });
|
|
2837
|
+
await execa4("git", ["commit", "-m", message], { cwd: process.cwd() });
|
|
2838
|
+
spinner11.start("Pushing to GitHub...");
|
|
2839
|
+
await execa4("git", ["push"], { cwd: process.cwd() });
|
|
2840
|
+
spinner11.stop("Pushed to GitHub");
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
} else {
|
|
2844
|
+
spinner11.stop("No uncommitted changes");
|
|
2845
|
+
}
|
|
2846
|
+
const deployType = options.preview ? "preview" : "production";
|
|
2847
|
+
spinner11.start(`Deploying to ${deployType}...`);
|
|
2848
|
+
try {
|
|
2849
|
+
const vercel = new VercelService(config);
|
|
2850
|
+
const deployment = await vercel.deploy(process.cwd(), !options.preview);
|
|
2851
|
+
spinner11.stop("Deployment complete!");
|
|
2852
|
+
console.log(boxedOutput(`
|
|
2853
|
+
${chalk6.green("\u2713")} Deployed successfully!
|
|
2854
|
+
|
|
2855
|
+
${chalk6.bold("URL:")} ${deployment.url}
|
|
2856
|
+
${chalk6.bold("Type:")} ${deployType}
|
|
2857
|
+
${chalk6.bold("Time:")} ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}
|
|
2858
|
+
|
|
2859
|
+
${chalk6.dim("View in Vercel Dashboard:")}
|
|
2860
|
+
${chalk6.dim(deployment.dashboardUrl || "https://vercel.com/dashboard")}
|
|
2861
|
+
`));
|
|
2862
|
+
} catch (error) {
|
|
2863
|
+
spinner11.stop("");
|
|
2864
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2865
|
+
p5.log.error(`Deployment failed: ${errorMsg}`);
|
|
2866
|
+
if (errorMsg.includes("Build failed")) {
|
|
2867
|
+
const retry = await p5.confirm({
|
|
2868
|
+
message: "Attempt to fix and retry?",
|
|
2869
|
+
initialValue: true
|
|
2870
|
+
});
|
|
2871
|
+
if (retry && !p5.isCancel(retry)) {
|
|
2872
|
+
spinner11.start("Analyzing Vercel build error...");
|
|
2873
|
+
spinner11.stop("Fix attempted");
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
p5.outro(chalk6.red("Deploy failed."));
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
async function autoFixWithAI(config, violations) {
|
|
2880
|
+
const anthropicCreds = config.getCredentials("anthropic");
|
|
2881
|
+
if (!anthropicCreds?.apiKey) {
|
|
2882
|
+
throw new Error("Anthropic API key not configured");
|
|
2883
|
+
}
|
|
2884
|
+
const anthropic = new Anthropic2({
|
|
2885
|
+
apiKey: anthropicCreds.apiKey
|
|
2886
|
+
});
|
|
2887
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2888
|
+
for (const v of violations) {
|
|
2889
|
+
if (!byFile.has(v.file)) {
|
|
2890
|
+
byFile.set(v.file, []);
|
|
2891
|
+
}
|
|
2892
|
+
byFile.get(v.file).push(v);
|
|
2893
|
+
}
|
|
2894
|
+
for (const [file, fileViolations] of byFile) {
|
|
2895
|
+
const filePath = path6.join(process.cwd(), file);
|
|
2896
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2897
|
+
const prompt = `Fix these CodeBakers pattern violations in this file:
|
|
2898
|
+
|
|
2899
|
+
Violations:
|
|
2900
|
+
${fileViolations.map((v) => `- Line ${v.line}: ${v.rule} - ${v.message}`).join("\n")}
|
|
2901
|
+
|
|
2902
|
+
Current file content:
|
|
2903
|
+
\`\`\`typescript
|
|
2904
|
+
${content}
|
|
2905
|
+
\`\`\`
|
|
2906
|
+
|
|
2907
|
+
Output ONLY the corrected file content, no explanations. Keep all existing functionality.`;
|
|
2908
|
+
const response = await anthropic.messages.create({
|
|
2909
|
+
model: "claude-sonnet-4-20250514",
|
|
2910
|
+
max_tokens: 8192,
|
|
2911
|
+
messages: [{ role: "user", content: prompt }]
|
|
2912
|
+
});
|
|
2913
|
+
let fixed = response.content[0].type === "text" ? response.content[0].text : "";
|
|
2914
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
2915
|
+
if (codeMatch) {
|
|
2916
|
+
fixed = codeMatch[1];
|
|
2917
|
+
}
|
|
2918
|
+
await fs6.writeFile(filePath, fixed);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
async function fixTypeScriptErrors(config) {
|
|
2922
|
+
try {
|
|
2923
|
+
const { stderr } = await execa4("npx", ["tsc", "--noEmit"], {
|
|
2924
|
+
cwd: process.cwd(),
|
|
2925
|
+
reject: false
|
|
2926
|
+
});
|
|
2927
|
+
if (!stderr) return true;
|
|
2928
|
+
const anthropicCreds = config.getCredentials("anthropic");
|
|
2929
|
+
if (!anthropicCreds?.apiKey) {
|
|
2930
|
+
return false;
|
|
2931
|
+
}
|
|
2932
|
+
const anthropic = new Anthropic2({
|
|
2933
|
+
apiKey: anthropicCreds.apiKey
|
|
2934
|
+
});
|
|
2935
|
+
const errorLines = stderr.split("\n").filter((line) => line.includes("error TS"));
|
|
2936
|
+
for (const errorLine of errorLines.slice(0, 10)) {
|
|
2937
|
+
const match = errorLine.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/);
|
|
2938
|
+
if (!match) continue;
|
|
2939
|
+
const [, file, line, , , message] = match;
|
|
2940
|
+
const filePath = path6.join(process.cwd(), file);
|
|
2941
|
+
if (!await fs6.pathExists(filePath)) continue;
|
|
2942
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2943
|
+
const prompt = `Fix this TypeScript error:
|
|
2944
|
+
|
|
2945
|
+
File: ${file}
|
|
2946
|
+
Line: ${line}
|
|
2947
|
+
Error: ${message}
|
|
2948
|
+
|
|
2949
|
+
Current file content:
|
|
2950
|
+
\`\`\`typescript
|
|
2951
|
+
${content}
|
|
2952
|
+
\`\`\`
|
|
2953
|
+
|
|
2954
|
+
Output ONLY the corrected file content, no explanations.`;
|
|
2955
|
+
const response = await anthropic.messages.create({
|
|
2956
|
+
model: "claude-sonnet-4-20250514",
|
|
2957
|
+
max_tokens: 8192,
|
|
2958
|
+
messages: [{ role: "user", content: prompt }]
|
|
2959
|
+
});
|
|
2960
|
+
let fixed = response.content[0].type === "text" ? response.content[0].text : "";
|
|
2961
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
2962
|
+
if (codeMatch) {
|
|
2963
|
+
fixed = codeMatch[1];
|
|
2964
|
+
}
|
|
2965
|
+
await fs6.writeFile(filePath, fixed);
|
|
2966
|
+
}
|
|
2967
|
+
return true;
|
|
2968
|
+
} catch {
|
|
2969
|
+
return false;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
async function fixBuildErrors(config, errorOutput) {
|
|
2973
|
+
const anthropicCreds = config.getCredentials("anthropic");
|
|
2974
|
+
if (!anthropicCreds?.apiKey) {
|
|
2975
|
+
return false;
|
|
2976
|
+
}
|
|
2977
|
+
const anthropic = new Anthropic2({
|
|
2978
|
+
apiKey: anthropicCreds.apiKey
|
|
2979
|
+
});
|
|
2980
|
+
const fileMatch = errorOutput.match(/(?:Error|error).*?(?:in|at)\s+(.+?\.tsx?)(?::|$)/);
|
|
2981
|
+
if (!fileMatch) return false;
|
|
2982
|
+
const file = fileMatch[1];
|
|
2983
|
+
const filePath = path6.join(process.cwd(), file);
|
|
2984
|
+
if (!await fs6.pathExists(filePath)) return false;
|
|
2985
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
2986
|
+
const prompt = `Fix this build error:
|
|
2987
|
+
|
|
2988
|
+
Error output:
|
|
2989
|
+
${errorOutput}
|
|
2990
|
+
|
|
2991
|
+
File: ${file}
|
|
2992
|
+
|
|
2993
|
+
Current file content:
|
|
2994
|
+
\`\`\`typescript
|
|
2995
|
+
${content}
|
|
2996
|
+
\`\`\`
|
|
2997
|
+
|
|
2998
|
+
Output ONLY the corrected file content, no explanations.`;
|
|
2999
|
+
try {
|
|
3000
|
+
const response = await anthropic.messages.create({
|
|
3001
|
+
model: "claude-sonnet-4-20250514",
|
|
3002
|
+
max_tokens: 8192,
|
|
3003
|
+
messages: [{ role: "user", content: prompt }]
|
|
3004
|
+
});
|
|
3005
|
+
let fixed = response.content[0].type === "text" ? response.content[0].text : "";
|
|
3006
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
3007
|
+
if (codeMatch) {
|
|
3008
|
+
fixed = codeMatch[1];
|
|
3009
|
+
}
|
|
3010
|
+
await fs6.writeFile(filePath, fixed);
|
|
3011
|
+
return true;
|
|
3012
|
+
} catch {
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
function boxedOutput(content) {
|
|
3017
|
+
const lines = content.split("\n");
|
|
3018
|
+
const maxLength = Math.max(...lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
3019
|
+
const border = "\u2500".repeat(maxLength + 2);
|
|
3020
|
+
return `
|
|
3021
|
+
\u256D${border}\u256E
|
|
3022
|
+
${lines.map((l) => `\u2502 ${l.padEnd(maxLength)} \u2502`).join("\n")}
|
|
3023
|
+
\u2570${border}\u256F`;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// src/commands/connect.ts
|
|
3027
|
+
async function connectCommand(service) {
|
|
3028
|
+
const config = new Config();
|
|
3029
|
+
await setupCommand();
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/commands/status.ts
|
|
3033
|
+
import * as p6 from "@clack/prompts";
|
|
3034
|
+
import chalk7 from "chalk";
|
|
3035
|
+
import * as fs7 from "fs-extra";
|
|
3036
|
+
import * as path7 from "path";
|
|
3037
|
+
async function statusCommand() {
|
|
3038
|
+
const config = new Config();
|
|
3039
|
+
const cwd = process.cwd();
|
|
3040
|
+
p6.intro(chalk7.bgCyan.black(" Project Status "));
|
|
3041
|
+
if (!config.isInProject()) {
|
|
3042
|
+
const projects = config.getProjects();
|
|
3043
|
+
if (projects.length === 0) {
|
|
3044
|
+
p6.log.info("No projects found. Run `codebakers init` to create one.");
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
console.log(chalk7.bold("\nYour Projects:\n"));
|
|
3048
|
+
for (const project of projects) {
|
|
3049
|
+
const exists = await fs7.pathExists(project.path);
|
|
3050
|
+
const status = exists ? chalk7.green("\u2713") : chalk7.red("\u2717");
|
|
3051
|
+
console.log(` ${status} ${project.name}`);
|
|
3052
|
+
console.log(chalk7.dim(` ${project.path}`));
|
|
3053
|
+
console.log("");
|
|
3054
|
+
}
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
const projectConfig = await fs7.readJson(
|
|
3058
|
+
path7.join(cwd, ".codebakers", "config.json")
|
|
3059
|
+
).catch(() => ({}));
|
|
3060
|
+
const packageJson = await fs7.readJson(
|
|
3061
|
+
path7.join(cwd, "package.json")
|
|
3062
|
+
).catch(() => ({}));
|
|
3063
|
+
console.log(`
|
|
3064
|
+
${chalk7.bold("Project:")} ${packageJson.name || path7.basename(cwd)}
|
|
3065
|
+
${chalk7.bold("Framework:")} ${projectConfig.framework || "unknown"}
|
|
3066
|
+
${chalk7.bold("UI:")} ${projectConfig.ui || "unknown"}
|
|
3067
|
+
|
|
3068
|
+
${chalk7.bold("Quick Actions:")}
|
|
3069
|
+
${chalk7.cyan("codebakers code")} \u2014 AI coding agent
|
|
3070
|
+
${chalk7.cyan("codebakers check")} \u2014 Pattern enforcement
|
|
3071
|
+
${chalk7.cyan("codebakers deploy")} \u2014 Deploy to production
|
|
3072
|
+
`);
|
|
3073
|
+
p6.outro("");
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/commands/gateway.ts
|
|
3077
|
+
import * as p7 from "@clack/prompts";
|
|
3078
|
+
import chalk8 from "chalk";
|
|
3079
|
+
async function gatewayCommand(options = {}) {
|
|
3080
|
+
const config = new Config();
|
|
3081
|
+
p7.intro(chalk8.bgCyan.black(" Channel Gateway "));
|
|
3082
|
+
if (options.start) {
|
|
3083
|
+
await startAllChannels(config);
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
if (options.stop) {
|
|
3087
|
+
await stopAllChannels(config);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
if (options.status) {
|
|
3091
|
+
await showGatewayStatus(config);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const action = await p7.select({
|
|
3095
|
+
message: "What do you want to do?",
|
|
3096
|
+
options: [
|
|
3097
|
+
{ value: "status", label: "\u{1F4CA} View status", hint: "see all channels" },
|
|
3098
|
+
{ value: "connect", label: "\u{1F517} Connect channel", hint: "add WhatsApp, Telegram, etc." },
|
|
3099
|
+
{ value: "start", label: "\u25B6\uFE0F Start gateway", hint: "start receiving messages" },
|
|
3100
|
+
{ value: "stop", label: "\u23F9\uFE0F Stop gateway", hint: "pause all channels" },
|
|
3101
|
+
{ value: "deploy", label: "\u{1F680} Deploy to cloud", hint: "always-on hosting" },
|
|
3102
|
+
{ value: "back", label: "\u2190 Back" }
|
|
3103
|
+
]
|
|
3104
|
+
});
|
|
3105
|
+
if (p7.isCancel(action) || action === "back") {
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
switch (action) {
|
|
3109
|
+
case "status":
|
|
3110
|
+
await showGatewayStatus(config);
|
|
3111
|
+
break;
|
|
3112
|
+
case "connect":
|
|
3113
|
+
await connectChannelWizard(config);
|
|
3114
|
+
break;
|
|
3115
|
+
case "start":
|
|
3116
|
+
await startAllChannels(config);
|
|
3117
|
+
break;
|
|
3118
|
+
case "stop":
|
|
3119
|
+
await stopAllChannels(config);
|
|
3120
|
+
break;
|
|
3121
|
+
case "deploy":
|
|
3122
|
+
await deployGatewayWizard(config);
|
|
3123
|
+
break;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
async function showGatewayStatus(config) {
|
|
3127
|
+
const channels = await getChannelStatuses(config);
|
|
3128
|
+
console.log(chalk8.bold("\n\u{1F4F1} Channel Gateway Status\n"));
|
|
3129
|
+
for (const channel of channels) {
|
|
3130
|
+
const statusIcon = {
|
|
3131
|
+
running: chalk8.green("\u25CF"),
|
|
3132
|
+
stopped: chalk8.gray("\u25CB"),
|
|
3133
|
+
error: chalk8.red("\u2717"),
|
|
3134
|
+
connecting: chalk8.yellow("\u25D0")
|
|
3135
|
+
}[channel.status];
|
|
3136
|
+
const statusText = {
|
|
3137
|
+
running: chalk8.green("Running"),
|
|
3138
|
+
stopped: chalk8.gray("Stopped"),
|
|
3139
|
+
error: chalk8.red("Error"),
|
|
3140
|
+
connecting: chalk8.yellow("Connecting...")
|
|
3141
|
+
}[channel.status];
|
|
3142
|
+
console.log(` ${channel.icon} ${channel.name.padEnd(15)} ${statusIcon} ${statusText}`);
|
|
3143
|
+
}
|
|
3144
|
+
console.log("");
|
|
3145
|
+
}
|
|
3146
|
+
async function getChannelStatuses(config) {
|
|
3147
|
+
const channels = [
|
|
3148
|
+
{
|
|
3149
|
+
id: "whatsapp",
|
|
3150
|
+
name: "WhatsApp",
|
|
3151
|
+
description: "WhatsApp via Baileys",
|
|
3152
|
+
icon: "\u{1F4AC}",
|
|
3153
|
+
connected: !!config.getChannelConfig("whatsapp")?.enabled,
|
|
3154
|
+
status: config.getChannelConfig("whatsapp")?.enabled ? "stopped" : "stopped"
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
id: "telegram",
|
|
3158
|
+
name: "Telegram",
|
|
3159
|
+
description: "Telegram Bot API",
|
|
3160
|
+
icon: "\u2708\uFE0F",
|
|
3161
|
+
connected: !!config.getChannelConfig("telegram")?.enabled,
|
|
3162
|
+
status: config.getChannelConfig("telegram")?.enabled ? "stopped" : "stopped"
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
id: "discord",
|
|
3166
|
+
name: "Discord",
|
|
3167
|
+
description: "Discord.js bot",
|
|
3168
|
+
icon: "\u{1F3AE}",
|
|
3169
|
+
connected: !!config.getChannelConfig("discord")?.enabled,
|
|
3170
|
+
status: config.getChannelConfig("discord")?.enabled ? "stopped" : "stopped"
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
id: "slack",
|
|
3174
|
+
name: "Slack",
|
|
3175
|
+
description: "Slack Bot API",
|
|
3176
|
+
icon: "\u{1F4BC}",
|
|
3177
|
+
connected: !!config.getChannelConfig("slack")?.enabled,
|
|
3178
|
+
status: config.getChannelConfig("slack")?.enabled ? "stopped" : "stopped"
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
id: "sms",
|
|
3182
|
+
name: "SMS",
|
|
3183
|
+
description: "Twilio SMS",
|
|
3184
|
+
icon: "\u{1F4F1}",
|
|
3185
|
+
connected: !!config.getCredentials("twilio")?.accountSid,
|
|
3186
|
+
status: config.getCredentials("twilio")?.accountSid ? "stopped" : "stopped"
|
|
3187
|
+
}
|
|
3188
|
+
];
|
|
3189
|
+
return channels;
|
|
3190
|
+
}
|
|
3191
|
+
async function connectChannelWizard(config) {
|
|
3192
|
+
const channel = await p7.select({
|
|
3193
|
+
message: "Which channel do you want to connect?",
|
|
3194
|
+
options: [
|
|
3195
|
+
{ value: "whatsapp", label: "\u{1F4AC} WhatsApp", hint: "Personal or Business" },
|
|
3196
|
+
{ value: "telegram", label: "\u2708\uFE0F Telegram", hint: "Create a bot" },
|
|
3197
|
+
{ value: "discord", label: "\u{1F3AE} Discord", hint: "Server bot" },
|
|
3198
|
+
{ value: "slack", label: "\u{1F4BC} Slack", hint: "Workspace app" },
|
|
3199
|
+
{ value: "sms", label: "\u{1F4F1} SMS", hint: "via Twilio" },
|
|
3200
|
+
{ value: "imessage", label: "\u{1F34E} iMessage", hint: "Mac only" }
|
|
3201
|
+
]
|
|
3202
|
+
});
|
|
3203
|
+
if (p7.isCancel(channel)) return;
|
|
3204
|
+
switch (channel) {
|
|
3205
|
+
case "whatsapp":
|
|
3206
|
+
await connectWhatsApp(config);
|
|
3207
|
+
break;
|
|
3208
|
+
case "telegram":
|
|
3209
|
+
await connectTelegram(config);
|
|
3210
|
+
break;
|
|
3211
|
+
case "discord":
|
|
3212
|
+
await connectDiscord(config);
|
|
3213
|
+
break;
|
|
3214
|
+
case "slack":
|
|
3215
|
+
await connectSlack(config);
|
|
3216
|
+
break;
|
|
3217
|
+
case "sms":
|
|
3218
|
+
await connectSMS(config);
|
|
3219
|
+
break;
|
|
3220
|
+
case "imessage":
|
|
3221
|
+
await connectiMessage(config);
|
|
3222
|
+
break;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
async function connectWhatsApp(config) {
|
|
3226
|
+
p7.log.info(chalk8.bold("WhatsApp Setup"));
|
|
3227
|
+
p7.log.info(chalk8.dim(`
|
|
3228
|
+
WhatsApp uses QR code authentication via Baileys library.
|
|
3229
|
+
Your phone needs to scan a QR code to connect.
|
|
3230
|
+
|
|
3231
|
+
Note: This is for personal accounts. For WhatsApp Business API,
|
|
3232
|
+
you'll need a Meta Business account.
|
|
3233
|
+
`));
|
|
3234
|
+
const proceed = await p7.confirm({
|
|
3235
|
+
message: "Ready to scan QR code?",
|
|
3236
|
+
initialValue: true
|
|
3237
|
+
});
|
|
3238
|
+
if (!proceed || p7.isCancel(proceed)) return;
|
|
3239
|
+
const spinner11 = p7.spinner();
|
|
3240
|
+
spinner11.start("Generating QR code...");
|
|
3241
|
+
try {
|
|
3242
|
+
spinner11.stop("");
|
|
3243
|
+
console.log(chalk8.cyan(`
|
|
3244
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
3245
|
+
\u2551 \u2551
|
|
3246
|
+
\u2551 \u2584\u2584\u2584\u2584\u2584\u2584\u2584 \u2584\u2584\u2584\u2584\u2584 \u2584\u2584\u2584\u2584\u2584\u2584\u2584 \u2551
|
|
3247
|
+
\u2551 \u2588 \u2584\u2584\u2584 \u2588 \u2580\u2588\u2580\u2584\u2588 \u2588 \u2584\u2584\u2584 \u2588 \u2551
|
|
3248
|
+
\u2551 \u2588 \u2588\u2588\u2588 \u2588 \u2584\u2580 \u2580\u2584 \u2588 \u2588\u2588\u2588 \u2588 \u2551
|
|
3249
|
+
\u2551 \u2588\u2584\u2584\u2584\u2584\u2584\u2588 \u2588 \u2584\u2580\u2588 \u2588\u2584\u2584\u2584\u2584\u2584\u2588 \u2551
|
|
3250
|
+
\u2551 \u2584\u2584\u2584\u2584\u2584 \u2584\u2584\u2584\u2588\u2588\u2580\u2580\u2584 \u2584 \u2584 \u2584 \u2551
|
|
3251
|
+
\u2551 \u2588\u2588\u2580\u2580\u2580\u2584\u2584\u2580\u2584\u2580\u2584\u2584\u2588\u2580\u2588\u2588\u2580\u2584\u2588\u2584\u2584 \u2551
|
|
3252
|
+
\u2551 \u2584\u2584\u2584\u2584\u2584\u2584\u2584 \u2588\u2584\u2580\u2584\u2580 \u2584\u2588\u2588\u2584\u2588\u2584 \u2551
|
|
3253
|
+
\u2551 \u2588 \u2584\u2584\u2584 \u2588 \u2588\u2580\u2580\u2584\u2584\u2584\u2584\u2580\u2584 \u2551
|
|
3254
|
+
\u2551 \u2588 \u2588\u2588\u2588 \u2588 \u2584\u2588\u2588\u2588\u2584\u2588\u2580\u2588\u2584\u2584\u2584\u2588 \u2551
|
|
3255
|
+
\u2551 \u2588\u2584\u2584\u2584\u2584\u2584\u2588 \u2588\u2588\u2580\u2584\u2580\u2580\u2584\u2588\u2584\u2580\u2588 \u2551
|
|
3256
|
+
\u2551 \u2551
|
|
3257
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
3258
|
+
|
|
3259
|
+
Scan this QR code with WhatsApp:
|
|
3260
|
+
1. Open WhatsApp on your phone
|
|
3261
|
+
2. Go to Settings > Linked Devices
|
|
3262
|
+
3. Tap "Link a Device"
|
|
3263
|
+
4. Scan the QR code above
|
|
3264
|
+
`));
|
|
3265
|
+
const connected = await p7.confirm({
|
|
3266
|
+
message: "Did you scan the QR code successfully?",
|
|
3267
|
+
initialValue: false
|
|
3268
|
+
});
|
|
3269
|
+
if (connected && !p7.isCancel(connected)) {
|
|
3270
|
+
config.setChannelConfig("whatsapp", { enabled: true });
|
|
3271
|
+
p7.log.success("WhatsApp connected!");
|
|
3272
|
+
}
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
spinner11.stop("Error connecting WhatsApp");
|
|
3275
|
+
p7.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
async function connectTelegram(config) {
|
|
3279
|
+
p7.log.info(chalk8.bold("Telegram Bot Setup"));
|
|
3280
|
+
p7.log.info(chalk8.dim(`
|
|
3281
|
+
To create a Telegram bot:
|
|
3282
|
+
1. Open Telegram and search for @BotFather
|
|
3283
|
+
2. Send /newbot
|
|
3284
|
+
3. Follow the prompts to name your bot
|
|
3285
|
+
4. Copy the API token
|
|
3286
|
+
`));
|
|
3287
|
+
const openBotFather = await p7.confirm({
|
|
3288
|
+
message: "Open BotFather in browser?",
|
|
3289
|
+
initialValue: true
|
|
3290
|
+
});
|
|
3291
|
+
if (openBotFather && !p7.isCancel(openBotFather)) {
|
|
3292
|
+
const open2 = (await import("open")).default;
|
|
3293
|
+
await open2("https://t.me/botfather");
|
|
3294
|
+
}
|
|
3295
|
+
const token = await p7.text({
|
|
3296
|
+
message: "Paste your bot token:",
|
|
3297
|
+
placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
|
|
3298
|
+
validate: (value) => {
|
|
3299
|
+
if (!value) return "Token is required";
|
|
3300
|
+
if (!value.includes(":")) return "Invalid token format";
|
|
3301
|
+
return void 0;
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
if (p7.isCancel(token)) return;
|
|
3305
|
+
const spinner11 = p7.spinner();
|
|
3306
|
+
spinner11.start("Verifying bot token...");
|
|
3307
|
+
try {
|
|
3308
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
3309
|
+
const data = await response.json();
|
|
3310
|
+
if (!data.ok) {
|
|
3311
|
+
throw new Error(data.description || "Invalid token");
|
|
3312
|
+
}
|
|
3313
|
+
spinner11.stop("Bot verified!");
|
|
3314
|
+
config.setChannelConfig("telegram", {
|
|
3315
|
+
enabled: true,
|
|
3316
|
+
botToken: token,
|
|
3317
|
+
botUsername: data.result.username
|
|
3318
|
+
});
|
|
3319
|
+
p7.log.success(`Connected to @${data.result.username}`);
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
spinner11.stop("Verification failed");
|
|
3322
|
+
p7.log.error(error instanceof Error ? error.message : "Invalid token");
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
async function connectDiscord(config) {
|
|
3326
|
+
p7.log.info(chalk8.bold("Discord Bot Setup"));
|
|
3327
|
+
p7.log.info(chalk8.dim(`
|
|
3328
|
+
To create a Discord bot:
|
|
3329
|
+
1. Go to Discord Developer Portal
|
|
3330
|
+
2. Create a new application
|
|
3331
|
+
3. Go to Bot section
|
|
3332
|
+
4. Create a bot and copy the token
|
|
3333
|
+
5. Enable necessary intents (Message Content)
|
|
3334
|
+
`));
|
|
3335
|
+
const openPortal = await p7.confirm({
|
|
3336
|
+
message: "Open Discord Developer Portal?",
|
|
3337
|
+
initialValue: true
|
|
3338
|
+
});
|
|
3339
|
+
if (openPortal && !p7.isCancel(openPortal)) {
|
|
3340
|
+
const open2 = (await import("open")).default;
|
|
3341
|
+
await open2("https://discord.com/developers/applications");
|
|
3342
|
+
}
|
|
3343
|
+
const token = await p7.text({
|
|
3344
|
+
message: "Paste your bot token:",
|
|
3345
|
+
placeholder: "MTIzNDU2Nzg5MDEyMzQ1Njc4.Gg1234.abc..."
|
|
3346
|
+
});
|
|
3347
|
+
if (p7.isCancel(token) || !token) return;
|
|
3348
|
+
const clientId = await p7.text({
|
|
3349
|
+
message: "Paste your application (client) ID:",
|
|
3350
|
+
placeholder: "123456789012345678"
|
|
3351
|
+
});
|
|
3352
|
+
if (p7.isCancel(clientId) || !clientId) return;
|
|
3353
|
+
config.setChannelConfig("discord", {
|
|
3354
|
+
enabled: true,
|
|
3355
|
+
botToken: token,
|
|
3356
|
+
clientId
|
|
3357
|
+
});
|
|
3358
|
+
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=2048&scope=bot`;
|
|
3359
|
+
p7.log.success("Discord bot configured!");
|
|
3360
|
+
p7.log.info(`Add bot to server: ${inviteUrl}`);
|
|
3361
|
+
}
|
|
3362
|
+
async function connectSlack(config) {
|
|
3363
|
+
p7.log.info(chalk8.bold("Slack App Setup"));
|
|
3364
|
+
p7.log.info(chalk8.dim(`
|
|
3365
|
+
To create a Slack app:
|
|
3366
|
+
1. Go to Slack API portal
|
|
3367
|
+
2. Create a new app
|
|
3368
|
+
3. Add bot scopes (chat:write, app_mentions:read, etc.)
|
|
3369
|
+
4. Install to workspace
|
|
3370
|
+
5. Copy the Bot Token
|
|
3371
|
+
`));
|
|
3372
|
+
const openSlack = await p7.confirm({
|
|
3373
|
+
message: "Open Slack API portal?",
|
|
3374
|
+
initialValue: true
|
|
3375
|
+
});
|
|
3376
|
+
if (openSlack && !p7.isCancel(openSlack)) {
|
|
3377
|
+
const open2 = (await import("open")).default;
|
|
3378
|
+
await open2("https://api.slack.com/apps");
|
|
3379
|
+
}
|
|
3380
|
+
const token = await p7.text({
|
|
3381
|
+
message: "Paste your Bot User OAuth Token:",
|
|
3382
|
+
placeholder: "xoxb-...",
|
|
3383
|
+
validate: (value) => {
|
|
3384
|
+
if (!value) return "Token is required";
|
|
3385
|
+
if (!value.startsWith("xoxb-")) return "Should start with xoxb-";
|
|
3386
|
+
return void 0;
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
if (p7.isCancel(token)) return;
|
|
3390
|
+
config.setChannelConfig("slack", {
|
|
3391
|
+
enabled: true,
|
|
3392
|
+
botToken: token
|
|
3393
|
+
});
|
|
3394
|
+
p7.log.success("Slack app configured!");
|
|
3395
|
+
}
|
|
3396
|
+
async function connectSMS(config) {
|
|
3397
|
+
p7.log.info(chalk8.bold("SMS via Twilio"));
|
|
3398
|
+
const twilioConfig = config.getCredentials("twilio");
|
|
3399
|
+
if (twilioConfig?.accountSid) {
|
|
3400
|
+
p7.log.info("Twilio is already configured.");
|
|
3401
|
+
const reconfigure = await p7.confirm({
|
|
3402
|
+
message: "Reconfigure Twilio?",
|
|
3403
|
+
initialValue: false
|
|
3404
|
+
});
|
|
3405
|
+
if (!reconfigure || p7.isCancel(reconfigure)) {
|
|
3406
|
+
config.setChannelConfig("sms", { enabled: true });
|
|
3407
|
+
p7.log.success("SMS channel enabled!");
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
const openTwilio = await p7.confirm({
|
|
3412
|
+
message: "Open Twilio Console?",
|
|
3413
|
+
initialValue: true
|
|
3414
|
+
});
|
|
3415
|
+
if (openTwilio && !p7.isCancel(openTwilio)) {
|
|
3416
|
+
const open2 = (await import("open")).default;
|
|
3417
|
+
await open2("https://console.twilio.com/");
|
|
3418
|
+
}
|
|
3419
|
+
const accountSid = await p7.text({
|
|
3420
|
+
message: "Account SID:",
|
|
3421
|
+
placeholder: "AC..."
|
|
3422
|
+
});
|
|
3423
|
+
if (p7.isCancel(accountSid)) return;
|
|
3424
|
+
const authToken = await p7.text({
|
|
3425
|
+
message: "Auth Token:",
|
|
3426
|
+
placeholder: "..."
|
|
3427
|
+
});
|
|
3428
|
+
if (p7.isCancel(authToken)) return;
|
|
3429
|
+
const phoneNumber = await p7.text({
|
|
3430
|
+
message: "Twilio phone number:",
|
|
3431
|
+
placeholder: "+1234567890"
|
|
3432
|
+
});
|
|
3433
|
+
if (p7.isCancel(phoneNumber)) return;
|
|
3434
|
+
config.setCredentials("twilio", {
|
|
3435
|
+
accountSid,
|
|
3436
|
+
authToken,
|
|
3437
|
+
phoneNumber
|
|
3438
|
+
});
|
|
3439
|
+
config.setChannelConfig("sms", { enabled: true });
|
|
3440
|
+
p7.log.success("SMS channel configured!");
|
|
3441
|
+
}
|
|
3442
|
+
async function connectiMessage(config) {
|
|
3443
|
+
p7.log.warn("iMessage support requires macOS and additional setup.");
|
|
3444
|
+
p7.log.info(chalk8.dim(`
|
|
3445
|
+
iMessage integration uses AppleScript on macOS.
|
|
3446
|
+
This requires:
|
|
3447
|
+
1. macOS with Messages app
|
|
3448
|
+
2. iCloud signed in
|
|
3449
|
+
3. Terminal with accessibility permissions
|
|
3450
|
+
`));
|
|
3451
|
+
const isMac = process.platform === "darwin";
|
|
3452
|
+
if (!isMac) {
|
|
3453
|
+
p7.log.error("iMessage is only available on macOS.");
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
p7.log.info("iMessage support coming soon.");
|
|
3457
|
+
}
|
|
3458
|
+
async function startAllChannels(config) {
|
|
3459
|
+
const spinner11 = p7.spinner();
|
|
3460
|
+
spinner11.start("Starting channel gateway...");
|
|
3461
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3462
|
+
spinner11.stop("Gateway started");
|
|
3463
|
+
console.log(chalk8.green(`
|
|
3464
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
3465
|
+
\u2551 Gateway is running! \u2551
|
|
3466
|
+
\u2551 \u2551
|
|
3467
|
+
\u2551 You can now receive messages from: \u2551
|
|
3468
|
+
\u2551 \u2022 WhatsApp \u2022 Telegram \u2022 Discord \u2551
|
|
3469
|
+
\u2551 \u2022 Slack \u2022 SMS \u2551
|
|
3470
|
+
\u2551 \u2551
|
|
3471
|
+
\u2551 Press Ctrl+C to stop \u2551
|
|
3472
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
3473
|
+
`));
|
|
3474
|
+
}
|
|
3475
|
+
async function stopAllChannels(config) {
|
|
3476
|
+
const spinner11 = p7.spinner();
|
|
3477
|
+
spinner11.start("Stopping gateway...");
|
|
3478
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3479
|
+
spinner11.stop("Gateway stopped");
|
|
3480
|
+
}
|
|
3481
|
+
async function deployGatewayWizard(config) {
|
|
3482
|
+
p7.log.info(chalk8.bold("Deploy Gateway to Cloud"));
|
|
3483
|
+
const platform = await p7.select({
|
|
3484
|
+
message: "Where do you want to deploy?",
|
|
3485
|
+
options: [
|
|
3486
|
+
{ value: "codebakers", label: "\u2601\uFE0F CodeBakers Cloud", hint: "$29/mo - We handle everything" },
|
|
3487
|
+
{ value: "vps", label: "\u{1F5A5}\uFE0F VPS", hint: "$5/mo - DigitalOcean, etc." },
|
|
3488
|
+
{ value: "local", label: "\u{1F3E0} Home server", hint: "Raspberry Pi, etc." }
|
|
3489
|
+
]
|
|
3490
|
+
});
|
|
3491
|
+
if (p7.isCancel(platform)) return;
|
|
3492
|
+
switch (platform) {
|
|
3493
|
+
case "codebakers":
|
|
3494
|
+
p7.log.info("CodeBakers Cloud coming soon!");
|
|
3495
|
+
p7.log.info(chalk8.dim("Sign up at https://codebakers.dev/cloud"));
|
|
3496
|
+
break;
|
|
3497
|
+
case "vps":
|
|
3498
|
+
await deployToVPS(config);
|
|
3499
|
+
break;
|
|
3500
|
+
case "local":
|
|
3501
|
+
p7.log.info("Local deployment guide:");
|
|
3502
|
+
p7.log.info(chalk8.dim(`
|
|
3503
|
+
1. Install Node.js on your server
|
|
3504
|
+
2. Clone your project
|
|
3505
|
+
3. Run: npm install
|
|
3506
|
+
4. Run: pm2 start codebakers -- gateway --start
|
|
3507
|
+
5. Configure firewall to allow outbound connections
|
|
3508
|
+
`));
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
async function deployToVPS(config) {
|
|
3513
|
+
const provider = await p7.select({
|
|
3514
|
+
message: "Which VPS provider?",
|
|
3515
|
+
options: [
|
|
3516
|
+
{ value: "digitalocean", label: "DigitalOcean" },
|
|
3517
|
+
{ value: "vultr", label: "Vultr" },
|
|
3518
|
+
{ value: "linode", label: "Linode" },
|
|
3519
|
+
{ value: "hetzner", label: "Hetzner" },
|
|
3520
|
+
{ value: "other", label: "Other" }
|
|
3521
|
+
]
|
|
3522
|
+
});
|
|
3523
|
+
if (p7.isCancel(provider)) return;
|
|
3524
|
+
if (provider === "digitalocean") {
|
|
3525
|
+
p7.log.info("DigitalOcean auto-deploy:");
|
|
3526
|
+
p7.log.info(chalk8.dim(`
|
|
3527
|
+
1. Opening DigitalOcean login...
|
|
3528
|
+
2. We'll create a $6/mo droplet
|
|
3529
|
+
3. Install CodeBakers gateway
|
|
3530
|
+
4. Connect all your channels
|
|
3531
|
+
`));
|
|
3532
|
+
const proceed = await p7.confirm({
|
|
3533
|
+
message: "Open DigitalOcean?",
|
|
3534
|
+
initialValue: true
|
|
3535
|
+
});
|
|
3536
|
+
if (proceed && !p7.isCancel(proceed)) {
|
|
3537
|
+
const open2 = (await import("open")).default;
|
|
3538
|
+
await open2("https://cloud.digitalocean.com/");
|
|
3539
|
+
}
|
|
3540
|
+
} else {
|
|
3541
|
+
p7.log.info(chalk8.dim(`
|
|
3542
|
+
Manual VPS setup:
|
|
3543
|
+
|
|
3544
|
+
1. Create a VPS (Ubuntu 22.04 recommended)
|
|
3545
|
+
2. SSH into your server
|
|
3546
|
+
3. Run these commands:
|
|
3547
|
+
|
|
3548
|
+
curl -fsSL https://codebakers.dev/install.sh | bash
|
|
3549
|
+
codebakers gateway --start
|
|
3550
|
+
|
|
3551
|
+
4. Your gateway will start automatically on boot
|
|
3552
|
+
`));
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/commands/learn.ts
|
|
3557
|
+
import * as p8 from "@clack/prompts";
|
|
3558
|
+
import chalk9 from "chalk";
|
|
3559
|
+
async function learnCommand() {
|
|
3560
|
+
const config = new Config();
|
|
3561
|
+
const learning = config.getLearning();
|
|
3562
|
+
p8.intro(chalk9.bgCyan.black(" Learning Settings "));
|
|
3563
|
+
console.log(chalk9.bold("\n\u{1F9E0} What CodeBakers has learned about you:\n"));
|
|
3564
|
+
const shortcuts = Object.entries(learning.shortcuts);
|
|
3565
|
+
if (shortcuts.length > 0) {
|
|
3566
|
+
console.log(chalk9.bold("Shortcuts:"));
|
|
3567
|
+
for (const [short, full] of shortcuts) {
|
|
3568
|
+
console.log(` ${chalk9.cyan(short)} \u2192 ${full}`);
|
|
3569
|
+
}
|
|
3570
|
+
console.log("");
|
|
3571
|
+
}
|
|
3572
|
+
const prefs = Object.entries(learning.preferences);
|
|
3573
|
+
if (prefs.length > 0) {
|
|
3574
|
+
console.log(chalk9.bold("Preferences:"));
|
|
3575
|
+
for (const [key, value] of prefs) {
|
|
3576
|
+
console.log(` ${key}: ${chalk9.cyan(String(value))}`);
|
|
3577
|
+
}
|
|
3578
|
+
console.log("");
|
|
3579
|
+
}
|
|
3580
|
+
if (shortcuts.length === 0 && prefs.length === 0) {
|
|
3581
|
+
console.log(chalk9.dim(" Nothing learned yet. Use CodeBakers more!\n"));
|
|
3582
|
+
}
|
|
3583
|
+
p8.outro("");
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// src/commands/security.ts
|
|
3587
|
+
import * as p9 from "@clack/prompts";
|
|
3588
|
+
import chalk10 from "chalk";
|
|
3589
|
+
import * as fs8 from "fs-extra";
|
|
3590
|
+
import glob2 from "fast-glob";
|
|
3591
|
+
import * as path8 from "path";
|
|
3592
|
+
async function securityCommand() {
|
|
3593
|
+
p9.intro(chalk10.bgCyan.black(" Security Audit "));
|
|
3594
|
+
const spinner11 = p9.spinner();
|
|
3595
|
+
spinner11.start("Scanning for security issues...");
|
|
3596
|
+
const issues = await runSecurityScan();
|
|
3597
|
+
spinner11.stop("Scan complete");
|
|
3598
|
+
if (issues.length === 0) {
|
|
3599
|
+
console.log(chalk10.green("\n\u2713 No security issues found!\n"));
|
|
3600
|
+
displaySecurityScore(100);
|
|
3601
|
+
} else {
|
|
3602
|
+
console.log(chalk10.yellow(`
|
|
3603
|
+
\u26A0\uFE0F ${issues.length} security issues found:
|
|
3604
|
+
`));
|
|
3605
|
+
for (const issue of issues) {
|
|
3606
|
+
const icon = issue.severity === "critical" ? chalk10.red("\u{1F534}") : issue.severity === "high" ? chalk10.red("\u{1F7E0}") : chalk10.yellow("\u{1F7E1}");
|
|
3607
|
+
console.log(`${icon} ${chalk10.bold(issue.title)}`);
|
|
3608
|
+
console.log(chalk10.dim(` ${issue.file}:${issue.line}`));
|
|
3609
|
+
console.log(` ${issue.description}
|
|
3610
|
+
`);
|
|
3611
|
+
}
|
|
3612
|
+
const score = Math.max(0, 100 - issues.filter((i) => i.severity === "critical").length * 30 - issues.filter((i) => i.severity === "high").length * 15);
|
|
3613
|
+
displaySecurityScore(score);
|
|
3614
|
+
}
|
|
3615
|
+
p9.outro("");
|
|
3616
|
+
}
|
|
3617
|
+
async function runSecurityScan() {
|
|
3618
|
+
const cwd = process.cwd();
|
|
3619
|
+
const issues = [];
|
|
3620
|
+
const files = await glob2(["src/**/*.{ts,tsx,js,jsx}"], {
|
|
3621
|
+
cwd,
|
|
3622
|
+
ignore: ["**/node_modules/**"]
|
|
3623
|
+
});
|
|
3624
|
+
const patterns = [
|
|
3625
|
+
{
|
|
3626
|
+
pattern: /(sk_live_|sk_test_)[a-zA-Z0-9]+/g,
|
|
3627
|
+
title: "Hardcoded Stripe key",
|
|
3628
|
+
severity: "critical",
|
|
3629
|
+
description: "Stripe API key should be in environment variables"
|
|
3630
|
+
},
|
|
3631
|
+
{
|
|
3632
|
+
pattern: /\beval\s*\(/g,
|
|
3633
|
+
title: "Use of eval()",
|
|
3634
|
+
severity: "critical",
|
|
3635
|
+
description: "eval() is a security vulnerability"
|
|
3636
|
+
},
|
|
3637
|
+
{
|
|
3638
|
+
pattern: /dangerouslySetInnerHTML/g,
|
|
3639
|
+
title: "dangerouslySetInnerHTML",
|
|
3640
|
+
severity: "high",
|
|
3641
|
+
description: "Ensure content is sanitized with DOMPurify"
|
|
3642
|
+
}
|
|
3643
|
+
];
|
|
3644
|
+
for (const file of files) {
|
|
3645
|
+
const content = await fs8.readFile(path8.join(cwd, file), "utf-8");
|
|
3646
|
+
for (const { pattern, title, severity, description } of patterns) {
|
|
3647
|
+
let match;
|
|
3648
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3649
|
+
while ((match = regex.exec(content)) !== null) {
|
|
3650
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
3651
|
+
issues.push({ title, description, file, line, severity });
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return issues;
|
|
3656
|
+
}
|
|
3657
|
+
function displaySecurityScore(score) {
|
|
3658
|
+
const color = score >= 80 ? chalk10.green : score >= 50 ? chalk10.yellow : chalk10.red;
|
|
3659
|
+
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
|
|
3660
|
+
console.log(chalk10.bold("Security Score:"));
|
|
3661
|
+
console.log(color(`
|
|
3662
|
+
${score}/100 Grade: ${grade}
|
|
3663
|
+
`));
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/commands/generate.ts
|
|
3667
|
+
import * as p10 from "@clack/prompts";
|
|
3668
|
+
import chalk11 from "chalk";
|
|
3669
|
+
import * as fs9 from "fs-extra";
|
|
3670
|
+
import * as path9 from "path";
|
|
3671
|
+
async function generateCommand(type) {
|
|
3672
|
+
p10.intro(chalk11.bgCyan.black(" Generate "));
|
|
3673
|
+
const generateType = type || await p10.select({
|
|
3674
|
+
message: "What do you want to generate?",
|
|
3675
|
+
options: [
|
|
3676
|
+
{ value: "component", label: "\u{1F9E9} Component" },
|
|
3677
|
+
{ value: "page", label: "\u{1F4C4} Page" },
|
|
3678
|
+
{ value: "api", label: "\u{1F50C} API Route" },
|
|
3679
|
+
{ value: "hook", label: "\u{1FA9D} Custom Hook" },
|
|
3680
|
+
{ value: "store", label: "\u{1F4E6} Zustand Store" },
|
|
3681
|
+
{ value: "form", label: "\u{1F4DD} Form Component" }
|
|
3682
|
+
]
|
|
3683
|
+
});
|
|
3684
|
+
if (p10.isCancel(generateType)) return;
|
|
3685
|
+
const name = await p10.text({
|
|
3686
|
+
message: "Name:",
|
|
3687
|
+
placeholder: generateType === "page" ? "dashboard" : "MyComponent",
|
|
3688
|
+
validate: (v) => !v ? "Name is required" : void 0
|
|
3689
|
+
});
|
|
3690
|
+
if (p10.isCancel(name)) return;
|
|
3691
|
+
const spinner11 = p10.spinner();
|
|
3692
|
+
spinner11.start("Generating...");
|
|
3693
|
+
await generateFile(generateType, name);
|
|
3694
|
+
spinner11.stop(`Generated ${name}`);
|
|
3695
|
+
p10.outro("");
|
|
3696
|
+
}
|
|
3697
|
+
async function generateFile(type, name) {
|
|
3698
|
+
const cwd = process.cwd();
|
|
3699
|
+
const templates = {
|
|
3700
|
+
component: {
|
|
3701
|
+
path: `src/components/${name}.tsx`,
|
|
3702
|
+
content: `'use client';
|
|
3703
|
+
|
|
3704
|
+
interface ${name}Props {
|
|
3705
|
+
// props
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
export function ${name}({}: ${name}Props) {
|
|
3709
|
+
return (
|
|
3710
|
+
<div>
|
|
3711
|
+
<h2>${name}</h2>
|
|
3712
|
+
</div>
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
`
|
|
3716
|
+
},
|
|
3717
|
+
page: {
|
|
3718
|
+
path: `src/app/${name}/page.tsx`,
|
|
3719
|
+
content: `export default function ${name.charAt(0).toUpperCase() + name.slice(1)}Page() {
|
|
3720
|
+
return (
|
|
3721
|
+
<div className="container mx-auto py-8">
|
|
3722
|
+
<h1 className="text-2xl font-bold">${name}</h1>
|
|
3723
|
+
</div>
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
`
|
|
3727
|
+
},
|
|
3728
|
+
api: {
|
|
3729
|
+
path: `src/app/api/${name}/route.ts`,
|
|
3730
|
+
content: `import { NextRequest, NextResponse } from 'next/server';
|
|
3731
|
+
|
|
3732
|
+
export async function GET(request: NextRequest) {
|
|
3733
|
+
try {
|
|
3734
|
+
return NextResponse.json({ data: [] });
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
export async function POST(request: NextRequest) {
|
|
3741
|
+
try {
|
|
3742
|
+
const body = await request.json();
|
|
3743
|
+
return NextResponse.json({ success: true }, { status: 201 });
|
|
3744
|
+
} catch (error) {
|
|
3745
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
`
|
|
3749
|
+
},
|
|
3750
|
+
hook: {
|
|
3751
|
+
path: `src/hooks/use-${name.toLowerCase()}.ts`,
|
|
3752
|
+
content: `import { useState, useEffect } from 'react';
|
|
3753
|
+
|
|
3754
|
+
export function use${name}() {
|
|
3755
|
+
const [data, setData] = useState(null);
|
|
3756
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
3757
|
+
const [error, setError] = useState<Error | null>(null);
|
|
3758
|
+
|
|
3759
|
+
return { data, isLoading, error };
|
|
3760
|
+
}
|
|
3761
|
+
`
|
|
3762
|
+
},
|
|
3763
|
+
store: {
|
|
3764
|
+
path: `src/stores/${name.toLowerCase()}-store.ts`,
|
|
3765
|
+
content: `import { create } from 'zustand';
|
|
3766
|
+
|
|
3767
|
+
interface ${name}State {
|
|
3768
|
+
items: unknown[];
|
|
3769
|
+
isLoading: boolean;
|
|
3770
|
+
setItems: (items: unknown[]) => void;
|
|
3771
|
+
reset: () => void;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
export const use${name}Store = create<${name}State>((set) => ({
|
|
3775
|
+
items: [],
|
|
3776
|
+
isLoading: false,
|
|
3777
|
+
setItems: (items) => set({ items }),
|
|
3778
|
+
reset: () => set({ items: [], isLoading: false }),
|
|
3779
|
+
}));
|
|
3780
|
+
`
|
|
3781
|
+
},
|
|
3782
|
+
form: {
|
|
3783
|
+
path: `src/components/forms/${name}-form.tsx`,
|
|
3784
|
+
content: `'use client';
|
|
3785
|
+
|
|
3786
|
+
import { useState } from 'react';
|
|
3787
|
+
import { useForm } from 'react-hook-form';
|
|
3788
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3789
|
+
import { z } from 'zod';
|
|
3790
|
+
import { toast } from 'sonner';
|
|
3791
|
+
|
|
3792
|
+
const schema = z.object({
|
|
3793
|
+
name: z.string().min(1, 'Required'),
|
|
3794
|
+
});
|
|
3795
|
+
|
|
3796
|
+
type FormData = z.infer<typeof schema>;
|
|
3797
|
+
|
|
3798
|
+
export function ${name}Form({ onSuccess }: { onSuccess?: () => void }) {
|
|
3799
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
3800
|
+
const form = useForm<FormData>({ resolver: zodResolver(schema) });
|
|
3801
|
+
|
|
3802
|
+
const onSubmit = async (data: FormData) => {
|
|
3803
|
+
setIsSubmitting(true);
|
|
3804
|
+
try {
|
|
3805
|
+
// Submit
|
|
3806
|
+
toast.success('Success!');
|
|
3807
|
+
onSuccess?.();
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
toast.error('Failed');
|
|
3810
|
+
} finally {
|
|
3811
|
+
setIsSubmitting(false);
|
|
3812
|
+
}
|
|
3813
|
+
};
|
|
3814
|
+
|
|
3815
|
+
return (
|
|
3816
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
3817
|
+
{/* Form fields */}
|
|
3818
|
+
<button type="submit" disabled={isSubmitting}>
|
|
3819
|
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
3820
|
+
</button>
|
|
3821
|
+
</form>
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
`
|
|
3825
|
+
}
|
|
3826
|
+
};
|
|
3827
|
+
const template = templates[type];
|
|
3828
|
+
if (!template) throw new Error(`Unknown type: ${type}`);
|
|
3829
|
+
const filePath = path9.join(cwd, template.path);
|
|
3830
|
+
await fs9.ensureDir(path9.dirname(filePath));
|
|
3831
|
+
await fs9.writeFile(filePath, template.content);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// src/commands/fix.ts
|
|
3835
|
+
import * as p11 from "@clack/prompts";
|
|
3836
|
+
import chalk12 from "chalk";
|
|
3837
|
+
async function fixCommand() {
|
|
3838
|
+
p11.intro(chalk12.bgCyan.black(" Auto-Fix "));
|
|
3839
|
+
const spinner11 = p11.spinner();
|
|
3840
|
+
spinner11.start("Analyzing code...");
|
|
3841
|
+
const result = await runPatternCheck(true);
|
|
3842
|
+
if (result.passed) {
|
|
3843
|
+
spinner11.stop("No issues found!");
|
|
3844
|
+
} else {
|
|
3845
|
+
spinner11.stop(`Fixed ${result.violations.length} issues`);
|
|
3846
|
+
}
|
|
3847
|
+
p11.outro(chalk12.green("Done!"));
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// src/commands/design.ts
|
|
3851
|
+
import * as p12 from "@clack/prompts";
|
|
3852
|
+
import chalk13 from "chalk";
|
|
3853
|
+
import * as fs10 from "fs-extra";
|
|
3854
|
+
import * as path10 from "path";
|
|
3855
|
+
var DESIGN_PROFILES = {
|
|
3856
|
+
minimal: {
|
|
3857
|
+
name: "Minimal",
|
|
3858
|
+
inspiration: "Linear, Notion, Vercel",
|
|
3859
|
+
fonts: { heading: "Inter", body: "Inter" },
|
|
3860
|
+
corners: "rounded-md",
|
|
3861
|
+
shadows: "none",
|
|
3862
|
+
spacing: "generous"
|
|
3863
|
+
},
|
|
3864
|
+
bold: {
|
|
3865
|
+
name: "Bold",
|
|
3866
|
+
inspiration: "Stripe, Ramp, Mercury",
|
|
3867
|
+
fonts: { heading: "Plus Jakarta Sans", body: "Inter" },
|
|
3868
|
+
corners: "rounded-2xl",
|
|
3869
|
+
shadows: "elevated",
|
|
3870
|
+
spacing: "generous"
|
|
3871
|
+
},
|
|
3872
|
+
editorial: {
|
|
3873
|
+
name: "Editorial",
|
|
3874
|
+
inspiration: "Medium, Substack, NY Times",
|
|
3875
|
+
fonts: { heading: "Playfair Display", body: "Source Serif Pro" },
|
|
3876
|
+
corners: "rounded-none",
|
|
3877
|
+
shadows: "none",
|
|
3878
|
+
spacing: "reading"
|
|
3879
|
+
},
|
|
3880
|
+
playful: {
|
|
3881
|
+
name: "Playful",
|
|
3882
|
+
inspiration: "Figma, Slack, Notion",
|
|
3883
|
+
fonts: { heading: "Nunito", body: "Nunito" },
|
|
3884
|
+
corners: "rounded-full",
|
|
3885
|
+
shadows: "soft",
|
|
3886
|
+
spacing: "comfortable"
|
|
3887
|
+
},
|
|
3888
|
+
premium: {
|
|
3889
|
+
name: "Premium",
|
|
3890
|
+
inspiration: "Apple, Porsche, Amex",
|
|
3891
|
+
fonts: { heading: "Cormorant Garamond", body: "Lato" },
|
|
3892
|
+
corners: "rounded-lg",
|
|
3893
|
+
shadows: "subtle",
|
|
3894
|
+
spacing: "luxurious"
|
|
3895
|
+
},
|
|
3896
|
+
dashboard: {
|
|
3897
|
+
name: "Dashboard",
|
|
3898
|
+
inspiration: "Datadog, Grafana, Linear",
|
|
3899
|
+
fonts: { heading: "Inter", body: "Inter" },
|
|
3900
|
+
corners: "rounded-md",
|
|
3901
|
+
shadows: "card",
|
|
3902
|
+
spacing: "compact"
|
|
3903
|
+
}
|
|
3904
|
+
};
|
|
3905
|
+
async function designCommand(subcommand) {
|
|
3906
|
+
const config = new Config();
|
|
3907
|
+
if (!config.isInProject()) {
|
|
3908
|
+
p12.log.error("Not in a CodeBakers project. Run `codebakers init` first.");
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
p12.intro(chalk13.bgCyan.black(" Design System "));
|
|
3912
|
+
const action = subcommand || await p12.select({
|
|
3913
|
+
message: "What do you want to do?",
|
|
3914
|
+
options: [
|
|
3915
|
+
{ value: "profile", label: "\u{1F3A8} Set design profile" },
|
|
3916
|
+
{ value: "palette", label: "\u{1F308} Generate color palette" },
|
|
3917
|
+
{ value: "check", label: "\u2705 Check design quality" },
|
|
3918
|
+
{ value: "view", label: "\u{1F440} View current settings" }
|
|
3919
|
+
]
|
|
3920
|
+
});
|
|
3921
|
+
if (p12.isCancel(action)) return;
|
|
3922
|
+
switch (action) {
|
|
3923
|
+
case "profile":
|
|
3924
|
+
await setProfile();
|
|
3925
|
+
break;
|
|
3926
|
+
case "palette":
|
|
3927
|
+
await generatePalette();
|
|
3928
|
+
break;
|
|
3929
|
+
case "check":
|
|
3930
|
+
await checkDesign();
|
|
3931
|
+
break;
|
|
3932
|
+
case "view":
|
|
3933
|
+
await viewSettings();
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
async function setProfile() {
|
|
3938
|
+
const profile = await p12.select({
|
|
3939
|
+
message: "Choose your design profile:",
|
|
3940
|
+
options: Object.entries(DESIGN_PROFILES).map(([key, value]) => ({
|
|
3941
|
+
value: key,
|
|
3942
|
+
label: `${value.name}`,
|
|
3943
|
+
hint: value.inspiration
|
|
3944
|
+
}))
|
|
3945
|
+
});
|
|
3946
|
+
if (p12.isCancel(profile)) return;
|
|
3947
|
+
const selected = DESIGN_PROFILES[profile];
|
|
3948
|
+
const configPath = path10.join(process.cwd(), ".codebakers", "design.json");
|
|
3949
|
+
await fs10.ensureDir(path10.dirname(configPath));
|
|
3950
|
+
await fs10.writeJson(configPath, {
|
|
3951
|
+
profile,
|
|
3952
|
+
...selected,
|
|
3953
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3954
|
+
}, { spaces: 2 });
|
|
3955
|
+
p12.log.success(`Design profile set to ${selected.name}`);
|
|
3956
|
+
console.log(chalk13.dim(`
|
|
3957
|
+
Fonts: ${selected.fonts.heading} / ${selected.fonts.body}
|
|
3958
|
+
Corners: ${selected.corners}
|
|
3959
|
+
Shadows: ${selected.shadows}
|
|
3960
|
+
Spacing: ${selected.spacing}
|
|
3961
|
+
`));
|
|
3962
|
+
}
|
|
3963
|
+
async function generatePalette() {
|
|
3964
|
+
const brandColor = await p12.text({
|
|
3965
|
+
message: "Enter your brand color (hex):",
|
|
3966
|
+
placeholder: "#6366F1",
|
|
3967
|
+
validate: (v) => {
|
|
3968
|
+
if (!v.match(/^#[0-9A-Fa-f]{6}$/)) return "Enter a valid hex color (#RRGGBB)";
|
|
3969
|
+
return void 0;
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
if (p12.isCancel(brandColor)) return;
|
|
3973
|
+
const palette = generateColorPalette(brandColor);
|
|
3974
|
+
const configPath = path10.join(process.cwd(), ".codebakers", "design.json");
|
|
3975
|
+
let config = {};
|
|
3976
|
+
if (await fs10.pathExists(configPath)) {
|
|
3977
|
+
config = await fs10.readJson(configPath);
|
|
3978
|
+
}
|
|
3979
|
+
await fs10.writeJson(configPath, {
|
|
3980
|
+
...config,
|
|
3981
|
+
colors: palette,
|
|
3982
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3983
|
+
}, { spaces: 2 });
|
|
3984
|
+
p12.log.success("Color palette generated!");
|
|
3985
|
+
console.log(`
|
|
3986
|
+
${chalk13.bgHex(palette.brand[500]).black(" Brand ")} ${palette.brand[500]}
|
|
3987
|
+
${chalk13.bgHex(palette.brand[100]).black(" Light ")} ${palette.brand[100]}
|
|
3988
|
+
${chalk13.bgHex(palette.brand[900]).white(" Dark ")} ${palette.brand[900]}
|
|
3989
|
+
${chalk13.bgHex(palette.accent).black(" Accent ")} ${palette.accent}
|
|
3990
|
+
`);
|
|
3991
|
+
}
|
|
3992
|
+
function generateColorPalette(hex) {
|
|
3993
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
3994
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
3995
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
3996
|
+
const max = Math.max(r, g, b);
|
|
3997
|
+
const min = Math.min(r, g, b);
|
|
3998
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
3999
|
+
if (max !== min) {
|
|
4000
|
+
const d = max - min;
|
|
4001
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
4002
|
+
switch (max) {
|
|
4003
|
+
case r:
|
|
4004
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
4005
|
+
break;
|
|
4006
|
+
case g:
|
|
4007
|
+
h = ((b - r) / d + 2) / 6;
|
|
4008
|
+
break;
|
|
4009
|
+
case b:
|
|
4010
|
+
h = ((r - g) / d + 4) / 6;
|
|
4011
|
+
break;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
h = Math.round(h * 360);
|
|
4015
|
+
s = Math.round(s * 100);
|
|
4016
|
+
l = Math.round(l * 100);
|
|
4017
|
+
const hslToHex = (h2, s2, l2) => {
|
|
4018
|
+
l2 /= 100;
|
|
4019
|
+
const a = s2 * Math.min(l2, 1 - l2) / 100;
|
|
4020
|
+
const f = (n) => {
|
|
4021
|
+
const k = (n + h2 / 30) % 12;
|
|
4022
|
+
const color = l2 - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
4023
|
+
return Math.round(255 * color).toString(16).padStart(2, "0");
|
|
4024
|
+
};
|
|
4025
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
4026
|
+
};
|
|
4027
|
+
return {
|
|
4028
|
+
brand: {
|
|
4029
|
+
50: hslToHex(h, s * 0.3, 97),
|
|
4030
|
+
100: hslToHex(h, s * 0.4, 94),
|
|
4031
|
+
200: hslToHex(h, s * 0.5, 86),
|
|
4032
|
+
300: hslToHex(h, s * 0.6, 74),
|
|
4033
|
+
400: hslToHex(h, s * 0.8, 62),
|
|
4034
|
+
500: hex,
|
|
4035
|
+
// Original
|
|
4036
|
+
600: hslToHex(h, s, l * 0.85),
|
|
4037
|
+
700: hslToHex(h, s, l * 0.7),
|
|
4038
|
+
800: hslToHex(h, s, l * 0.55),
|
|
4039
|
+
900: hslToHex(h, s, l * 0.4)
|
|
4040
|
+
},
|
|
4041
|
+
accent: hslToHex((h + 180) % 360, s, l)
|
|
4042
|
+
// Complementary
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
async function checkDesign() {
|
|
4046
|
+
const spinner11 = p12.spinner();
|
|
4047
|
+
spinner11.start("Checking design quality...");
|
|
4048
|
+
const cwd = process.cwd();
|
|
4049
|
+
const issues = [];
|
|
4050
|
+
const glob3 = (await import("fast-glob")).default;
|
|
4051
|
+
const files = await glob3(["src/**/*.{tsx,jsx}"], { cwd });
|
|
4052
|
+
for (const file of files) {
|
|
4053
|
+
const content = await fs10.readFile(path10.join(cwd, file), "utf-8");
|
|
4054
|
+
if (content.includes("bg-gradient-to-r from-blue-500") || content.includes("bg-gradient-to-r from-purple-500")) {
|
|
4055
|
+
issues.push(`${file}: Generic gradient hero detected`);
|
|
4056
|
+
}
|
|
4057
|
+
const iconMatches = content.match(/<(Rocket|Shield|Zap|Star|Check|Lightning)/g);
|
|
4058
|
+
if (iconMatches && iconMatches.length > 3) {
|
|
4059
|
+
issues.push(`${file}: Too many generic icons (${iconMatches.length})`);
|
|
4060
|
+
}
|
|
4061
|
+
if (content.includes(">Get Started<") || content.includes(">Learn More<")) {
|
|
4062
|
+
issues.push(`${file}: Generic CTA text - be more specific`);
|
|
4063
|
+
}
|
|
4064
|
+
if (content.includes("No data") || content.includes("Nothing here")) {
|
|
4065
|
+
issues.push(`${file}: Lazy empty state - add helpful message`);
|
|
4066
|
+
}
|
|
4067
|
+
if (content.includes("py-4") || content.includes("py-6") || content.includes("py-8")) {
|
|
4068
|
+
if (content.includes("<section")) {
|
|
4069
|
+
issues.push(`${file}: Section padding too small - use py-16 or larger`);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
spinner11.stop("Check complete");
|
|
4074
|
+
if (issues.length === 0) {
|
|
4075
|
+
console.log(chalk13.green("\n\u2713 No design issues found!\n"));
|
|
4076
|
+
} else {
|
|
4077
|
+
console.log(chalk13.yellow(`
|
|
4078
|
+
\u26A0\uFE0F ${issues.length} design issues found:
|
|
4079
|
+
`));
|
|
4080
|
+
for (const issue of issues) {
|
|
4081
|
+
console.log(chalk13.dim(` \u2022 ${issue}`));
|
|
4082
|
+
}
|
|
4083
|
+
console.log("");
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
async function viewSettings() {
|
|
4087
|
+
const configPath = path10.join(process.cwd(), ".codebakers", "design.json");
|
|
4088
|
+
if (!await fs10.pathExists(configPath)) {
|
|
4089
|
+
p12.log.info("No design settings configured. Run `codebakers design profile` to set up.");
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
const config = await fs10.readJson(configPath);
|
|
4093
|
+
console.log(chalk13.bold("\nCurrent Design Settings:\n"));
|
|
4094
|
+
console.log(` Profile: ${config.name || config.profile}`);
|
|
4095
|
+
console.log(` Fonts: ${config.fonts?.heading} / ${config.fonts?.body}`);
|
|
4096
|
+
console.log(` Corners: ${config.corners}`);
|
|
4097
|
+
console.log(` Shadows: ${config.shadows}`);
|
|
4098
|
+
console.log(` Spacing: ${config.spacing}`);
|
|
4099
|
+
if (config.colors) {
|
|
4100
|
+
console.log(` Brand: ${config.colors.brand?.[500]}`);
|
|
4101
|
+
console.log(` Accent: ${config.colors.accent}`);
|
|
4102
|
+
}
|
|
4103
|
+
console.log("");
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
// src/index.ts
|
|
4107
|
+
var VERSION2 = "1.0.0";
|
|
4108
|
+
var logo = `
|
|
4109
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
4110
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
4111
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
4112
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551
|
|
4113
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
4114
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
4115
|
+
`;
|
|
4116
|
+
async function showMainMenu() {
|
|
4117
|
+
const config = new Config();
|
|
4118
|
+
const isSetup = config.isConfigured();
|
|
4119
|
+
console.log(gradient.pastel.multiline(logo));
|
|
4120
|
+
console.log(chalk14.dim(` v${VERSION2} \u2014 AI dev team that follows the rules
|
|
4121
|
+
`));
|
|
4122
|
+
if (!isSetup) {
|
|
4123
|
+
console.log(boxen(
|
|
4124
|
+
chalk14.yellow("Welcome to CodeBakers! Let's get you set up."),
|
|
4125
|
+
{ padding: 1, borderColor: "yellow", borderStyle: "round" }
|
|
4126
|
+
));
|
|
4127
|
+
await setupCommand();
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const inProject = config.isInProject();
|
|
4131
|
+
const action = await p13.select({
|
|
4132
|
+
message: "What do you want to do?",
|
|
4133
|
+
options: inProject ? [
|
|
4134
|
+
{ value: "code", label: "\u{1F4AC} Code with AI", hint: "build features, fix bugs" },
|
|
4135
|
+
{ value: "check", label: "\u{1F50D} Check code quality", hint: "run pattern enforcement" },
|
|
4136
|
+
{ value: "deploy", label: "\u{1F680} Deploy", hint: "deploy to Vercel" },
|
|
4137
|
+
{ value: "fix", label: "\u{1F527} Fix errors", hint: "auto-fix build/deploy errors" },
|
|
4138
|
+
{ value: "generate", label: "\u26A1 Generate", hint: "scaffold components, pages" },
|
|
4139
|
+
{ value: "status", label: "\u{1F4CA} Project status", hint: "view project health" },
|
|
4140
|
+
{ value: "separator1", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" },
|
|
4141
|
+
{ value: "connect", label: "\u{1F517} Connect service", hint: "add integrations" },
|
|
4142
|
+
{ value: "gateway", label: "\u{1F4F1} Channel gateway", hint: "WhatsApp, Telegram, etc." },
|
|
4143
|
+
{ value: "learn", label: "\u{1F9E0} Learning settings", hint: "view what I've learned" },
|
|
4144
|
+
{ value: "security", label: "\u{1F512} Security audit", hint: "check for vulnerabilities" },
|
|
4145
|
+
{ value: "design", label: "\u{1F3A8} Design system", hint: "set profile, colors" },
|
|
4146
|
+
{ value: "separator2", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" },
|
|
4147
|
+
{ value: "new", label: "\u{1F195} Create new project" },
|
|
4148
|
+
{ value: "settings", label: "\u2699\uFE0F Settings" },
|
|
4149
|
+
{ value: "help", label: "\u2753 Help" }
|
|
4150
|
+
] : [
|
|
4151
|
+
{ value: "new", label: "\u{1F195} Create new project" },
|
|
4152
|
+
{ value: "open", label: "\u{1F4C2} Open existing project" },
|
|
4153
|
+
{ value: "status", label: "\u{1F4CA} View all projects" },
|
|
4154
|
+
{ value: "separator1", label: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" },
|
|
4155
|
+
{ value: "connect", label: "\u{1F517} Connect service" },
|
|
4156
|
+
{ value: "gateway", label: "\u{1F4F1} Channel gateway", hint: "WhatsApp, Telegram, etc." },
|
|
4157
|
+
{ value: "learn", label: "\u{1F9E0} Learning settings" },
|
|
4158
|
+
{ value: "settings", label: "\u2699\uFE0F Settings" },
|
|
4159
|
+
{ value: "help", label: "\u2753 Help" }
|
|
4160
|
+
]
|
|
4161
|
+
});
|
|
4162
|
+
if (p13.isCancel(action)) {
|
|
4163
|
+
p13.cancel("Goodbye!");
|
|
4164
|
+
process.exit(0);
|
|
4165
|
+
}
|
|
4166
|
+
switch (action) {
|
|
4167
|
+
case "code":
|
|
4168
|
+
await codeCommand();
|
|
4169
|
+
break;
|
|
4170
|
+
case "check":
|
|
4171
|
+
await checkCommand();
|
|
4172
|
+
break;
|
|
4173
|
+
case "deploy":
|
|
4174
|
+
await deployCommand();
|
|
4175
|
+
break;
|
|
4176
|
+
case "fix":
|
|
4177
|
+
await fixCommand();
|
|
4178
|
+
break;
|
|
4179
|
+
case "generate":
|
|
4180
|
+
await generateCommand();
|
|
4181
|
+
break;
|
|
4182
|
+
case "status":
|
|
4183
|
+
await statusCommand();
|
|
4184
|
+
break;
|
|
4185
|
+
case "connect":
|
|
4186
|
+
await connectCommand();
|
|
4187
|
+
break;
|
|
4188
|
+
case "gateway":
|
|
4189
|
+
await gatewayCommand();
|
|
4190
|
+
break;
|
|
4191
|
+
case "learn":
|
|
4192
|
+
await learnCommand();
|
|
4193
|
+
break;
|
|
4194
|
+
case "security":
|
|
4195
|
+
await securityCommand();
|
|
4196
|
+
break;
|
|
4197
|
+
case "design":
|
|
4198
|
+
await designCommand();
|
|
4199
|
+
break;
|
|
4200
|
+
case "new":
|
|
4201
|
+
await initCommand();
|
|
4202
|
+
break;
|
|
4203
|
+
case "settings":
|
|
4204
|
+
await setupCommand();
|
|
4205
|
+
break;
|
|
4206
|
+
case "help":
|
|
4207
|
+
showHelp2();
|
|
4208
|
+
break;
|
|
4209
|
+
default:
|
|
4210
|
+
await showMainMenu();
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
function showHelp2() {
|
|
4214
|
+
console.log(boxen(`
|
|
4215
|
+
${chalk14.bold("CodeBakers CLI")} \u2014 AI dev team that follows the rules
|
|
4216
|
+
|
|
4217
|
+
${chalk14.bold("Commands:")}
|
|
4218
|
+
${chalk14.cyan("codebakers")} Interactive menu (or just run with no args)
|
|
4219
|
+
${chalk14.cyan("codebakers init")} Create a new project
|
|
4220
|
+
${chalk14.cyan("codebakers code")} Start AI coding session
|
|
4221
|
+
${chalk14.cyan("codebakers check")} Run pattern enforcement
|
|
4222
|
+
${chalk14.cyan("codebakers deploy")} Deploy to production
|
|
4223
|
+
${chalk14.cyan("codebakers fix")} Auto-fix errors
|
|
4224
|
+
${chalk14.cyan("codebakers generate")} Generate components/pages
|
|
4225
|
+
${chalk14.cyan("codebakers connect")} Connect external services
|
|
4226
|
+
${chalk14.cyan("codebakers gateway")} Manage messaging channels
|
|
4227
|
+
${chalk14.cyan("codebakers status")} View project status
|
|
4228
|
+
${chalk14.cyan("codebakers security")} Run security audit
|
|
4229
|
+
${chalk14.cyan("codebakers learn")} View/manage learning
|
|
4230
|
+
|
|
4231
|
+
${chalk14.bold("Help at any time:")}
|
|
4232
|
+
Press ${chalk14.yellow("?")} during any command to get contextual help
|
|
4233
|
+
|
|
4234
|
+
${chalk14.bold("Documentation:")}
|
|
4235
|
+
${chalk14.dim("https://codebakers.dev/docs")}
|
|
4236
|
+
`, { padding: 1, borderColor: "cyan", borderStyle: "round" }));
|
|
4237
|
+
}
|
|
4238
|
+
var program = new Command();
|
|
4239
|
+
program.name("codebakers").description("AI dev team that follows the rules").version(VERSION2).action(showMainMenu);
|
|
4240
|
+
program.command("setup").description("Configure CodeBakers (first-time setup)").action(setupCommand);
|
|
4241
|
+
program.command("init").description("Create a new project").option("-n, --name <name>", "Project name").option("-t, --template <template>", "Template (nextjs, voice-agent, saas)").option("--no-git", "Skip GitHub repo creation").option("--no-vercel", "Skip Vercel deployment").option("--no-supabase", "Skip Supabase setup").action(initCommand);
|
|
4242
|
+
program.command("code [prompt]").description("Start AI coding session").option("-w, --watch", "Watch mode - continuous assistance").action(codeCommand);
|
|
4243
|
+
program.command("check").description("Run CodeBakers pattern enforcement").option("-f, --fix", "Auto-fix violations").option("--watch", "Watch mode").action(checkCommand);
|
|
4244
|
+
program.command("deploy").description("Deploy to production").option("-p, --preview", "Deploy to preview only").option("--no-check", "Skip pattern check").action(deployCommand);
|
|
4245
|
+
program.command("fix").description("Auto-fix build and deployment errors").option("--auto", "Fix without asking").action(fixCommand);
|
|
4246
|
+
program.command("generate <type>").alias("g").description("Generate components, pages, API routes").option("-n, --name <name>", "Name of the generated item").action(generateCommand);
|
|
4247
|
+
program.command("connect [service]").description("Connect external services (Stripe, VAPI, etc.)").action(connectCommand);
|
|
4248
|
+
program.command("gateway").description("Manage messaging channel gateway").option("--start", "Start the gateway").option("--stop", "Stop the gateway").option("--status", "Show gateway status").action(gatewayCommand);
|
|
4249
|
+
program.command("status").description("View project status and health").option("-a, --all", "Show all projects").action(statusCommand);
|
|
4250
|
+
program.command("security").description("Run security audit").option("--fix", "Auto-fix security issues").action(securityCommand);
|
|
4251
|
+
program.command("learn").description("View and manage learning settings").option("--forget <item>", "Forget a learned preference").option("--reset", "Reset all learning").option("--export", "Export learned patterns").action(learnCommand);
|
|
4252
|
+
program.command("design [action]").description("Manage design system (profile, palette, check)").action(designCommand);
|
|
4253
|
+
var args = process.argv.slice(2);
|
|
4254
|
+
if (args.length === 0) {
|
|
4255
|
+
checkForUpdates().catch(() => {
|
|
4256
|
+
});
|
|
4257
|
+
showMainMenu().catch(console.error);
|
|
4258
|
+
} else {
|
|
4259
|
+
program.parse();
|
|
4260
|
+
}
|