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
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import * as fs from 'fs-extra';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
+
import { Config } from '../utils/config.js';
|
|
8
|
+
import { VercelService } from '../services/vercel.js';
|
|
9
|
+
import { runPatternCheck } from './check.js';
|
|
10
|
+
|
|
11
|
+
interface DeployOptions {
|
|
12
|
+
preview?: boolean;
|
|
13
|
+
check?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function deployCommand(options: DeployOptions = {}): Promise<void> {
|
|
17
|
+
const config = new Config();
|
|
18
|
+
|
|
19
|
+
if (!config.isInProject()) {
|
|
20
|
+
p.log.error('Not in a CodeBakers project.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
p.intro(chalk.bgCyan.black(' Deploy to Production '));
|
|
25
|
+
|
|
26
|
+
const spinner = p.spinner();
|
|
27
|
+
|
|
28
|
+
// Step 1: Run pattern check (unless skipped)
|
|
29
|
+
if (options.check !== false) {
|
|
30
|
+
spinner.start('Running CodeBakers check...');
|
|
31
|
+
const checkResult = await runPatternCheck(false);
|
|
32
|
+
|
|
33
|
+
if (!checkResult.passed) {
|
|
34
|
+
spinner.stop('');
|
|
35
|
+
const errors = checkResult.violations.filter(v => v.severity === 'error');
|
|
36
|
+
|
|
37
|
+
if (errors.length > 0) {
|
|
38
|
+
p.log.error(`${errors.length} pattern violations found. Fix before deploying.`);
|
|
39
|
+
|
|
40
|
+
const showViolations = await p.confirm({
|
|
41
|
+
message: 'Show violations?',
|
|
42
|
+
initialValue: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (showViolations && !p.isCancel(showViolations)) {
|
|
46
|
+
for (const v of errors) {
|
|
47
|
+
console.log(chalk.red(` ✗ ${v.file}:${v.line} - ${v.message}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const autoFix = await p.confirm({
|
|
52
|
+
message: 'Attempt auto-fix with AI?',
|
|
53
|
+
initialValue: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!autoFix || p.isCancel(autoFix)) {
|
|
57
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Auto-fix with AI
|
|
62
|
+
spinner.start('Auto-fixing with AI...');
|
|
63
|
+
await autoFixWithAI(config, errors);
|
|
64
|
+
spinner.stop('Auto-fix complete');
|
|
65
|
+
|
|
66
|
+
// Re-run check
|
|
67
|
+
const recheck = await runPatternCheck(false);
|
|
68
|
+
if (!recheck.passed) {
|
|
69
|
+
p.log.error('Some violations remain. Manual fix required.');
|
|
70
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
spinner.stop('Pattern check passed');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 2: Run TypeScript check
|
|
79
|
+
spinner.start('Running TypeScript check...');
|
|
80
|
+
try {
|
|
81
|
+
await execa('npx', ['tsc', '--noEmit'], { cwd: process.cwd() });
|
|
82
|
+
spinner.stop('TypeScript check passed');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
spinner.stop('');
|
|
85
|
+
p.log.error('TypeScript errors found.');
|
|
86
|
+
|
|
87
|
+
const fix = await p.confirm({
|
|
88
|
+
message: 'Attempt to fix TypeScript errors?',
|
|
89
|
+
initialValue: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (fix && !p.isCancel(fix)) {
|
|
93
|
+
spinner.start('Fixing TypeScript errors...');
|
|
94
|
+
const fixed = await fixTypeScriptErrors(config);
|
|
95
|
+
spinner.stop(fixed ? 'TypeScript errors fixed' : 'Could not auto-fix all errors');
|
|
96
|
+
|
|
97
|
+
if (!fixed) {
|
|
98
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 3: Run build
|
|
108
|
+
spinner.start('Building project...');
|
|
109
|
+
try {
|
|
110
|
+
await execa('pnpm', ['build'], { cwd: process.cwd() });
|
|
111
|
+
spinner.stop('Build successful');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
spinner.stop('');
|
|
114
|
+
p.log.error('Build failed.');
|
|
115
|
+
|
|
116
|
+
const errorOutput = error instanceof Error ? error.message : 'Unknown error';
|
|
117
|
+
console.log(chalk.dim(errorOutput));
|
|
118
|
+
|
|
119
|
+
const fix = await p.confirm({
|
|
120
|
+
message: 'Attempt to fix build errors with AI?',
|
|
121
|
+
initialValue: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (fix && !p.isCancel(fix)) {
|
|
125
|
+
spinner.start('Fixing build errors...');
|
|
126
|
+
const fixed = await fixBuildErrors(config, errorOutput);
|
|
127
|
+
spinner.stop(fixed ? 'Build errors fixed' : 'Could not auto-fix');
|
|
128
|
+
|
|
129
|
+
if (fixed) {
|
|
130
|
+
// Retry build
|
|
131
|
+
spinner.start('Retrying build...');
|
|
132
|
+
try {
|
|
133
|
+
await execa('pnpm', ['build'], { cwd: process.cwd() });
|
|
134
|
+
spinner.stop('Build successful');
|
|
135
|
+
} catch {
|
|
136
|
+
spinner.stop('Build still failing');
|
|
137
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
p.outro(chalk.red('Deploy cancelled.'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step 4: Git commit if there are changes
|
|
151
|
+
spinner.start('Checking for uncommitted changes...');
|
|
152
|
+
const { stdout: gitStatus } = await execa('git', ['status', '--porcelain'], { cwd: process.cwd() });
|
|
153
|
+
|
|
154
|
+
if (gitStatus.trim()) {
|
|
155
|
+
spinner.stop('');
|
|
156
|
+
const commit = await p.confirm({
|
|
157
|
+
message: 'You have uncommitted changes. Commit before deploying?',
|
|
158
|
+
initialValue: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (commit && !p.isCancel(commit)) {
|
|
162
|
+
const message = await p.text({
|
|
163
|
+
message: 'Commit message:',
|
|
164
|
+
placeholder: 'Pre-deploy changes',
|
|
165
|
+
initialValue: 'Pre-deploy changes',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!p.isCancel(message)) {
|
|
169
|
+
await execa('git', ['add', '.'], { cwd: process.cwd() });
|
|
170
|
+
await execa('git', ['commit', '-m', message as string], { cwd: process.cwd() });
|
|
171
|
+
|
|
172
|
+
spinner.start('Pushing to GitHub...');
|
|
173
|
+
await execa('git', ['push'], { cwd: process.cwd() });
|
|
174
|
+
spinner.stop('Pushed to GitHub');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
spinner.stop('No uncommitted changes');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Step 5: Deploy to Vercel
|
|
182
|
+
const deployType = options.preview ? 'preview' : 'production';
|
|
183
|
+
spinner.start(`Deploying to ${deployType}...`);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const vercel = new VercelService(config);
|
|
187
|
+
const deployment = await vercel.deploy(process.cwd(), !options.preview);
|
|
188
|
+
|
|
189
|
+
spinner.stop('Deployment complete!');
|
|
190
|
+
|
|
191
|
+
console.log(boxedOutput(`
|
|
192
|
+
${chalk.green('✓')} Deployed successfully!
|
|
193
|
+
|
|
194
|
+
${chalk.bold('URL:')} ${deployment.url}
|
|
195
|
+
${chalk.bold('Type:')} ${deployType}
|
|
196
|
+
${chalk.bold('Time:')} ${new Date().toLocaleTimeString()}
|
|
197
|
+
|
|
198
|
+
${chalk.dim('View in Vercel Dashboard:')}
|
|
199
|
+
${chalk.dim(deployment.dashboardUrl || 'https://vercel.com/dashboard')}
|
|
200
|
+
`));
|
|
201
|
+
|
|
202
|
+
} catch (error) {
|
|
203
|
+
spinner.stop('');
|
|
204
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
205
|
+
p.log.error(`Deployment failed: ${errorMsg}`);
|
|
206
|
+
|
|
207
|
+
// Check if it's a build error from Vercel
|
|
208
|
+
if (errorMsg.includes('Build failed')) {
|
|
209
|
+
const retry = await p.confirm({
|
|
210
|
+
message: 'Attempt to fix and retry?',
|
|
211
|
+
initialValue: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (retry && !p.isCancel(retry)) {
|
|
215
|
+
spinner.start('Analyzing Vercel build error...');
|
|
216
|
+
// Would fetch Vercel logs and auto-fix
|
|
217
|
+
spinner.stop('Fix attempted');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
p.outro(chalk.red('Deploy failed.'));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function autoFixWithAI(
|
|
226
|
+
config: Config,
|
|
227
|
+
violations: Array<{ file: string; line: number; rule: string; message: string }>
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
230
|
+
if (!anthropicCreds?.apiKey) {
|
|
231
|
+
throw new Error('Anthropic API key not configured');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const anthropic = new Anthropic({
|
|
235
|
+
apiKey: anthropicCreds.apiKey,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Group by file
|
|
239
|
+
const byFile = new Map<string, typeof violations>();
|
|
240
|
+
for (const v of violations) {
|
|
241
|
+
if (!byFile.has(v.file)) {
|
|
242
|
+
byFile.set(v.file, []);
|
|
243
|
+
}
|
|
244
|
+
byFile.get(v.file)!.push(v);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [file, fileViolations] of byFile) {
|
|
248
|
+
const filePath = path.join(process.cwd(), file);
|
|
249
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
250
|
+
|
|
251
|
+
const prompt = `Fix these CodeBakers pattern violations in this file:
|
|
252
|
+
|
|
253
|
+
Violations:
|
|
254
|
+
${fileViolations.map(v => `- Line ${v.line}: ${v.rule} - ${v.message}`).join('\n')}
|
|
255
|
+
|
|
256
|
+
Current file content:
|
|
257
|
+
\`\`\`typescript
|
|
258
|
+
${content}
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
Output ONLY the corrected file content, no explanations. Keep all existing functionality.`;
|
|
262
|
+
|
|
263
|
+
const response = await anthropic.messages.create({
|
|
264
|
+
model: 'claude-sonnet-4-20250514',
|
|
265
|
+
max_tokens: 8192,
|
|
266
|
+
messages: [{ role: 'user', content: prompt }],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
let fixed = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
270
|
+
|
|
271
|
+
// Extract code from markdown if present
|
|
272
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
273
|
+
if (codeMatch) {
|
|
274
|
+
fixed = codeMatch[1];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await fs.writeFile(filePath, fixed);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function fixTypeScriptErrors(config: Config): Promise<boolean> {
|
|
282
|
+
try {
|
|
283
|
+
// Get TypeScript errors
|
|
284
|
+
const { stderr } = await execa('npx', ['tsc', '--noEmit'], {
|
|
285
|
+
cwd: process.cwd(),
|
|
286
|
+
reject: false,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!stderr) return true;
|
|
290
|
+
|
|
291
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
292
|
+
if (!anthropicCreds?.apiKey) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const anthropic = new Anthropic({
|
|
297
|
+
apiKey: anthropicCreds.apiKey,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Parse error locations
|
|
301
|
+
const errorLines = stderr.split('\n').filter(line => line.includes('error TS'));
|
|
302
|
+
|
|
303
|
+
for (const errorLine of errorLines.slice(0, 10)) { // Limit to 10 errors
|
|
304
|
+
const match = errorLine.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/);
|
|
305
|
+
if (!match) continue;
|
|
306
|
+
|
|
307
|
+
const [, file, line, , , message] = match;
|
|
308
|
+
const filePath = path.join(process.cwd(), file);
|
|
309
|
+
|
|
310
|
+
if (!await fs.pathExists(filePath)) continue;
|
|
311
|
+
|
|
312
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
313
|
+
|
|
314
|
+
const prompt = `Fix this TypeScript error:
|
|
315
|
+
|
|
316
|
+
File: ${file}
|
|
317
|
+
Line: ${line}
|
|
318
|
+
Error: ${message}
|
|
319
|
+
|
|
320
|
+
Current file content:
|
|
321
|
+
\`\`\`typescript
|
|
322
|
+
${content}
|
|
323
|
+
\`\`\`
|
|
324
|
+
|
|
325
|
+
Output ONLY the corrected file content, no explanations.`;
|
|
326
|
+
|
|
327
|
+
const response = await anthropic.messages.create({
|
|
328
|
+
model: 'claude-sonnet-4-20250514',
|
|
329
|
+
max_tokens: 8192,
|
|
330
|
+
messages: [{ role: 'user', content: prompt }],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
let fixed = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
334
|
+
|
|
335
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
336
|
+
if (codeMatch) {
|
|
337
|
+
fixed = codeMatch[1];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await fs.writeFile(filePath, fixed);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function fixBuildErrors(config: Config, errorOutput: string): Promise<boolean> {
|
|
350
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
351
|
+
if (!anthropicCreds?.apiKey) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const anthropic = new Anthropic({
|
|
356
|
+
apiKey: anthropicCreds.apiKey,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Parse error to find file
|
|
360
|
+
const fileMatch = errorOutput.match(/(?:Error|error).*?(?:in|at)\s+(.+?\.tsx?)(?::|$)/);
|
|
361
|
+
if (!fileMatch) return false;
|
|
362
|
+
|
|
363
|
+
const file = fileMatch[1];
|
|
364
|
+
const filePath = path.join(process.cwd(), file);
|
|
365
|
+
|
|
366
|
+
if (!await fs.pathExists(filePath)) return false;
|
|
367
|
+
|
|
368
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
369
|
+
|
|
370
|
+
const prompt = `Fix this build error:
|
|
371
|
+
|
|
372
|
+
Error output:
|
|
373
|
+
${errorOutput}
|
|
374
|
+
|
|
375
|
+
File: ${file}
|
|
376
|
+
|
|
377
|
+
Current file content:
|
|
378
|
+
\`\`\`typescript
|
|
379
|
+
${content}
|
|
380
|
+
\`\`\`
|
|
381
|
+
|
|
382
|
+
Output ONLY the corrected file content, no explanations.`;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const response = await anthropic.messages.create({
|
|
386
|
+
model: 'claude-sonnet-4-20250514',
|
|
387
|
+
max_tokens: 8192,
|
|
388
|
+
messages: [{ role: 'user', content: prompt }],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
let fixed = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
392
|
+
|
|
393
|
+
const codeMatch = fixed.match(/```(?:typescript|tsx?)?\n([\s\S]+?)\n```/);
|
|
394
|
+
if (codeMatch) {
|
|
395
|
+
fixed = codeMatch[1];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await fs.writeFile(filePath, fixed);
|
|
399
|
+
return true;
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function boxedOutput(content: string): string {
|
|
406
|
+
const lines = content.split('\n');
|
|
407
|
+
const maxLength = Math.max(...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
|
|
408
|
+
const border = '─'.repeat(maxLength + 2);
|
|
409
|
+
|
|
410
|
+
return `
|
|
411
|
+
╭${border}╮
|
|
412
|
+
${lines.map(l => `│ ${l.padEnd(maxLength)} │`).join('\n')}
|
|
413
|
+
╰${border}╯`;
|
|
414
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as fs from 'fs-extra';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { Config } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
const DESIGN_PROFILES = {
|
|
8
|
+
minimal: {
|
|
9
|
+
name: 'Minimal',
|
|
10
|
+
inspiration: 'Linear, Notion, Vercel',
|
|
11
|
+
fonts: { heading: 'Inter', body: 'Inter' },
|
|
12
|
+
corners: 'rounded-md',
|
|
13
|
+
shadows: 'none',
|
|
14
|
+
spacing: 'generous',
|
|
15
|
+
},
|
|
16
|
+
bold: {
|
|
17
|
+
name: 'Bold',
|
|
18
|
+
inspiration: 'Stripe, Ramp, Mercury',
|
|
19
|
+
fonts: { heading: 'Plus Jakarta Sans', body: 'Inter' },
|
|
20
|
+
corners: 'rounded-2xl',
|
|
21
|
+
shadows: 'elevated',
|
|
22
|
+
spacing: 'generous',
|
|
23
|
+
},
|
|
24
|
+
editorial: {
|
|
25
|
+
name: 'Editorial',
|
|
26
|
+
inspiration: 'Medium, Substack, NY Times',
|
|
27
|
+
fonts: { heading: 'Playfair Display', body: 'Source Serif Pro' },
|
|
28
|
+
corners: 'rounded-none',
|
|
29
|
+
shadows: 'none',
|
|
30
|
+
spacing: 'reading',
|
|
31
|
+
},
|
|
32
|
+
playful: {
|
|
33
|
+
name: 'Playful',
|
|
34
|
+
inspiration: 'Figma, Slack, Notion',
|
|
35
|
+
fonts: { heading: 'Nunito', body: 'Nunito' },
|
|
36
|
+
corners: 'rounded-full',
|
|
37
|
+
shadows: 'soft',
|
|
38
|
+
spacing: 'comfortable',
|
|
39
|
+
},
|
|
40
|
+
premium: {
|
|
41
|
+
name: 'Premium',
|
|
42
|
+
inspiration: 'Apple, Porsche, Amex',
|
|
43
|
+
fonts: { heading: 'Cormorant Garamond', body: 'Lato' },
|
|
44
|
+
corners: 'rounded-lg',
|
|
45
|
+
shadows: 'subtle',
|
|
46
|
+
spacing: 'luxurious',
|
|
47
|
+
},
|
|
48
|
+
dashboard: {
|
|
49
|
+
name: 'Dashboard',
|
|
50
|
+
inspiration: 'Datadog, Grafana, Linear',
|
|
51
|
+
fonts: { heading: 'Inter', body: 'Inter' },
|
|
52
|
+
corners: 'rounded-md',
|
|
53
|
+
shadows: 'card',
|
|
54
|
+
spacing: 'compact',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function designCommand(subcommand?: string): Promise<void> {
|
|
59
|
+
const config = new Config();
|
|
60
|
+
|
|
61
|
+
if (!config.isInProject()) {
|
|
62
|
+
p.log.error('Not in a CodeBakers project. Run `codebakers init` first.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
p.intro(chalk.bgCyan.black(' Design System '));
|
|
67
|
+
|
|
68
|
+
const action = subcommand || await p.select({
|
|
69
|
+
message: 'What do you want to do?',
|
|
70
|
+
options: [
|
|
71
|
+
{ value: 'profile', label: '🎨 Set design profile' },
|
|
72
|
+
{ value: 'palette', label: '🌈 Generate color palette' },
|
|
73
|
+
{ value: 'check', label: '✅ Check design quality' },
|
|
74
|
+
{ value: 'view', label: '👀 View current settings' },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (p.isCancel(action)) return;
|
|
79
|
+
|
|
80
|
+
switch (action) {
|
|
81
|
+
case 'profile':
|
|
82
|
+
await setProfile();
|
|
83
|
+
break;
|
|
84
|
+
case 'palette':
|
|
85
|
+
await generatePalette();
|
|
86
|
+
break;
|
|
87
|
+
case 'check':
|
|
88
|
+
await checkDesign();
|
|
89
|
+
break;
|
|
90
|
+
case 'view':
|
|
91
|
+
await viewSettings();
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function setProfile(): Promise<void> {
|
|
97
|
+
const profile = await p.select({
|
|
98
|
+
message: 'Choose your design profile:',
|
|
99
|
+
options: Object.entries(DESIGN_PROFILES).map(([key, value]) => ({
|
|
100
|
+
value: key,
|
|
101
|
+
label: `${value.name}`,
|
|
102
|
+
hint: value.inspiration,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (p.isCancel(profile)) return;
|
|
107
|
+
|
|
108
|
+
const selected = DESIGN_PROFILES[profile as keyof typeof DESIGN_PROFILES];
|
|
109
|
+
|
|
110
|
+
// Save to project config
|
|
111
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
112
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
113
|
+
await fs.writeJson(configPath, {
|
|
114
|
+
profile,
|
|
115
|
+
...selected,
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
}, { spaces: 2 });
|
|
118
|
+
|
|
119
|
+
p.log.success(`Design profile set to ${selected.name}`);
|
|
120
|
+
|
|
121
|
+
console.log(chalk.dim(`
|
|
122
|
+
Fonts: ${selected.fonts.heading} / ${selected.fonts.body}
|
|
123
|
+
Corners: ${selected.corners}
|
|
124
|
+
Shadows: ${selected.shadows}
|
|
125
|
+
Spacing: ${selected.spacing}
|
|
126
|
+
`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function generatePalette(): Promise<void> {
|
|
130
|
+
const brandColor = await p.text({
|
|
131
|
+
message: 'Enter your brand color (hex):',
|
|
132
|
+
placeholder: '#6366F1',
|
|
133
|
+
validate: (v) => {
|
|
134
|
+
if (!v.match(/^#[0-9A-Fa-f]{6}$/)) return 'Enter a valid hex color (#RRGGBB)';
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (p.isCancel(brandColor)) return;
|
|
140
|
+
|
|
141
|
+
const palette = generateColorPalette(brandColor as string);
|
|
142
|
+
|
|
143
|
+
// Save to project
|
|
144
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
145
|
+
let config = {};
|
|
146
|
+
if (await fs.pathExists(configPath)) {
|
|
147
|
+
config = await fs.readJson(configPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await fs.writeJson(configPath, {
|
|
151
|
+
...config,
|
|
152
|
+
colors: palette,
|
|
153
|
+
updatedAt: new Date().toISOString(),
|
|
154
|
+
}, { spaces: 2 });
|
|
155
|
+
|
|
156
|
+
p.log.success('Color palette generated!');
|
|
157
|
+
|
|
158
|
+
console.log(`
|
|
159
|
+
${chalk.bgHex(palette.brand[500]).black(' Brand ')} ${palette.brand[500]}
|
|
160
|
+
${chalk.bgHex(palette.brand[100]).black(' Light ')} ${palette.brand[100]}
|
|
161
|
+
${chalk.bgHex(palette.brand[900]).white(' Dark ')} ${palette.brand[900]}
|
|
162
|
+
${chalk.bgHex(palette.accent).black(' Accent ')} ${palette.accent}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function generateColorPalette(hex: string): Record<string, any> {
|
|
167
|
+
// Convert hex to HSL
|
|
168
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
169
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
170
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
171
|
+
|
|
172
|
+
const max = Math.max(r, g, b);
|
|
173
|
+
const min = Math.min(r, g, b);
|
|
174
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
175
|
+
|
|
176
|
+
if (max !== min) {
|
|
177
|
+
const d = max - min;
|
|
178
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
179
|
+
switch (max) {
|
|
180
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
181
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
182
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
h = Math.round(h * 360);
|
|
187
|
+
s = Math.round(s * 100);
|
|
188
|
+
l = Math.round(l * 100);
|
|
189
|
+
|
|
190
|
+
// Generate scale
|
|
191
|
+
const hslToHex = (h: number, s: number, l: number): string => {
|
|
192
|
+
l /= 100;
|
|
193
|
+
const a = s * Math.min(l, 1 - l) / 100;
|
|
194
|
+
const f = (n: number) => {
|
|
195
|
+
const k = (n + h / 30) % 12;
|
|
196
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
197
|
+
return Math.round(255 * color).toString(16).padStart(2, '0');
|
|
198
|
+
};
|
|
199
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
brand: {
|
|
204
|
+
50: hslToHex(h, s * 0.3, 97),
|
|
205
|
+
100: hslToHex(h, s * 0.4, 94),
|
|
206
|
+
200: hslToHex(h, s * 0.5, 86),
|
|
207
|
+
300: hslToHex(h, s * 0.6, 74),
|
|
208
|
+
400: hslToHex(h, s * 0.8, 62),
|
|
209
|
+
500: hex, // Original
|
|
210
|
+
600: hslToHex(h, s, l * 0.85),
|
|
211
|
+
700: hslToHex(h, s, l * 0.7),
|
|
212
|
+
800: hslToHex(h, s, l * 0.55),
|
|
213
|
+
900: hslToHex(h, s, l * 0.4),
|
|
214
|
+
},
|
|
215
|
+
accent: hslToHex((h + 180) % 360, s, l), // Complementary
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function checkDesign(): Promise<void> {
|
|
220
|
+
const spinner = p.spinner();
|
|
221
|
+
spinner.start('Checking design quality...');
|
|
222
|
+
|
|
223
|
+
const cwd = process.cwd();
|
|
224
|
+
const issues: string[] = [];
|
|
225
|
+
|
|
226
|
+
// Check for anti-patterns in code
|
|
227
|
+
const glob = (await import('fast-glob')).default;
|
|
228
|
+
const files = await glob(['src/**/*.{tsx,jsx}'], { cwd });
|
|
229
|
+
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
232
|
+
|
|
233
|
+
// Check for generic gradient hero
|
|
234
|
+
if (content.includes('bg-gradient-to-r from-blue-500') ||
|
|
235
|
+
content.includes('bg-gradient-to-r from-purple-500')) {
|
|
236
|
+
issues.push(`${file}: Generic gradient hero detected`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for icon spam
|
|
240
|
+
const iconMatches = content.match(/<(Rocket|Shield|Zap|Star|Check|Lightning)/g);
|
|
241
|
+
if (iconMatches && iconMatches.length > 3) {
|
|
242
|
+
issues.push(`${file}: Too many generic icons (${iconMatches.length})`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for generic CTAs
|
|
246
|
+
if (content.includes('>Get Started<') || content.includes('>Learn More<')) {
|
|
247
|
+
issues.push(`${file}: Generic CTA text - be more specific`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for lazy empty states
|
|
251
|
+
if (content.includes('No data') || content.includes('Nothing here')) {
|
|
252
|
+
issues.push(`${file}: Lazy empty state - add helpful message`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check for small section padding
|
|
256
|
+
if (content.includes('py-4') || content.includes('py-6') || content.includes('py-8')) {
|
|
257
|
+
if (content.includes('<section')) {
|
|
258
|
+
issues.push(`${file}: Section padding too small - use py-16 or larger`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
spinner.stop('Check complete');
|
|
264
|
+
|
|
265
|
+
if (issues.length === 0) {
|
|
266
|
+
console.log(chalk.green('\n✓ No design issues found!\n'));
|
|
267
|
+
} else {
|
|
268
|
+
console.log(chalk.yellow(`\n⚠️ ${issues.length} design issues found:\n`));
|
|
269
|
+
for (const issue of issues) {
|
|
270
|
+
console.log(chalk.dim(` • ${issue}`));
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function viewSettings(): Promise<void> {
|
|
277
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
278
|
+
|
|
279
|
+
if (!await fs.pathExists(configPath)) {
|
|
280
|
+
p.log.info('No design settings configured. Run `codebakers design profile` to set up.');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const config = await fs.readJson(configPath);
|
|
285
|
+
|
|
286
|
+
console.log(chalk.bold('\nCurrent Design Settings:\n'));
|
|
287
|
+
console.log(` Profile: ${config.name || config.profile}`);
|
|
288
|
+
console.log(` Fonts: ${config.fonts?.heading} / ${config.fonts?.body}`);
|
|
289
|
+
console.log(` Corners: ${config.corners}`);
|
|
290
|
+
console.log(` Shadows: ${config.shadows}`);
|
|
291
|
+
console.log(` Spacing: ${config.spacing}`);
|
|
292
|
+
|
|
293
|
+
if (config.colors) {
|
|
294
|
+
console.log(` Brand: ${config.colors.brand?.[500]}`);
|
|
295
|
+
console.log(` Accent: ${config.colors.accent}`);
|
|
296
|
+
}
|
|
297
|
+
console.log('');
|
|
298
|
+
}
|