@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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Command
|
|
3
|
+
*
|
|
4
|
+
* Generate code artifacts (controllers, services, modules, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineCommand } from './index';
|
|
8
|
+
import { getOption, hasFlag, type ParsedArgs } from '../core/args';
|
|
9
|
+
import { cliConsole, colors } from '../core/console';
|
|
10
|
+
import { confirm, isInteractive } from '../core/prompt';
|
|
11
|
+
import { spinner } from '../core/spinner';
|
|
12
|
+
import {
|
|
13
|
+
fileExists,
|
|
14
|
+
writeFile,
|
|
15
|
+
readFile,
|
|
16
|
+
getProjectRoot,
|
|
17
|
+
isBuenoProject,
|
|
18
|
+
joinPaths,
|
|
19
|
+
processTemplate,
|
|
20
|
+
} from '../utils/fs';
|
|
21
|
+
import { kebabCase, pascalCase, camelCase } from '../utils/strings';
|
|
22
|
+
import { CLIError, CLIErrorType } from '../index';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generator types
|
|
26
|
+
*/
|
|
27
|
+
type GeneratorType =
|
|
28
|
+
| 'controller'
|
|
29
|
+
| 'service'
|
|
30
|
+
| 'module'
|
|
31
|
+
| 'guard'
|
|
32
|
+
| 'interceptor'
|
|
33
|
+
| 'pipe'
|
|
34
|
+
| 'filter'
|
|
35
|
+
| 'dto'
|
|
36
|
+
| 'middleware'
|
|
37
|
+
| 'migration';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generator aliases
|
|
41
|
+
*/
|
|
42
|
+
const GENERATOR_ALIASES: Record<string, GeneratorType> = {
|
|
43
|
+
c: 'controller',
|
|
44
|
+
s: 'service',
|
|
45
|
+
m: 'module',
|
|
46
|
+
gu: 'guard',
|
|
47
|
+
i: 'interceptor',
|
|
48
|
+
p: 'pipe',
|
|
49
|
+
f: 'filter',
|
|
50
|
+
d: 'dto',
|
|
51
|
+
mw: 'middleware',
|
|
52
|
+
mi: 'migration',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generator configuration
|
|
57
|
+
*/
|
|
58
|
+
interface GeneratorConfig {
|
|
59
|
+
type: GeneratorType;
|
|
60
|
+
name: string;
|
|
61
|
+
module?: string;
|
|
62
|
+
path?: string;
|
|
63
|
+
dryRun: boolean;
|
|
64
|
+
force: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get template content for a generator type
|
|
69
|
+
*/
|
|
70
|
+
function getTemplate(type: GeneratorType): string {
|
|
71
|
+
const templates: Record<GeneratorType, string> = {
|
|
72
|
+
controller: `import { Controller, Get, Post, Put, Delete{{#if path}} } from 'bueno'{{/if}}{{#if service}}, { {{pascalCase service}}Service } from './{{kebabCase service}}.service'{{/if}};
|
|
73
|
+
import type { Context } from 'bueno';
|
|
74
|
+
|
|
75
|
+
@Controller('{{path}}')
|
|
76
|
+
export class {{pascalCase name}}Controller {
|
|
77
|
+
{{#if service}}
|
|
78
|
+
constructor(private readonly {{camelCase service}}Service: {{pascalCase service}}Service) {}
|
|
79
|
+
{{/if}}
|
|
80
|
+
|
|
81
|
+
@Get()
|
|
82
|
+
async findAll(ctx: Context) {
|
|
83
|
+
return { message: '{{pascalCase name}} controller' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Get(':id')
|
|
87
|
+
async findOne(ctx: Context) {
|
|
88
|
+
const id = ctx.params.id;
|
|
89
|
+
return { id, message: '{{pascalCase name}} item' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Post()
|
|
93
|
+
async create(ctx: Context) {
|
|
94
|
+
const body = await ctx.body();
|
|
95
|
+
return { message: 'Created', data: body };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Put(':id')
|
|
99
|
+
async update(ctx: Context) {
|
|
100
|
+
const id = ctx.params.id;
|
|
101
|
+
const body = await ctx.body();
|
|
102
|
+
return { id, message: 'Updated', data: body };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Delete(':id')
|
|
106
|
+
async remove(ctx: Context) {
|
|
107
|
+
const id = ctx.params.id;
|
|
108
|
+
return { id, message: 'Deleted' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
service: `import { Injectable } from 'bueno';
|
|
113
|
+
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class {{pascalCase name}}Service {
|
|
116
|
+
async findAll() {
|
|
117
|
+
// TODO: Implement findAll
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async findOne(id: string) {
|
|
122
|
+
// TODO: Implement findOne
|
|
123
|
+
return { id };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async create(data: unknown) {
|
|
127
|
+
// TODO: Implement create
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async update(id: string, data: unknown) {
|
|
132
|
+
// TODO: Implement update
|
|
133
|
+
return { id, ...data };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async remove(id: string) {
|
|
137
|
+
// TODO: Implement remove
|
|
138
|
+
return { id };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
module: `import { Module } from 'bueno';
|
|
143
|
+
import { {{pascalCase name}}Controller } from './{{kebabCase name}}.controller';
|
|
144
|
+
import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
|
145
|
+
|
|
146
|
+
@Module({
|
|
147
|
+
controllers: [{{pascalCase name}}Controller],
|
|
148
|
+
providers: [{{pascalCase name}}Service],
|
|
149
|
+
exports: [{{pascalCase name}}Service],
|
|
150
|
+
})
|
|
151
|
+
export class {{pascalCase name}}Module {}
|
|
152
|
+
`,
|
|
153
|
+
guard: `import { Injectable, type CanActivate, type Context } from 'bueno';
|
|
154
|
+
|
|
155
|
+
@Injectable()
|
|
156
|
+
export class {{pascalCase name}}Guard implements CanActivate {
|
|
157
|
+
async canActivate(ctx: Context): Promise<boolean> {
|
|
158
|
+
// TODO: Implement guard logic
|
|
159
|
+
// Return true to allow access, false to deny
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
interceptor: `import { Injectable, type NestInterceptor, type CallHandler, type Context } from 'bueno';
|
|
165
|
+
import type { Observable } from 'rxjs';
|
|
166
|
+
|
|
167
|
+
@Injectable()
|
|
168
|
+
export class {{pascalCase name}}Interceptor implements NestInterceptor {
|
|
169
|
+
async intercept(ctx: Context, next: CallHandler): Promise<Observable<unknown>> {
|
|
170
|
+
// Before handler execution
|
|
171
|
+
console.log('{{pascalCase name}}Interceptor - Before');
|
|
172
|
+
|
|
173
|
+
// Call the handler
|
|
174
|
+
const result = await next.handle();
|
|
175
|
+
|
|
176
|
+
// After handler execution
|
|
177
|
+
console.log('{{pascalCase name}}Interceptor - After');
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
`,
|
|
183
|
+
pipe: `import { Injectable, type PipeTransform, type Context } from 'bueno';
|
|
184
|
+
|
|
185
|
+
@Injectable()
|
|
186
|
+
export class {{pascalCase name}}Pipe implements PipeTransform {
|
|
187
|
+
async transform(value: unknown, ctx: Context): Promise<unknown> {
|
|
188
|
+
// TODO: Implement transformation/validation logic
|
|
189
|
+
// Throw an error to reject the value
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
`,
|
|
194
|
+
filter: `import { Injectable, type ExceptionFilter, type Context } from 'bueno';
|
|
195
|
+
import type { Response } from 'bueno';
|
|
196
|
+
|
|
197
|
+
@Injectable()
|
|
198
|
+
export class {{pascalCase name}}Filter implements ExceptionFilter {
|
|
199
|
+
async catch(exception: Error, ctx: Context): Promise<Response> {
|
|
200
|
+
// TODO: Implement exception handling
|
|
201
|
+
console.error('{{pascalCase name}}Filter caught:', exception);
|
|
202
|
+
|
|
203
|
+
return new Response(
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
statusCode: 500,
|
|
206
|
+
message: 'Internal Server Error',
|
|
207
|
+
error: exception.message,
|
|
208
|
+
}),
|
|
209
|
+
{
|
|
210
|
+
status: 500,
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
`,
|
|
217
|
+
dto: `/**
|
|
218
|
+
* {{pascalCase name}} DTO
|
|
219
|
+
*/
|
|
220
|
+
export interface {{pascalCase name}}Dto {
|
|
221
|
+
// TODO: Define properties
|
|
222
|
+
id?: string;
|
|
223
|
+
createdAt?: Date;
|
|
224
|
+
updatedAt?: Date;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Create {{pascalCase name}} DTO
|
|
229
|
+
*/
|
|
230
|
+
export interface Create{{pascalCase name}}Dto {
|
|
231
|
+
// TODO: Define required properties for creation
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update {{pascalCase name}} DTO
|
|
236
|
+
*/
|
|
237
|
+
export interface Update{{pascalCase name}}Dto extends Partial<Create{{pascalCase name}}Dto> {
|
|
238
|
+
// TODO: Define optional properties for update
|
|
239
|
+
}
|
|
240
|
+
`,
|
|
241
|
+
middleware: `import type { Middleware, Context, Handler } from 'bueno';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* {{pascalCase name}} Middleware
|
|
245
|
+
*/
|
|
246
|
+
export const {{camelCase name}}Middleware: Middleware = async (
|
|
247
|
+
ctx: Context,
|
|
248
|
+
next: Handler
|
|
249
|
+
) => {
|
|
250
|
+
// Before handler execution
|
|
251
|
+
console.log('{{pascalCase name}}Middleware - Before');
|
|
252
|
+
|
|
253
|
+
// Call the next handler
|
|
254
|
+
const result = await next();
|
|
255
|
+
|
|
256
|
+
// After handler execution
|
|
257
|
+
console.log('{{pascalCase name}}Middleware - After');
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
};
|
|
261
|
+
`,
|
|
262
|
+
migration: `import { createMigration, type MigrationRunner } from 'bueno';
|
|
263
|
+
|
|
264
|
+
export default createMigration('{{migrationId}}', '{{migrationName}}')
|
|
265
|
+
.up(async (db: MigrationRunner) => {
|
|
266
|
+
// TODO: Add migration logic
|
|
267
|
+
// Example:
|
|
268
|
+
// await db.createTable({
|
|
269
|
+
// name: '{{tableName}}',
|
|
270
|
+
// columns: [
|
|
271
|
+
// { name: 'id', type: 'uuid', primary: true },
|
|
272
|
+
// { name: 'created_at', type: 'timestamp', default: 'NOW()' },
|
|
273
|
+
// ],
|
|
274
|
+
// });
|
|
275
|
+
})
|
|
276
|
+
.down(async (db: MigrationRunner) => {
|
|
277
|
+
// TODO: Add rollback logic
|
|
278
|
+
// Example:
|
|
279
|
+
// await db.dropTable('{{tableName}}');
|
|
280
|
+
});
|
|
281
|
+
`,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return templates[type];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get file extension for generator type
|
|
289
|
+
*/
|
|
290
|
+
function getFileExtension(type: GeneratorType): string {
|
|
291
|
+
return type === 'dto' ? '.dto.ts' : '.ts';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get default directory for generator type
|
|
296
|
+
*/
|
|
297
|
+
function getDefaultDirectory(type: GeneratorType): string {
|
|
298
|
+
switch (type) {
|
|
299
|
+
case 'controller':
|
|
300
|
+
case 'service':
|
|
301
|
+
case 'module':
|
|
302
|
+
case 'dto':
|
|
303
|
+
return 'modules';
|
|
304
|
+
case 'guard':
|
|
305
|
+
return 'common/guards';
|
|
306
|
+
case 'interceptor':
|
|
307
|
+
return 'common/interceptors';
|
|
308
|
+
case 'pipe':
|
|
309
|
+
return 'common/pipes';
|
|
310
|
+
case 'filter':
|
|
311
|
+
return 'common/filters';
|
|
312
|
+
case 'middleware':
|
|
313
|
+
return 'common/middleware';
|
|
314
|
+
case 'migration':
|
|
315
|
+
return 'database/migrations';
|
|
316
|
+
default:
|
|
317
|
+
return '';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Generate a file
|
|
323
|
+
*/
|
|
324
|
+
async function generateFile(config: GeneratorConfig): Promise<string> {
|
|
325
|
+
const { type, name, module, path: customPath, dryRun, force } = config;
|
|
326
|
+
|
|
327
|
+
// Get project root
|
|
328
|
+
const projectRoot = await getProjectRoot();
|
|
329
|
+
if (!projectRoot) {
|
|
330
|
+
throw new CLIError(
|
|
331
|
+
'Not in a Bueno project directory',
|
|
332
|
+
CLIErrorType.NOT_FOUND,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Determine file path
|
|
337
|
+
const kebabName = kebabCase(name);
|
|
338
|
+
const defaultDir = getDefaultDirectory(type);
|
|
339
|
+
let targetDir: string;
|
|
340
|
+
|
|
341
|
+
if (customPath) {
|
|
342
|
+
targetDir = joinPaths(projectRoot, customPath);
|
|
343
|
+
} else if (module) {
|
|
344
|
+
targetDir = joinPaths(projectRoot, 'server', defaultDir, kebabCase(module));
|
|
345
|
+
} else if (type === 'migration') {
|
|
346
|
+
targetDir = joinPaths(projectRoot, 'server', defaultDir);
|
|
347
|
+
} else {
|
|
348
|
+
targetDir = joinPaths(projectRoot, 'server', defaultDir, kebabName);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const fileName = type === 'migration'
|
|
352
|
+
? `${generateMigrationId()}_${kebabName}${getFileExtension(type)}`
|
|
353
|
+
: `${kebabName}${getFileExtension(type)}`;
|
|
354
|
+
const filePath = joinPaths(targetDir, fileName);
|
|
355
|
+
|
|
356
|
+
// Check if file exists
|
|
357
|
+
if (!force && await fileExists(filePath)) {
|
|
358
|
+
if (isInteractive()) {
|
|
359
|
+
const shouldOverwrite = await confirm(
|
|
360
|
+
`File ${colors.cyan(filePath)} already exists. Overwrite?`,
|
|
361
|
+
{ default: false },
|
|
362
|
+
);
|
|
363
|
+
if (!shouldOverwrite) {
|
|
364
|
+
throw new CLIError(
|
|
365
|
+
'File already exists. Use --force to overwrite.',
|
|
366
|
+
CLIErrorType.FILE_EXISTS,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
throw new CLIError(
|
|
371
|
+
`File already exists: ${filePath}. Use --force to overwrite.`,
|
|
372
|
+
CLIErrorType.FILE_EXISTS,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Get template and process it
|
|
378
|
+
const template = getTemplate(type);
|
|
379
|
+
const content = processTemplate(template, {
|
|
380
|
+
name,
|
|
381
|
+
module: module ?? '',
|
|
382
|
+
path: customPath ?? kebabName,
|
|
383
|
+
service: type === 'controller' ? name : '',
|
|
384
|
+
migrationId: generateMigrationId(),
|
|
385
|
+
migrationName: name,
|
|
386
|
+
tableName: kebabName,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Write file or show dry run
|
|
390
|
+
if (dryRun) {
|
|
391
|
+
cliConsole.log(`\n${colors.bold('File:')} ${filePath}`);
|
|
392
|
+
cliConsole.log(colors.bold('Content:'));
|
|
393
|
+
cliConsole.log(content);
|
|
394
|
+
cliConsole.log('');
|
|
395
|
+
} else {
|
|
396
|
+
await writeFile(filePath, content);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return filePath;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate migration ID
|
|
404
|
+
*/
|
|
405
|
+
function generateMigrationId(): string {
|
|
406
|
+
const now = new Date();
|
|
407
|
+
const year = now.getFullYear();
|
|
408
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
409
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
410
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
411
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
412
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
413
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Handle generate command
|
|
418
|
+
*/
|
|
419
|
+
async function handleGenerate(args: ParsedArgs): Promise<void> {
|
|
420
|
+
// Get generator type
|
|
421
|
+
const typeArg = args.positionals[0];
|
|
422
|
+
if (!typeArg) {
|
|
423
|
+
throw new CLIError(
|
|
424
|
+
'Generator type is required. Usage: bueno generate <type> <name>',
|
|
425
|
+
CLIErrorType.INVALID_ARGS,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const type = GENERATOR_ALIASES[typeArg] ?? typeArg as GeneratorType;
|
|
430
|
+
if (!getTemplate(type)) {
|
|
431
|
+
throw new CLIError(
|
|
432
|
+
`Unknown generator type: ${typeArg}. Available types: controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration`,
|
|
433
|
+
CLIErrorType.INVALID_ARGS,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Get name
|
|
438
|
+
const name = args.positionals[1];
|
|
439
|
+
if (!name) {
|
|
440
|
+
throw new CLIError(
|
|
441
|
+
'Name is required. Usage: bueno generate <type> <name>',
|
|
442
|
+
CLIErrorType.INVALID_ARGS,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Get options
|
|
447
|
+
const config: GeneratorConfig = {
|
|
448
|
+
type,
|
|
449
|
+
name,
|
|
450
|
+
module: getOption<string>(args, 'module', {
|
|
451
|
+
name: 'module',
|
|
452
|
+
type: 'string',
|
|
453
|
+
description: '',
|
|
454
|
+
}),
|
|
455
|
+
path: getOption<string>(args, 'path', {
|
|
456
|
+
name: 'path',
|
|
457
|
+
type: 'string',
|
|
458
|
+
description: '',
|
|
459
|
+
}),
|
|
460
|
+
dryRun: hasFlag(args, 'dry-run'),
|
|
461
|
+
force: hasFlag(args, 'force'),
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Check if in a Bueno project
|
|
465
|
+
if (!config.dryRun && !(await isBuenoProject())) {
|
|
466
|
+
throw new CLIError(
|
|
467
|
+
'Not in a Bueno project directory. Run this command from a Bueno project.',
|
|
468
|
+
CLIErrorType.NOT_FOUND,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Generate
|
|
473
|
+
const s = spinner(`Generating ${colors.cyan(type)} ${colors.cyan(name)}...`);
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const filePath = await generateFile(config);
|
|
477
|
+
|
|
478
|
+
if (config.dryRun) {
|
|
479
|
+
s.info('Dry run complete');
|
|
480
|
+
} else {
|
|
481
|
+
s.success(`Created ${colors.green(filePath)}`);
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
s.error();
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Register the command
|
|
490
|
+
defineCommand(
|
|
491
|
+
{
|
|
492
|
+
name: 'generate',
|
|
493
|
+
alias: 'g',
|
|
494
|
+
description: 'Generate code artifacts (controllers, services, modules, etc.)',
|
|
495
|
+
positionals: [
|
|
496
|
+
{
|
|
497
|
+
name: 'type',
|
|
498
|
+
required: true,
|
|
499
|
+
description: 'Type of artifact to generate (controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration)',
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: 'name',
|
|
503
|
+
required: true,
|
|
504
|
+
description: 'Name of the artifact',
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
options: [
|
|
508
|
+
{
|
|
509
|
+
name: 'module',
|
|
510
|
+
alias: 'm',
|
|
511
|
+
type: 'string',
|
|
512
|
+
description: 'Parent module to register with',
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: 'path',
|
|
516
|
+
type: 'string',
|
|
517
|
+
description: 'Custom path for controller routes',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: 'dry-run',
|
|
521
|
+
type: 'boolean',
|
|
522
|
+
default: false,
|
|
523
|
+
description: 'Show what would be created without writing',
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'force',
|
|
527
|
+
type: 'boolean',
|
|
528
|
+
default: false,
|
|
529
|
+
description: 'Overwrite existing files',
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
examples: [
|
|
533
|
+
'bueno generate controller users',
|
|
534
|
+
'bueno g service auth',
|
|
535
|
+
'bueno g module posts',
|
|
536
|
+
'bueno g guard auth-guard --module auth',
|
|
537
|
+
'bueno g dto create-user --module users',
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
handleGenerate,
|
|
541
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help Command
|
|
3
|
+
*
|
|
4
|
+
* Display help information for commands
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineCommand } from './index';
|
|
8
|
+
import { generateGlobalHelpText, generateHelpText, hasFlag } from '../core/args';
|
|
9
|
+
import { cliConsole } from '../core/console';
|
|
10
|
+
import { registry } from './index';
|
|
11
|
+
|
|
12
|
+
defineCommand(
|
|
13
|
+
{
|
|
14
|
+
name: 'help',
|
|
15
|
+
description: 'Show help information for commands',
|
|
16
|
+
positionals: [
|
|
17
|
+
{
|
|
18
|
+
name: 'command',
|
|
19
|
+
required: false,
|
|
20
|
+
description: 'Command to show help for',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
options: [
|
|
24
|
+
{
|
|
25
|
+
name: 'all',
|
|
26
|
+
alias: 'a',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false,
|
|
29
|
+
description: 'Show help for all commands',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
async (args) => {
|
|
34
|
+
const commandName = args.positionals[0];
|
|
35
|
+
|
|
36
|
+
if (commandName && registry.has(commandName)) {
|
|
37
|
+
// Show help for specific command
|
|
38
|
+
const cmd = registry.get(commandName);
|
|
39
|
+
if (cmd) {
|
|
40
|
+
cliConsole.log(generateHelpText(cmd.definition));
|
|
41
|
+
}
|
|
42
|
+
} else if (hasFlag(args, 'all')) {
|
|
43
|
+
// Show detailed help for all commands
|
|
44
|
+
cliConsole.log('\nBueno CLI - Available Commands\n');
|
|
45
|
+
|
|
46
|
+
for (const cmd of registry.getAll()) {
|
|
47
|
+
cliConsole.log(generateHelpText(cmd));
|
|
48
|
+
cliConsole.log('---');
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Show global help
|
|
52
|
+
cliConsole.log(generateGlobalHelpText(registry.getAll()));
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Registry for Bueno CLI
|
|
3
|
+
*
|
|
4
|
+
* Manages command registration and execution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandDefinition, ParsedArgs } from '../core/args';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Command handler function type
|
|
11
|
+
*/
|
|
12
|
+
export type CommandHandler = (args: ParsedArgs) => Promise<void> | void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registered command
|
|
16
|
+
*/
|
|
17
|
+
export interface RegisteredCommand {
|
|
18
|
+
definition: CommandDefinition;
|
|
19
|
+
handler: CommandHandler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Command registry
|
|
24
|
+
*/
|
|
25
|
+
class CommandRegistry {
|
|
26
|
+
private commands: Map<string, RegisteredCommand> = new Map();
|
|
27
|
+
private aliases: Map<string, string> = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a command
|
|
31
|
+
*/
|
|
32
|
+
register(
|
|
33
|
+
definition: CommandDefinition,
|
|
34
|
+
handler: CommandHandler,
|
|
35
|
+
): void {
|
|
36
|
+
this.commands.set(definition.name, {
|
|
37
|
+
definition,
|
|
38
|
+
handler,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (definition.alias) {
|
|
42
|
+
this.aliases.set(definition.alias, definition.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get a command by name or alias
|
|
48
|
+
*/
|
|
49
|
+
get(name: string): RegisteredCommand | undefined {
|
|
50
|
+
const commandName = this.aliases.get(name) ?? name;
|
|
51
|
+
return this.commands.get(commandName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a command exists
|
|
56
|
+
*/
|
|
57
|
+
has(name: string): boolean {
|
|
58
|
+
const commandName = this.aliases.get(name) ?? name;
|
|
59
|
+
return this.commands.has(commandName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all command definitions
|
|
64
|
+
*/
|
|
65
|
+
getAll(): CommandDefinition[] {
|
|
66
|
+
return Array.from(this.commands.values()).map((c) => c.definition);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get all registered commands
|
|
71
|
+
*/
|
|
72
|
+
getCommands(): Map<string, RegisteredCommand> {
|
|
73
|
+
return new Map(this.commands);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute a command
|
|
78
|
+
*/
|
|
79
|
+
async execute(name: string, args: ParsedArgs): Promise<void> {
|
|
80
|
+
const command = this.get(name);
|
|
81
|
+
|
|
82
|
+
if (!command) {
|
|
83
|
+
throw new Error(`Unknown command: ${name}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await command.handler(args);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Global command registry instance
|
|
91
|
+
export const registry = new CommandRegistry();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a command (decorator-style)
|
|
95
|
+
*/
|
|
96
|
+
export function command(
|
|
97
|
+
definition: CommandDefinition,
|
|
98
|
+
): (handler: CommandHandler) => void {
|
|
99
|
+
return (handler: CommandHandler) => {
|
|
100
|
+
registry.register(definition, handler);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Define a command with its handler
|
|
106
|
+
*/
|
|
107
|
+
export function defineCommand(
|
|
108
|
+
definition: CommandDefinition,
|
|
109
|
+
handler: CommandHandler,
|
|
110
|
+
): void {
|
|
111
|
+
registry.register(definition, handler);
|
|
112
|
+
}
|