speedrun-cli 2.7.5 → 2.7.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedrun-cli",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "description": "CLI tool to scaffold a production-ready NestJS authentication system with JWT, refresh tokens, and RBAC",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -79,6 +79,81 @@ async function detectOrm(targetDir) {
79
79
  return 'prisma';
80
80
  }
81
81
 
82
+ /**
83
+ * Ensures src/common/base exists with required abstract classes & DTOs
84
+ */
85
+ async function ensureBaseArchitecture(targetDir) {
86
+ try {
87
+ const commonBaseDir = path.join(targetDir, 'src', 'common', 'base');
88
+
89
+ if (!(await fs.pathExists(commonBaseDir))) {
90
+ await fs.ensureDir(commonBaseDir);
91
+
92
+ // 1. base.controller.ts
93
+ await fs.writeFile(
94
+ path.join(commonBaseDir, 'base.controller.ts'),
95
+ `import { Type } from '@nestjs/common';
96
+
97
+ export abstract class BaseController<T, CreateDto, UpdateDto> {
98
+ constructor(protected readonly service: any) {}
99
+ protected abstract getDtoClass(): Type<T>;
100
+ async create(dto: CreateDto): Promise<any> { return this.service.create(dto); }
101
+ async findAll(query: any): Promise<any> { return this.service.findAll(query); }
102
+ async findOne(id: string): Promise<any> { return this.service.findOne(id); }
103
+ async update(id: string, dto: UpdateDto): Promise<any> { return this.service.update(id, dto); }
104
+ async remove(id: string): Promise<any> { return this.service.remove(id); }
105
+ }
106
+ `
107
+ );
108
+
109
+ // 2. base.service.ts
110
+ await fs.writeFile(
111
+ path.join(commonBaseDir, 'base.service.ts'),
112
+ `import { Injectable } from '@nestjs/common';
113
+
114
+ export interface PaginationQueryDto { page?: number; limit?: number; }
115
+ export interface IBaseRepository<T, CreateDto, UpdateDto> {
116
+ create(dto: CreateDto): Promise<T>;
117
+ findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }>;
118
+ findOne(id: string): Promise<T | null>;
119
+ update(id: string, dto: UpdateDto): Promise<T>;
120
+ remove(id: string): Promise<T>;
121
+ }
122
+
123
+ @Injectable()
124
+ export abstract class BaseService<T, CreateDto, UpdateDto> {
125
+ protected abstract getRepository(): IBaseRepository<T, CreateDto, UpdateDto>;
126
+ async create(dto: CreateDto): Promise<T> { return this.getRepository().create(dto); }
127
+ async findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }> { return this.getRepository().findAll(pagination); }
128
+ async findOne(id: string): Promise<T | null> { return this.getRepository().findOne(id); }
129
+ async update(id: string, dto: UpdateDto): Promise<T> { return this.getRepository().update(id, dto); }
130
+ async remove(id: string): Promise<T> { return this.getRepository().remove(id); }
131
+ }
132
+ `
133
+ );
134
+
135
+ // 3. index.ts (Re-exports & DTO helpers)
136
+ await fs.writeFile(
137
+ path.join(commonBaseDir, 'index.ts'),
138
+ `export * from './base.controller';
139
+ export * from './base.service';
140
+
141
+ export class ApiResponseDto<T> { statusCode: number; message: string; data: T; }
142
+ export class PaginatedResponseDto<T> { statusCode: number; message: string; data: T[]; total: number; page: number; limit: number; }
143
+ export class PaginationQueryDto { page?: number; limit?: number; }
144
+
145
+ export function ApiResponseSchema(dto: any): any { return {}; }
146
+ export function PaginatedResponseSchema(dto: any): any { return {}; }
147
+ `
148
+ );
149
+
150
+ console.log(chalk.green(' ✓ Auto-generated missing src/common/base architecture'));
151
+ }
152
+ } catch (error) {
153
+ console.warn(chalk.yellow(` ⚠️ Could not verify/create base architecture: ${error.message}`));
154
+ }
155
+ }
156
+
82
157
  /**
83
158
  * Interactive prompt for module options (Name, CRUD Mode, Fields)
84
159
  */
@@ -124,6 +199,7 @@ async function promptForModuleOptions(providedModuleName) {
124
199
  selectedOperations = customOps;
125
200
  }
126
201
 
202
+ // Interactive Field Builder Loop
127
203
  // Interactive Field Builder Loop
128
204
  const { addCustomFields } = await inquirer.prompt([{
129
205
  type: 'confirm',
@@ -135,13 +211,16 @@ async function promptForModuleOptions(providedModuleName) {
135
211
  const fields = [];
136
212
 
137
213
  if (addCustomFields) {
138
- let addAnother = true;
139
- while (addAnother) {
140
- const fieldAnswers = await inquirer.prompt([
214
+ let building = true;
215
+
216
+ // Helper untuk input field baru / edit
217
+ const promptSingleField = async (initialValues = {}) => {
218
+ return await inquirer.prompt([
141
219
  {
142
220
  type: 'input',
143
221
  name: 'fieldName',
144
222
  message: 'Enter field name (e.g., totalAmount, title):',
223
+ default: initialValues.name,
145
224
  validate: (input) => {
146
225
  if (!input || !input.trim()) return 'Field name is required';
147
226
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
@@ -155,30 +234,96 @@ async function promptForModuleOptions(providedModuleName) {
155
234
  name: 'fieldType',
156
235
  message: (answers) => `Select field type for '${answers.fieldName}':`,
157
236
  choices: ['String', 'Number', 'Boolean', 'Date'],
158
- default: 'String',
237
+ default: initialValues.type || 'String',
159
238
  },
160
239
  {
161
240
  type: 'confirm',
162
241
  name: 'isOptional',
163
242
  message: (answers) => `Is '${answers.fieldName}' optional?`,
164
- default: false,
243
+ default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
165
244
  },
166
245
  ]);
246
+ };
247
+
248
+ // Tambah field pertama
249
+ console.log(chalk.cyan('\n--- Add Field 1 ---'));
250
+ const firstField = await promptSingleField();
251
+ fields.push({
252
+ name: firstField.fieldName.trim(),
253
+ type: firstField.fieldType,
254
+ isOptional: firstField.isOptional,
255
+ });
167
256
 
168
- fields.push({
169
- name: fieldAnswers.fieldName.trim(),
170
- type: fieldAnswers.fieldType,
171
- isOptional: fieldAnswers.isOptional,
172
- });
173
-
174
- const { continueLoop } = await inquirer.prompt([{
175
- type: 'confirm',
176
- name: 'continueLoop',
177
- message: 'Do you want to add another field?',
178
- default: false,
257
+ // Menu Navigasi (Add, Edit, Delete, Finish)
258
+ while (building) {
259
+ console.log(chalk.gray(`\nCurrent Fields (${fields.length}): `) + fields.map(f => chalk.yellow(`${f.name} (${f.type}${f.isOptional ? '?' : ''})`)).join(', '));
260
+
261
+ const { action } = await inquirer.prompt([{
262
+ type: 'list',
263
+ name: 'action',
264
+ message: 'What do you want to do next?',
265
+ choices: [
266
+ { name: ' Add another field', value: 'add' },
267
+ { name: '✏️ Edit an existing field', value: 'edit' },
268
+ { name: '🗑️ Delete a field', value: 'delete' },
269
+ { name: '✅ Finish and generate module', value: 'done' },
270
+ ],
179
271
  }]);
180
272
 
181
- addAnother = continueLoop;
273
+ if (action === 'add') {
274
+ console.log(chalk.cyan(`\n--- Add Field ${fields.length + 1} ---`));
275
+ const newField = await promptSingleField();
276
+ fields.push({
277
+ name: newField.fieldName.trim(),
278
+ type: newField.fieldType,
279
+ isOptional: newField.isOptional,
280
+ });
281
+ } else if (action === 'edit') {
282
+ if (fields.length === 0) {
283
+ console.log(chalk.yellow('⚠️ No fields available to edit.'));
284
+ continue;
285
+ }
286
+
287
+ const { fieldToEditIndex } = await inquirer.prompt([{
288
+ type: 'list',
289
+ name: 'fieldToEditIndex',
290
+ message: 'Select field to edit:',
291
+ choices: fields.map((f, index) => ({
292
+ name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
293
+ value: index,
294
+ })),
295
+ }]);
296
+
297
+ console.log(chalk.cyan(`\n--- Editing Field '${fields[fieldToEditIndex].name}' ---`));
298
+ const editedField = await promptSingleField(fields[fieldToEditIndex]);
299
+ fields[fieldToEditIndex] = {
300
+ name: editedField.fieldName.trim(),
301
+ type: editedField.fieldType,
302
+ isOptional: editedField.isOptional,
303
+ };
304
+ console.log(chalk.green(`✓ Field '${fields[fieldToEditIndex].name}' updated successfully.`));
305
+ } else if (action === 'delete') {
306
+ if (fields.length === 0) {
307
+ console.log(chalk.yellow('⚠️ No fields available to delete.'));
308
+ continue;
309
+ }
310
+
311
+ const { fieldToDeleteIndex } = await inquirer.prompt([{
312
+ type: 'list',
313
+ name: 'fieldToDeleteIndex',
314
+ message: 'Select field to delete:',
315
+ choices: fields.map((f, index) => ({
316
+ name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
317
+ value: index,
318
+ })),
319
+ }]);
320
+
321
+ const deletedName = fields[fieldToDeleteIndex].name;
322
+ fields.splice(fieldToDeleteIndex, 1);
323
+ console.log(chalk.red(`🗑️ Field '${deletedName}' removed.`));
324
+ } else if (action === 'done') {
325
+ building = false;
326
+ }
182
327
  }
183
328
  }
184
329
 
@@ -186,12 +331,6 @@ async function promptForModuleOptions(providedModuleName) {
186
331
  if (fields.length === 0) {
187
332
  fields.push({ name: 'name', type: 'String', isOptional: false });
188
333
  }
189
-
190
- return {
191
- moduleName,
192
- operations: selectedOperations,
193
- fields,
194
- };
195
334
  }
196
335
 
197
336
  function getFieldExampleValue(field, pascalName) {
@@ -284,9 +423,9 @@ ${fieldLines.join('\n')}
284
423
 
285
424
  content += modelDefinition;
286
425
  await fs.writeFile(schemaPath, content, 'utf8');
287
- console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
426
+ console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
288
427
  } catch (error) {
289
- console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
428
+ console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
290
429
  }
291
430
  }
292
431
 
@@ -341,9 +480,9 @@ ${fieldLines.join('\n\n')}
341
480
  `;
342
481
 
343
482
  await fs.writeFile(entityPath, entityContent, 'utf8');
344
- console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
483
+ console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
345
484
  } catch (error) {
346
- console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
485
+ console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
347
486
  }
348
487
  }
349
488
 
@@ -394,9 +533,9 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
394
533
  `;
395
534
 
396
535
  await fs.writeFile(schemaPath, schemaContent, 'utf8');
397
- console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
536
+ console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
398
537
  } catch (error) {
399
- console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
538
+ console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
400
539
  }
401
540
  }
402
541
 
@@ -442,9 +581,9 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
442
581
  `;
443
582
 
444
583
  await fs.writeFile(schemaPath, schemaContent, 'utf8');
445
- console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
584
+ console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
446
585
  } catch (error) {
447
- console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
586
+ console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
448
587
  }
449
588
  }
450
589
 
@@ -478,7 +617,7 @@ ${dummyObjFields}
478
617
  }
479
618
  `;
480
619
  await fs.writeFile(seedPath, seedContent, 'utf8');
481
- console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
620
+ console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
482
621
  } else {
483
622
  const seedsDir = path.join(targetDir, 'src', 'database', 'seeds');
484
623
  await fs.ensureDir(seedsDir);
@@ -502,10 +641,10 @@ ${dummyObjFields}
502
641
  }
503
642
  `;
504
643
  await fs.writeFile(seedPath, seedContent, 'utf8');
505
- console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
644
+ console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
506
645
  }
507
646
  } catch (error) {
508
- console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
647
+ console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
509
648
  }
510
649
  }
511
650
 
@@ -843,6 +982,9 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
843
982
  */
844
983
  async function generateModule(providedModuleName, targetDir = process.cwd(), specifiedOrm = null) {
845
984
  try {
985
+ // Ensure src/common/base exists before generating any module components
986
+ await ensureBaseArchitecture(targetDir);
987
+
846
988
  const options = await promptForModuleOptions(providedModuleName);
847
989
  const kebabName = toKebabCase(options.moduleName);
848
990
  const pascalName = toPascalCase(options.moduleName);
@@ -991,11 +1133,10 @@ ${responseFieldsText}
991
1133
  );
992
1134
  await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
993
1135
 
994
- // 4. Generate Controller
1136
+ // 4. Generate Controller (Fixed single clean import path from common/base)
995
1137
  const controllerContent = `import { Controller${ops.findAll ? ', Query' : ''}${ops.findOne || ops.update || ops.remove ? ', Param, ParseUUIDPipe, HttpStatus' : ''}${ops.create ? ', Post, Body' : ''}${ops.findAll || ops.findOne ? ', Get' : ''}${ops.update ? ', Put' : ''}${ops.remove ? ', Delete' : ''}, Type } from '@nestjs/common';
996
1138
  import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
997
- import { BaseController } from '../../common/base/base.controller';
998
- import { ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
1139
+ import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
999
1140
  import { ${pascalName}Service } from './${kebabName}.service';
1000
1141
  ${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : `type ${createDtoName} = any;\ntype ${updateDtoName} = any;`}
1001
1142
  import { ${responseDtoName} } from './dto/${kebabName}.dto';
@@ -1074,7 +1215,7 @@ ${ops.create ? `
1074
1215
  await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, options.fields);
1075
1216
 
1076
1217
  console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
1077
- console.log(chalk.gray(` Detected ORM: ${orm}`));
1218
+ console.log(chalk.gray(` Detected ORM: ${orm}`));
1078
1219
 
1079
1220
  if (!isAutoRegistered) {
1080
1221
  console.log(chalk.yellow(`\n⚠️ Please manually register ${pascalName}Module in src/app.module.ts:`));