@buenojs/bueno 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New Command
|
|
3
|
+
*
|
|
4
|
+
* Create a new Bueno project
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineCommand } from './index';
|
|
8
|
+
import { getOption, hasFlag, getOptionValues, type ParsedArgs } from '../core/args';
|
|
9
|
+
import { cliConsole, colors, printTable } from '../core/console';
|
|
10
|
+
import { prompt, confirm, select, isInteractive } from '../core/prompt';
|
|
11
|
+
import { spinner, runTasks, type TaskOptions } from '../core/spinner';
|
|
12
|
+
import {
|
|
13
|
+
fileExists,
|
|
14
|
+
writeFile,
|
|
15
|
+
createDirectory,
|
|
16
|
+
copyDirectory,
|
|
17
|
+
joinPaths,
|
|
18
|
+
} from '../utils/fs';
|
|
19
|
+
import { kebabCase } from '../utils/strings';
|
|
20
|
+
import { CLIError, CLIErrorType } from '../index';
|
|
21
|
+
import {
|
|
22
|
+
getDockerfileTemplate,
|
|
23
|
+
getDockerignoreTemplate,
|
|
24
|
+
getDockerComposeTemplate,
|
|
25
|
+
getDockerEnvTemplate,
|
|
26
|
+
} from '../templates/docker';
|
|
27
|
+
import {
|
|
28
|
+
type DeployPlatform,
|
|
29
|
+
getDeployTemplate,
|
|
30
|
+
getDeployFilename,
|
|
31
|
+
getDeployPlatformName,
|
|
32
|
+
} from '../templates/deploy';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Project templates
|
|
36
|
+
*/
|
|
37
|
+
type ProjectTemplate = 'default' | 'minimal' | 'fullstack' | 'api';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Frontend frameworks
|
|
41
|
+
*/
|
|
42
|
+
type FrontendFramework = 'react' | 'vue' | 'svelte' | 'solid';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Database drivers
|
|
46
|
+
*/
|
|
47
|
+
type DatabaseDriver = 'sqlite' | 'postgresql' | 'mysql';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Project configuration
|
|
51
|
+
*/
|
|
52
|
+
interface ProjectConfig {
|
|
53
|
+
name: string;
|
|
54
|
+
template: ProjectTemplate;
|
|
55
|
+
framework: FrontendFramework;
|
|
56
|
+
database: DatabaseDriver;
|
|
57
|
+
skipInstall: boolean;
|
|
58
|
+
skipGit: boolean;
|
|
59
|
+
docker: boolean;
|
|
60
|
+
deploy: DeployPlatform[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate project name
|
|
65
|
+
*/
|
|
66
|
+
function validateProjectName(name: string): boolean | string {
|
|
67
|
+
if (!name || name.length === 0) {
|
|
68
|
+
return 'Project name is required';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
72
|
+
return 'Project name can only contain letters, numbers, hyphens, and underscores';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (name.startsWith('-') || name.startsWith('_')) {
|
|
76
|
+
return 'Project name cannot start with a hyphen or underscore';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (name.length > 100) {
|
|
80
|
+
return 'Project name is too long (max 100 characters)';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get package.json template
|
|
88
|
+
*/
|
|
89
|
+
function getPackageJsonTemplate(config: ProjectConfig): string {
|
|
90
|
+
const dependencies: Record<string, string> = {
|
|
91
|
+
bueno: '^0.1.0',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const devDependencies: Record<string, string> = {
|
|
95
|
+
'@types/bun': 'latest',
|
|
96
|
+
typescript: '^5.3.0',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (config.template === 'fullstack' || config.template === 'default') {
|
|
100
|
+
dependencies.zod = '^4.0.0';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const scripts: Record<string, string> = {
|
|
104
|
+
dev: 'bun run --watch server/main.ts',
|
|
105
|
+
build: 'bun build ./server/main.ts --outdir ./dist --target bun',
|
|
106
|
+
start: 'bun run dist/main.js',
|
|
107
|
+
test: 'bun test',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (config.template === 'fullstack') {
|
|
111
|
+
scripts['dev:frontend'] = 'bun run --watch client/index.html';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return JSON.stringify(
|
|
115
|
+
{
|
|
116
|
+
name: kebabCase(config.name),
|
|
117
|
+
version: '0.1.0',
|
|
118
|
+
type: 'module',
|
|
119
|
+
scripts,
|
|
120
|
+
dependencies,
|
|
121
|
+
devDependencies,
|
|
122
|
+
},
|
|
123
|
+
null,
|
|
124
|
+
2,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get tsconfig.json template
|
|
130
|
+
*/
|
|
131
|
+
function getTsConfigTemplate(): string {
|
|
132
|
+
return JSON.stringify(
|
|
133
|
+
{
|
|
134
|
+
compilerOptions: {
|
|
135
|
+
target: 'ESNext',
|
|
136
|
+
module: 'ESNext',
|
|
137
|
+
moduleResolution: 'bundler',
|
|
138
|
+
strict: true,
|
|
139
|
+
skipLibCheck: true,
|
|
140
|
+
esModuleInterop: true,
|
|
141
|
+
allowSyntheticDefaultImports: true,
|
|
142
|
+
jsx: 'react-jsx',
|
|
143
|
+
paths: {
|
|
144
|
+
bueno: ['./node_modules/bueno/dist/index.d.ts'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
include: ['server/**/*', 'client/**/*'],
|
|
148
|
+
exclude: ['node_modules', 'dist'],
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get main.ts template
|
|
157
|
+
*/
|
|
158
|
+
function getMainTemplate(config: ProjectConfig): string {
|
|
159
|
+
if (config.template === 'minimal') {
|
|
160
|
+
return `import { createServer } from 'bueno';
|
|
161
|
+
|
|
162
|
+
const app = createServer();
|
|
163
|
+
|
|
164
|
+
app.router.get('/', () => {
|
|
165
|
+
return { message: 'Hello, Bueno!' };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await app.listen(3000);
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `import { createApp, Module, Controller, Get, Injectable } from 'bueno';
|
|
173
|
+
import type { Context } from 'bueno';
|
|
174
|
+
|
|
175
|
+
// Services
|
|
176
|
+
@Injectable()
|
|
177
|
+
export class AppService {
|
|
178
|
+
findAll() {
|
|
179
|
+
return { message: 'Welcome to Bueno!', items: [] };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Controllers
|
|
184
|
+
@Controller()
|
|
185
|
+
export class AppController {
|
|
186
|
+
constructor(private readonly appService: AppService) {}
|
|
187
|
+
|
|
188
|
+
@Get()
|
|
189
|
+
findAll(ctx: Context) {
|
|
190
|
+
return this.appService.findAll();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Get('health')
|
|
194
|
+
health(ctx: Context) {
|
|
195
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Module
|
|
200
|
+
@Module({
|
|
201
|
+
controllers: [AppController],
|
|
202
|
+
providers: [AppService],
|
|
203
|
+
})
|
|
204
|
+
export class AppModule {}
|
|
205
|
+
|
|
206
|
+
// Bootstrap
|
|
207
|
+
const app = createApp(AppModule);
|
|
208
|
+
await app.listen(3000);
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get bueno.config.ts template
|
|
214
|
+
*/
|
|
215
|
+
function getConfigTemplate(config: ProjectConfig): string {
|
|
216
|
+
const dbConfig = config.database === 'sqlite'
|
|
217
|
+
? `{ url: 'sqlite:./data.db' }`
|
|
218
|
+
: `{ url: process.env.DATABASE_URL ?? '${config.database}://localhost/${kebabCase(config.name)}' }`;
|
|
219
|
+
|
|
220
|
+
return `import { defineConfig } from 'bueno';
|
|
221
|
+
|
|
222
|
+
export default defineConfig({
|
|
223
|
+
server: {
|
|
224
|
+
port: 3000,
|
|
225
|
+
host: 'localhost',
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
database: ${dbConfig},
|
|
229
|
+
|
|
230
|
+
logger: {
|
|
231
|
+
level: 'info',
|
|
232
|
+
pretty: true,
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
health: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
healthPath: '/health',
|
|
238
|
+
readyPath: '/ready',
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get .env.example template
|
|
246
|
+
*/
|
|
247
|
+
function getEnvExampleTemplate(config: ProjectConfig): string {
|
|
248
|
+
if (config.database === 'sqlite') {
|
|
249
|
+
return `# Bueno Environment Variables
|
|
250
|
+
NODE_ENV=development
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return `# Bueno Environment Variables
|
|
255
|
+
NODE_ENV=development
|
|
256
|
+
DATABASE_URL=${config.database}://user:password@localhost:5432/${kebabCase(config.name)}
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get .gitignore template
|
|
262
|
+
*/
|
|
263
|
+
function getGitignoreTemplate(): string {
|
|
264
|
+
return `# Dependencies
|
|
265
|
+
node_modules/
|
|
266
|
+
|
|
267
|
+
# Build output
|
|
268
|
+
dist/
|
|
269
|
+
|
|
270
|
+
# Environment files
|
|
271
|
+
.env
|
|
272
|
+
.env.local
|
|
273
|
+
.env.*.local
|
|
274
|
+
|
|
275
|
+
# IDE
|
|
276
|
+
.idea/
|
|
277
|
+
.vscode/
|
|
278
|
+
*.swp
|
|
279
|
+
*.swo
|
|
280
|
+
|
|
281
|
+
# OS
|
|
282
|
+
.DS_Store
|
|
283
|
+
Thumbs.db
|
|
284
|
+
|
|
285
|
+
# Logs
|
|
286
|
+
*.log
|
|
287
|
+
logs/
|
|
288
|
+
|
|
289
|
+
# Database
|
|
290
|
+
*.db
|
|
291
|
+
*.sqlite
|
|
292
|
+
*.sqlite3
|
|
293
|
+
|
|
294
|
+
# Test coverage
|
|
295
|
+
coverage/
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get README.md template
|
|
301
|
+
*/
|
|
302
|
+
function getReadmeTemplate(config: ProjectConfig): string {
|
|
303
|
+
return `# ${config.name}
|
|
304
|
+
|
|
305
|
+
A Bueno application.
|
|
306
|
+
|
|
307
|
+
## Getting Started
|
|
308
|
+
|
|
309
|
+
\`\`\`bash
|
|
310
|
+
# Install dependencies
|
|
311
|
+
bun install
|
|
312
|
+
|
|
313
|
+
# Start development server
|
|
314
|
+
bun run dev
|
|
315
|
+
|
|
316
|
+
# Build for production
|
|
317
|
+
bun run build
|
|
318
|
+
|
|
319
|
+
# Start production server
|
|
320
|
+
bun run start
|
|
321
|
+
\`\`\`
|
|
322
|
+
|
|
323
|
+
## Project Structure
|
|
324
|
+
|
|
325
|
+
\`\`\`
|
|
326
|
+
├── server/ # Server-side code
|
|
327
|
+
│ ├── main.ts # Entry point
|
|
328
|
+
│ ├── modules/ # Feature modules
|
|
329
|
+
│ └── database/ # Database files
|
|
330
|
+
├── client/ # Client-side code (if applicable)
|
|
331
|
+
├── tests/ # Test files
|
|
332
|
+
└── bueno.config.ts # Configuration
|
|
333
|
+
\`\`\`
|
|
334
|
+
|
|
335
|
+
## Learn More
|
|
336
|
+
|
|
337
|
+
- [Bueno Documentation](https://github.com/sivaraj/bueno#readme)
|
|
338
|
+
- [Bun Documentation](https://bun.sh/docs)
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create project files
|
|
344
|
+
*/
|
|
345
|
+
async function createProjectFiles(
|
|
346
|
+
projectPath: string,
|
|
347
|
+
config: ProjectConfig,
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
const tasks: TaskOptions[] = [];
|
|
350
|
+
|
|
351
|
+
// Create directories
|
|
352
|
+
tasks.push({
|
|
353
|
+
text: 'Creating project structure',
|
|
354
|
+
task: async () => {
|
|
355
|
+
await createDirectory(joinPaths(projectPath, 'server', 'modules', 'app'));
|
|
356
|
+
await createDirectory(joinPaths(projectPath, 'server', 'common', 'middleware'));
|
|
357
|
+
await createDirectory(joinPaths(projectPath, 'server', 'common', 'guards'));
|
|
358
|
+
await createDirectory(joinPaths(projectPath, 'server', 'common', 'interceptors'));
|
|
359
|
+
await createDirectory(joinPaths(projectPath, 'server', 'common', 'pipes'));
|
|
360
|
+
await createDirectory(joinPaths(projectPath, 'server', 'common', 'filters'));
|
|
361
|
+
await createDirectory(joinPaths(projectPath, 'server', 'database', 'migrations'));
|
|
362
|
+
await createDirectory(joinPaths(projectPath, 'server', 'config'));
|
|
363
|
+
await createDirectory(joinPaths(projectPath, 'tests', 'unit'));
|
|
364
|
+
await createDirectory(joinPaths(projectPath, 'tests', 'integration'));
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Create package.json
|
|
369
|
+
tasks.push({
|
|
370
|
+
text: 'Creating package.json',
|
|
371
|
+
task: async () => {
|
|
372
|
+
await writeFile(
|
|
373
|
+
joinPaths(projectPath, 'package.json'),
|
|
374
|
+
getPackageJsonTemplate(config),
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Create tsconfig.json
|
|
380
|
+
tasks.push({
|
|
381
|
+
text: 'Creating tsconfig.json',
|
|
382
|
+
task: async () => {
|
|
383
|
+
await writeFile(
|
|
384
|
+
joinPaths(projectPath, 'tsconfig.json'),
|
|
385
|
+
getTsConfigTemplate(),
|
|
386
|
+
);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Create main.ts
|
|
391
|
+
tasks.push({
|
|
392
|
+
text: 'Creating server/main.ts',
|
|
393
|
+
task: async () => {
|
|
394
|
+
await writeFile(
|
|
395
|
+
joinPaths(projectPath, 'server', 'main.ts'),
|
|
396
|
+
getMainTemplate(config),
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Create bueno.config.ts
|
|
402
|
+
tasks.push({
|
|
403
|
+
text: 'Creating bueno.config.ts',
|
|
404
|
+
task: async () => {
|
|
405
|
+
await writeFile(
|
|
406
|
+
joinPaths(projectPath, 'bueno.config.ts'),
|
|
407
|
+
getConfigTemplate(config),
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Create .env.example
|
|
413
|
+
tasks.push({
|
|
414
|
+
text: 'Creating .env.example',
|
|
415
|
+
task: async () => {
|
|
416
|
+
await writeFile(
|
|
417
|
+
joinPaths(projectPath, '.env.example'),
|
|
418
|
+
getEnvExampleTemplate(config),
|
|
419
|
+
);
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Create .gitignore
|
|
424
|
+
tasks.push({
|
|
425
|
+
text: 'Creating .gitignore',
|
|
426
|
+
task: async () => {
|
|
427
|
+
await writeFile(
|
|
428
|
+
joinPaths(projectPath, '.gitignore'),
|
|
429
|
+
getGitignoreTemplate(),
|
|
430
|
+
);
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Create README.md
|
|
435
|
+
tasks.push({
|
|
436
|
+
text: 'Creating README.md',
|
|
437
|
+
task: async () => {
|
|
438
|
+
await writeFile(
|
|
439
|
+
joinPaths(projectPath, 'README.md'),
|
|
440
|
+
getReadmeTemplate(config),
|
|
441
|
+
);
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Create Docker files if enabled
|
|
446
|
+
if (config.docker) {
|
|
447
|
+
tasks.push({
|
|
448
|
+
text: 'Creating Dockerfile',
|
|
449
|
+
task: async () => {
|
|
450
|
+
await writeFile(
|
|
451
|
+
joinPaths(projectPath, 'Dockerfile'),
|
|
452
|
+
getDockerfileTemplate(config.name, config.database),
|
|
453
|
+
);
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
tasks.push({
|
|
458
|
+
text: 'Creating .dockerignore',
|
|
459
|
+
task: async () => {
|
|
460
|
+
await writeFile(
|
|
461
|
+
joinPaths(projectPath, '.dockerignore'),
|
|
462
|
+
getDockerignoreTemplate(),
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
tasks.push({
|
|
468
|
+
text: 'Creating docker-compose.yml',
|
|
469
|
+
task: async () => {
|
|
470
|
+
await writeFile(
|
|
471
|
+
joinPaths(projectPath, 'docker-compose.yml'),
|
|
472
|
+
getDockerComposeTemplate(config.name, config.database),
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
tasks.push({
|
|
478
|
+
text: 'Creating .env.docker',
|
|
479
|
+
task: async () => {
|
|
480
|
+
await writeFile(
|
|
481
|
+
joinPaths(projectPath, '.env.docker'),
|
|
482
|
+
getDockerEnvTemplate(config.name, config.database),
|
|
483
|
+
);
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Create deployment configuration files
|
|
489
|
+
for (const platform of config.deploy) {
|
|
490
|
+
const filename = getDeployFilename(platform);
|
|
491
|
+
tasks.push({
|
|
492
|
+
text: `Creating ${filename} for ${getDeployPlatformName(platform)}`,
|
|
493
|
+
task: async () => {
|
|
494
|
+
await writeFile(
|
|
495
|
+
joinPaths(projectPath, filename),
|
|
496
|
+
getDeployTemplate(platform, config.name, config.database),
|
|
497
|
+
);
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await runTasks(tasks);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Handle new command
|
|
507
|
+
*/
|
|
508
|
+
async function handleNew(args: ParsedArgs): Promise<void> {
|
|
509
|
+
// Get project name
|
|
510
|
+
let name = args.positionals[0];
|
|
511
|
+
const useDefaults = hasFlag(args, 'yes') || hasFlag(args, 'y');
|
|
512
|
+
|
|
513
|
+
// Interactive prompts if no name provided
|
|
514
|
+
if (!name && isInteractive()) {
|
|
515
|
+
name = await prompt('Project name:', {
|
|
516
|
+
validate: validateProjectName,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!name) {
|
|
521
|
+
throw new CLIError(
|
|
522
|
+
'Project name is required. Usage: bueno new <project-name>',
|
|
523
|
+
CLIErrorType.INVALID_ARGS,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const validation = validateProjectName(name);
|
|
528
|
+
if (validation !== true) {
|
|
529
|
+
throw new CLIError(validation as string, CLIErrorType.INVALID_ARGS);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Get options
|
|
533
|
+
let template = getOption(args, 'template', {
|
|
534
|
+
name: 'template',
|
|
535
|
+
alias: 't',
|
|
536
|
+
type: 'string',
|
|
537
|
+
description: '',
|
|
538
|
+
}) as ProjectTemplate;
|
|
539
|
+
|
|
540
|
+
let framework = getOption(args, 'framework', {
|
|
541
|
+
name: 'framework',
|
|
542
|
+
alias: 'f',
|
|
543
|
+
type: 'string',
|
|
544
|
+
description: '',
|
|
545
|
+
}) as FrontendFramework;
|
|
546
|
+
|
|
547
|
+
let database = getOption(args, 'database', {
|
|
548
|
+
name: 'database',
|
|
549
|
+
alias: 'd',
|
|
550
|
+
type: 'string',
|
|
551
|
+
description: '',
|
|
552
|
+
}) as DatabaseDriver;
|
|
553
|
+
|
|
554
|
+
const skipInstall = hasFlag(args, 'skip-install');
|
|
555
|
+
const skipGit = hasFlag(args, 'skip-git');
|
|
556
|
+
const docker = hasFlag(args, 'docker');
|
|
557
|
+
|
|
558
|
+
// Get deployment platforms (can be specified multiple times)
|
|
559
|
+
const deployPlatforms = getOptionValues(args, 'deploy');
|
|
560
|
+
const validPlatforms: DeployPlatform[] = ['render', 'fly', 'railway'];
|
|
561
|
+
const deploy: DeployPlatform[] = [];
|
|
562
|
+
|
|
563
|
+
for (const platform of deployPlatforms) {
|
|
564
|
+
if (validPlatforms.includes(platform as DeployPlatform)) {
|
|
565
|
+
if (!deploy.includes(platform as DeployPlatform)) {
|
|
566
|
+
deploy.push(platform as DeployPlatform);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
throw new CLIError(
|
|
570
|
+
`Invalid deployment platform: ${platform}. Valid options are: ${validPlatforms.join(', ')}`,
|
|
571
|
+
CLIErrorType.INVALID_ARGS,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Interactive prompts for missing options
|
|
577
|
+
if (!useDefaults && isInteractive()) {
|
|
578
|
+
if (!template) {
|
|
579
|
+
template = await select<ProjectTemplate>(
|
|
580
|
+
'Select a template:',
|
|
581
|
+
[
|
|
582
|
+
{ value: 'default', name: 'Default - Standard project with modules and database' },
|
|
583
|
+
{ value: 'minimal', name: 'Minimal - Bare minimum project structure' },
|
|
584
|
+
{ value: 'fullstack', name: 'Fullstack - Full-stack project with SSR and auth' },
|
|
585
|
+
{ value: 'api', name: 'API - API-only project without frontend' },
|
|
586
|
+
],
|
|
587
|
+
{ default: 'default' },
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if ((template === 'fullstack' || template === 'default') && !framework) {
|
|
592
|
+
framework = await select<FrontendFramework>(
|
|
593
|
+
'Select a frontend framework:',
|
|
594
|
+
[
|
|
595
|
+
{ value: 'react', name: 'React' },
|
|
596
|
+
{ value: 'vue', name: 'Vue' },
|
|
597
|
+
{ value: 'svelte', name: 'Svelte' },
|
|
598
|
+
{ value: 'solid', name: 'Solid' },
|
|
599
|
+
],
|
|
600
|
+
{ default: 'react' },
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!database) {
|
|
605
|
+
database = await select<DatabaseDriver>(
|
|
606
|
+
'Select a database:',
|
|
607
|
+
[
|
|
608
|
+
{ value: 'sqlite', name: 'SQLite - Local file-based database' },
|
|
609
|
+
{ value: 'postgresql', name: 'PostgreSQL - Production-ready relational database' },
|
|
610
|
+
{ value: 'mysql', name: 'MySQL - Popular relational database' },
|
|
611
|
+
],
|
|
612
|
+
{ default: 'sqlite' },
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Set defaults
|
|
618
|
+
template = template || 'default';
|
|
619
|
+
framework = framework || 'react';
|
|
620
|
+
database = database || 'sqlite';
|
|
621
|
+
|
|
622
|
+
const config: ProjectConfig = {
|
|
623
|
+
name,
|
|
624
|
+
template,
|
|
625
|
+
framework,
|
|
626
|
+
database,
|
|
627
|
+
skipInstall,
|
|
628
|
+
skipGit,
|
|
629
|
+
docker,
|
|
630
|
+
deploy,
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Check if directory exists
|
|
634
|
+
const projectPath = joinPaths(process.cwd(), kebabCase(name));
|
|
635
|
+
if (await fileExists(projectPath)) {
|
|
636
|
+
throw new CLIError(
|
|
637
|
+
`Directory already exists: ${kebabCase(name)}`,
|
|
638
|
+
CLIErrorType.FILE_EXISTS,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Display project info
|
|
643
|
+
cliConsole.header(`Creating a new Bueno project: ${colors.cyan(name)}`);
|
|
644
|
+
|
|
645
|
+
const rows = [
|
|
646
|
+
['Template', template],
|
|
647
|
+
['Framework', framework],
|
|
648
|
+
['Database', database],
|
|
649
|
+
['Docker', docker ? colors.green('Yes') : colors.red('No')],
|
|
650
|
+
['Deploy', deploy.length > 0 ? colors.green(deploy.map(getDeployPlatformName).join(', ')) : colors.red('None')],
|
|
651
|
+
['Install dependencies', skipInstall ? colors.red('No') : colors.green('Yes')],
|
|
652
|
+
['Initialize git', skipGit ? colors.red('No') : colors.green('Yes')],
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
printTable(['Setting', 'Value'], rows);
|
|
656
|
+
cliConsole.log('');
|
|
657
|
+
|
|
658
|
+
// Create project
|
|
659
|
+
cliConsole.subheader('Creating project files...');
|
|
660
|
+
await createProjectFiles(projectPath, config);
|
|
661
|
+
|
|
662
|
+
// Install dependencies
|
|
663
|
+
if (!skipInstall) {
|
|
664
|
+
cliConsole.subheader('Installing dependencies...');
|
|
665
|
+
const installSpinner = spinner('Running bun install...');
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const proc = Bun.spawn(['bun', 'install'], {
|
|
669
|
+
cwd: projectPath,
|
|
670
|
+
stdout: 'pipe',
|
|
671
|
+
stderr: 'pipe',
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const exitCode = await proc.exited;
|
|
675
|
+
|
|
676
|
+
if (exitCode === 0) {
|
|
677
|
+
installSpinner.success('Dependencies installed');
|
|
678
|
+
} else {
|
|
679
|
+
installSpinner.warn('Failed to install dependencies. Run `bun install` manually.');
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
installSpinner.warn('Failed to install dependencies. Run `bun install` manually.');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Initialize git
|
|
687
|
+
if (!skipGit) {
|
|
688
|
+
cliConsole.subheader('Initializing git repository...');
|
|
689
|
+
const gitSpinner = spinner('Running git init...');
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
const proc = Bun.spawn(['git', 'init'], {
|
|
693
|
+
cwd: projectPath,
|
|
694
|
+
stdout: 'pipe',
|
|
695
|
+
stderr: 'pipe',
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const exitCode = await proc.exited;
|
|
699
|
+
|
|
700
|
+
if (exitCode === 0) {
|
|
701
|
+
// Add all files
|
|
702
|
+
Bun.spawn(['git', 'add', '.'], { cwd: projectPath });
|
|
703
|
+
Bun.spawn(['git', 'commit', '-m', 'Initial commit from Bueno CLI'], {
|
|
704
|
+
cwd: projectPath,
|
|
705
|
+
});
|
|
706
|
+
gitSpinner.success('Git repository initialized');
|
|
707
|
+
} else {
|
|
708
|
+
gitSpinner.warn('Failed to initialize git. Run `git init` manually.');
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
gitSpinner.warn('Failed to initialize git. Run `git init` manually.');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Show success message
|
|
716
|
+
cliConsole.log('');
|
|
717
|
+
cliConsole.success(`Project created successfully!`);
|
|
718
|
+
cliConsole.log('');
|
|
719
|
+
cliConsole.log('Next steps:');
|
|
720
|
+
cliConsole.log(` ${colors.cyan(`cd ${kebabCase(name)}`)}`);
|
|
721
|
+
cliConsole.log(` ${colors.cyan('bun run dev')}`);
|
|
722
|
+
cliConsole.log('');
|
|
723
|
+
cliConsole.log(`Documentation: ${colors.dim('https://github.com/sivaraj/bueno')}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Register the command
|
|
727
|
+
defineCommand(
|
|
728
|
+
{
|
|
729
|
+
name: 'new',
|
|
730
|
+
description: 'Create a new Bueno project',
|
|
731
|
+
positionals: [
|
|
732
|
+
{
|
|
733
|
+
name: 'name',
|
|
734
|
+
required: false,
|
|
735
|
+
description: 'Project name',
|
|
736
|
+
},
|
|
737
|
+
],
|
|
738
|
+
options: [
|
|
739
|
+
{
|
|
740
|
+
name: 'template',
|
|
741
|
+
alias: 't',
|
|
742
|
+
type: 'string',
|
|
743
|
+
description: 'Project template (default, minimal, fullstack, api)',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: 'framework',
|
|
747
|
+
alias: 'f',
|
|
748
|
+
type: 'string',
|
|
749
|
+
description: 'Frontend framework (react, vue, svelte, solid)',
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'database',
|
|
753
|
+
alias: 'd',
|
|
754
|
+
type: 'string',
|
|
755
|
+
description: 'Database driver (sqlite, postgresql, mysql)',
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: 'skip-install',
|
|
759
|
+
type: 'boolean',
|
|
760
|
+
default: false,
|
|
761
|
+
description: 'Skip dependency installation',
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: 'skip-git',
|
|
765
|
+
type: 'boolean',
|
|
766
|
+
default: false,
|
|
767
|
+
description: 'Skip git initialization',
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'docker',
|
|
771
|
+
type: 'boolean',
|
|
772
|
+
default: false,
|
|
773
|
+
description: 'Include Docker configuration (Dockerfile, docker-compose.yml)',
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: 'deploy',
|
|
777
|
+
type: 'string',
|
|
778
|
+
description: 'Deployment platform configuration (render, fly, railway). Can be specified multiple times.',
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: 'yes',
|
|
782
|
+
alias: 'y',
|
|
783
|
+
type: 'boolean',
|
|
784
|
+
default: false,
|
|
785
|
+
description: 'Use default options without prompts',
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
examples: [
|
|
789
|
+
'bueno new my-app',
|
|
790
|
+
'bueno new my-api --template api',
|
|
791
|
+
'bueno new my-fullstack --template fullstack --framework react',
|
|
792
|
+
'bueno new my-project --database postgresql',
|
|
793
|
+
'bueno new my-app --docker',
|
|
794
|
+
'bueno new my-app --docker --database postgresql',
|
|
795
|
+
'bueno new my-app --deploy render',
|
|
796
|
+
'bueno new my-app --deploy fly',
|
|
797
|
+
'bueno new my-app --deploy render --deploy fly',
|
|
798
|
+
'bueno new my-app --docker --deploy render',
|
|
799
|
+
'bueno new my-app --docker --database postgresql --deploy render',
|
|
800
|
+
'bueno new my-app -y',
|
|
801
|
+
],
|
|
802
|
+
},
|
|
803
|
+
handleNew,
|
|
804
|
+
);
|