speedrun-cli 2.7.9 → 2.7.15

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.9",
3
+ "version": "2.7.15",
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",
@@ -1,192 +1,339 @@
1
- /**
2
- * Interactive Field Manager for existing modules
3
- * @module fieldManager
4
- */
5
-
6
- const inquirer = require('inquirer');
7
- const fs = require('fs-extra');
8
- const path = require('path');
9
- const chalk = require('chalk');
10
-
11
- // Helper untuk membaca field dari file DTO response yang sudah ada
12
- async function parseExistingFields(moduleDir, kebabName) {
13
- const dtoPath = path.join(moduleDir, 'dto', `${kebabName}.dto.ts`);
14
- if (!(await fs.pathExists(dtoPath))) return [];
15
-
16
- const content = await fs.readFile(dtoPath, 'utf8');
17
- const fields = [];
18
-
19
- // RegEx untuk mengekstrak property DTO NestJS (namaField?: type)
20
- const regex = /^\s*([a-zA-Z0-9_]+)(\?)?:\s*([a-zA-Z]+);/gm;
21
- let match;
22
-
23
- const ignoredFields = ['id', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
24
-
25
- while ((match = regex.exec(content)) !== null) {
26
- const [, name, optional, tsType] = match;
27
- if (!ignoredFields.includes(name)) {
28
- let type = 'String';
29
- if (tsType === 'number') type = 'Number';
30
- if (tsType === 'boolean') type = 'Boolean';
31
- if (tsType === 'Date') type = 'Date';
32
-
33
- fields.push({
34
- name,
35
- type,
36
- isOptional: !!optional,
37
- });
38
- }
39
- }
40
-
41
- return fields;
42
- }
43
-
44
- /**
45
- * Main Interactive Field Manager Entry Point
46
- */
47
- async function manageFields(providedModuleName, targetDir = process.cwd()) {
48
- try {
49
- let moduleName = providedModuleName;
50
-
51
- if (!moduleName) {
52
- const nameAnswer = await inquirer.prompt([{
53
- type: 'input',
54
- name: 'moduleName',
55
- message: 'Which module do you want to manage fields for? (e.g., orders, products)',
56
- validate: (input) => (input && input.trim() ? true : 'Module name is required'),
57
- }]);
58
- moduleName = nameAnswer.moduleName.trim();
59
- }
60
-
61
- const kebabName = moduleName.toLowerCase();
62
- const moduleDir = path.join(targetDir, 'src', 'modules', kebabName);
63
-
64
- if (!(await fs.pathExists(moduleDir))) {
65
- console.error(chalk.red(`\nāŒ Module "${kebabName}" not found at src/modules/${kebabName}`));
66
- return false;
67
- }
68
-
69
- // Load existing fields
70
- let fields = await parseExistingFields(moduleDir, kebabName);
71
- console.log(chalk.cyan(`\nšŸ“¦ Managing fields for module: ${chalk.bold(kebabName)}`));
72
-
73
- let managing = true;
74
-
75
- const promptSingleField = async (initialValues = {}) => {
76
- return await inquirer.prompt([
77
- {
78
- type: 'input',
79
- name: 'fieldName',
80
- message: 'Enter field name:',
81
- default: initialValues.name,
82
- validate: (input) => {
83
- if (!input || !input.trim()) return 'Field name is required';
84
- if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
85
- return 'Field name must be a valid identifier';
86
- }
87
- return true;
88
- },
89
- },
90
- {
91
- type: 'list',
92
- name: 'fieldType',
93
- message: (answers) => `Select field type for '${answers.fieldName}':`,
94
- choices: ['String', 'Number', 'Boolean', 'Date'],
95
- default: initialValues.type || 'String',
96
- },
97
- {
98
- type: 'confirm',
99
- name: 'isOptional',
100
- message: (answers) => `Is '${answers.fieldName}' optional?`,
101
- default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
102
- },
103
- ]);
104
- };
105
-
106
- while (managing) {
107
- console.log(
108
- chalk.gray(`\nCurrent Fields (${fields.length}): `) +
109
- (fields.length > 0
110
- ? fields.map(f => chalk.yellow(`${f.name} (${f.type}${f.isOptional ? '?' : ''})`)).join(', ')
111
- : chalk.gray('None'))
112
- );
113
-
114
- const { action } = await inquirer.prompt([{
115
- type: 'list',
116
- name: 'action',
117
- message: 'What do you want to do?',
118
- choices: [
119
- { name: 'āž• Add new field', value: 'add' },
120
- { name: 'āœļø Edit an existing field', value: 'edit' },
121
- { name: 'šŸ—‘ļø Delete a field', value: 'delete' },
122
- { name: 'šŸ’¾ Save changes and update files', value: 'save' },
123
- { name: 'āŒ Cancel', value: 'cancel' },
124
- ],
125
- }]);
126
-
127
- if (action === 'add') {
128
- const newField = await promptSingleField();
129
- fields.push({
130
- name: newField.fieldName.trim(),
131
- type: newField.fieldType,
132
- isOptional: newField.isOptional,
133
- });
134
- } else if (action === 'edit') {
135
- if (fields.length === 0) {
136
- console.log(chalk.yellow('āš ļø No fields to edit.'));
137
- continue;
138
- }
139
-
140
- const { fieldToEditIndex } = await inquirer.prompt([{
141
- type: 'list',
142
- name: 'fieldToEditIndex',
143
- message: 'Select field to edit:',
144
- choices: fields.map((f, index) => ({
145
- name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
146
- value: index,
147
- })),
148
- }]);
149
-
150
- const editedField = await promptSingleField(fields[fieldToEditIndex]);
151
- fields[fieldToEditIndex] = {
152
- name: editedField.fieldName.trim(),
153
- type: editedField.fieldType,
154
- isOptional: editedField.isOptional,
155
- };
156
- console.log(chalk.green(`āœ“ Field updated.`));
157
- } else if (action === 'delete') {
158
- if (fields.length === 0) {
159
- console.log(chalk.yellow('āš ļø No fields to delete.'));
160
- continue;
161
- }
162
-
163
- const { fieldToDeleteIndex } = await inquirer.prompt([{
164
- type: 'list',
165
- name: 'fieldToDeleteIndex',
166
- message: 'Select field to delete:',
167
- choices: fields.map((f, index) => ({
168
- name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
169
- value: index,
170
- })),
171
- }]);
172
-
173
- const deletedName = fields[fieldToDeleteIndex].name;
174
- fields.splice(fieldToDeleteIndex, 1);
175
- console.log(chalk.red(`šŸ—‘ļø Field '${deletedName}' removed.`));
176
- } else if (action === 'save') {
177
- managing = false;
178
- // Panggil fungsi regenerasi DTO & Sync ORM dari moduleGenerator
179
- const { regenerateModuleComponents } = require('./moduleGenerator');
180
- await regenerateModuleComponents(moduleName, fields, targetDir);
181
- console.log(chalk.green(`\nāœ… Successfully updated fields for module "${kebabName}"!`));
182
- } else if (action === 'cancel') {
183
- console.log(chalk.gray('Cancelled field management. No files were modified.'));
184
- managing = false;
185
- }
186
- }
187
- } catch (error) {
188
- console.error(chalk.red('\nāŒ Field management failed:'), error.message);
189
- }
190
- }
191
-
1
+ /**
2
+ * Interactive Field Manager for existing modules
3
+ * @module fieldManager
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+ const chalk = require('chalk');
10
+ const { execSync } = require('child_process');
11
+ const { detectOrm, getOrmFieldChoices, regenerateModuleComponents, toKebabCase } = require('./moduleGenerator');
12
+ const { detectPackageManager, getRunPrefix } = require('./utils');
13
+
14
+ /**
15
+ * Map a class-validator decorator name to the best ORM-native type for the given ORM.
16
+ * Used when round-tripping field types from the DTO file back into the field editor.
17
+ */
18
+ function inferOrmTypeFromValidator(decoratorName, orm) {
19
+ switch (decoratorName) {
20
+ case 'IsInt':
21
+ return orm === 'prisma' ? 'Int' : orm === 'mongoose' ? 'Number' : 'int';
22
+ case 'IsNumber':
23
+ if (orm === 'prisma') return 'Float';
24
+ if (orm === 'typeorm') return 'decimal';
25
+ if (orm === 'drizzle') return 'numeric';
26
+ return 'Number'; // mongoose
27
+ case 'IsBoolean':
28
+ return orm === 'prisma' || orm === 'mongoose' ? 'Boolean' : 'boolean';
29
+ case 'IsDate':
30
+ if (orm === 'prisma') return 'DateTime';
31
+ if (orm === 'mongoose') return 'Date';
32
+ return 'timestamp'; // typeorm / drizzle
33
+ case 'IsObject':
34
+ return orm === 'prisma' ? 'Json' : orm === 'mongoose' ? 'Object' : 'json';
35
+ case 'IsArray':
36
+ return orm === 'mongoose' ? 'Array' : orm === 'prisma' ? 'Json' : 'json';
37
+ case 'IsString':
38
+ default:
39
+ return orm === 'prisma' || orm === 'mongoose' ? 'String' : 'varchar';
40
+ }
41
+ }
42
+
43
+ // Helper to parse existing fields from the create DTO file (most accurate source)
44
+ async function parseExistingFields(moduleDir, kebabName, orm = 'prisma') {
45
+ // Prefer create DTO (has validators + real field types); fall back to response DTO
46
+ const createDtoPath = path.join(moduleDir, 'dto', `create-${kebabName}.dto.ts`);
47
+ const responseDtoPath = path.join(moduleDir, 'dto', `${kebabName}.dto.ts`);
48
+
49
+ const targetPath = (await fs.pathExists(createDtoPath)) ? createDtoPath : responseDtoPath;
50
+ if (!(await fs.pathExists(targetPath))) return [];
51
+
52
+ const content = await fs.readFile(targetPath, 'utf8');
53
+ const fields = [];
54
+
55
+ // System / PK / FK fields to exclude from editing
56
+ const IGNORED_EXACT = new Set(['id', 'status', 'createdAt', 'updatedAt', 'deletedAt']);
57
+
58
+ // Split into per-property blocks by splitting at each decorator line or property line
59
+ // Strategy: iterate line-by-line, collect decorator context then resolve property
60
+ const lines = content.split('\n');
61
+ let pendingDecorators = [];
62
+
63
+ for (const line of lines) {
64
+ const trimmed = line.trim();
65
+
66
+ // Collect validator decorator names (e.g. @IsString(), @IsInt())
67
+ const decMatch = trimmed.match(/^@(Is[A-Z][a-zA-Z]+)\(/);
68
+ if (decMatch) {
69
+ pendingDecorators.push(decMatch[1]);
70
+ continue;
71
+ }
72
+
73
+ // Reset decorator context on non-decorator, non-property lines
74
+ if (trimmed.startsWith('@') || trimmed === '' || trimmed.startsWith('import') || trimmed.startsWith('export') || trimmed.startsWith('//')) {
75
+ if (!trimmed.match(/^([a-zA-Z0-9_]+)\??\s*:/)) {
76
+ pendingDecorators = [];
77
+ }
78
+ continue;
79
+ }
80
+
81
+ // Match property line: fieldName?: tsType;
82
+ const propMatch = trimmed.match(/^([a-zA-Z0-9_]+)(\?)?\s*:\s*([a-zA-Z_][\w\[\]]*)\s*;/);
83
+ if (propMatch) {
84
+ const [, name, optional, tsType] = propMatch;
85
+
86
+ // Skip system fields and FK / custom PK patterns
87
+ if (
88
+ IGNORED_EXACT.has(name) ||
89
+ /_id$/i.test(name) ||
90
+ /Id$/.test(name)
91
+ ) {
92
+ pendingDecorators = [];
93
+ continue;
94
+ }
95
+
96
+ // Determine ORM type from validator decorators (most accurate) or TS type fallback
97
+ let ormType;
98
+ const validatorDec = pendingDecorators.find((d) => d.startsWith('Is'));
99
+ if (validatorDec) {
100
+ ormType = inferOrmTypeFromValidator(validatorDec, orm);
101
+ } else {
102
+ // TS-type fallback mapping
103
+ if (tsType === 'number') ormType = orm === 'prisma' ? 'Float' : orm === 'mongoose' ? 'Number' : 'decimal';
104
+ else if (tsType === 'boolean') ormType = orm === 'prisma' || orm === 'mongoose' ? 'Boolean' : 'boolean';
105
+ else if (tsType === 'Date') ormType = orm === 'prisma' ? 'DateTime' : orm === 'mongoose' ? 'Date' : 'timestamp';
106
+ else if (tsType === 'object') ormType = orm === 'prisma' ? 'Json' : orm === 'mongoose' ? 'Object' : 'json';
107
+ else ormType = orm === 'prisma' || orm === 'mongoose' ? 'String' : 'varchar';
108
+ }
109
+
110
+ fields.push({
111
+ name,
112
+ type: ormType,
113
+ isOptional: !!optional,
114
+ });
115
+
116
+ pendingDecorators = [];
117
+ continue;
118
+ }
119
+
120
+ pendingDecorators = [];
121
+ }
122
+
123
+ return fields;
124
+ }
125
+
126
+
127
+ /**
128
+ * Executes ORM-specific database migration / schema sync
129
+ */
130
+ async function runDatabaseMigration(targetDir, orm, packageManager) {
131
+ const pmPrefix = getRunPrefix(packageManager);
132
+ console.log(chalk.yellow(`\n⚔ Running database migration for ORM: ${chalk.bold(orm)}...\n`));
133
+
134
+ try {
135
+ if (orm === 'prisma') {
136
+ execSync('npx prisma db push', { cwd: targetDir, stdio: 'inherit' });
137
+ } else if (orm === 'typeorm') {
138
+ execSync(`${pmPrefix} schema:sync`, { cwd: targetDir, stdio: 'inherit' });
139
+ } else if (orm === 'drizzle') {
140
+ execSync(`${pmPrefix} db:push`, { cwd: targetDir, stdio: 'inherit' });
141
+ } else if (orm === 'mongoose') {
142
+ console.log(chalk.green(' āœ“ Mongoose schemas update dynamically on application startup.'));
143
+ }
144
+ console.log(chalk.green('\n āœ“ Database migration complete!\n'));
145
+ } catch (error) {
146
+ console.error(chalk.red(`\n āœ— Database migration failed: ${error.message}\n`));
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Executes ORM-specific database seed
152
+ */
153
+ async function runDatabaseSeed(targetDir, orm, packageManager) {
154
+ const pmPrefix = getRunPrefix(packageManager);
155
+ console.log(chalk.yellow(`\n🌱 Seeding database for ORM: ${chalk.bold(orm)}...\n`));
156
+
157
+ try {
158
+ if (orm === 'prisma') {
159
+ execSync(`${pmPrefix} prisma:seed`, { cwd: targetDir, stdio: 'inherit' });
160
+ } else if (orm === 'typeorm') {
161
+ execSync(`${pmPrefix} seed`, { cwd: targetDir, stdio: 'inherit' });
162
+ } else if (orm === 'mongoose' || orm === 'drizzle') {
163
+ execSync(`${pmPrefix} db:seed`, { cwd: targetDir, stdio: 'inherit' });
164
+ }
165
+ console.log(chalk.green('\n āœ“ Database seed complete!\n'));
166
+ } catch (error) {
167
+ console.error(chalk.red(`\n āœ— Database seed failed: ${error.message}\n`));
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Main Interactive Field Manager Entry Point
173
+ */
174
+ async function manageFields(providedModuleName, targetDir = process.cwd()) {
175
+ try {
176
+ const orm = await detectOrm(targetDir);
177
+ const ormTypeChoices = getOrmFieldChoices(orm);
178
+ const pm = detectPackageManager();
179
+
180
+ let moduleName = providedModuleName;
181
+
182
+ if (!moduleName) {
183
+ const nameAnswer = await inquirer.prompt([{
184
+ type: 'input',
185
+ name: 'moduleName',
186
+ message: 'Which module do you want to manage fields for? (e.g., orders, products)',
187
+ validate: (input) => (input && input.trim() ? true : 'Module name is required'),
188
+ }]);
189
+ moduleName = nameAnswer.moduleName.trim();
190
+ }
191
+
192
+ const kebabName = toKebabCase(moduleName);
193
+ const moduleDir = path.join(targetDir, 'src', 'modules', kebabName);
194
+
195
+ if (!(await fs.pathExists(moduleDir))) {
196
+ console.error(chalk.red(`\nāŒ Module "${kebabName}" not found at src/modules/${kebabName}`));
197
+ return false;
198
+ }
199
+
200
+ // Load existing fields (pass orm so type inference produces ORM-native types)
201
+ let fields = await parseExistingFields(moduleDir, kebabName, orm);
202
+ console.log(chalk.cyan(`\nšŸ“¦ Managing fields for module: ${chalk.bold(kebabName)} (Detected ORM: ${orm})`));
203
+
204
+ let managing = true;
205
+
206
+ const promptSingleField = async (initialValues = {}) => {
207
+ return await inquirer.prompt([
208
+ {
209
+ type: 'input',
210
+ name: 'fieldName',
211
+ message: 'Enter field name:',
212
+ default: initialValues.name,
213
+ validate: (input) => {
214
+ if (!input || !input.trim()) return 'Field name is required';
215
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
216
+ return 'Field name must be a valid identifier';
217
+ }
218
+ return true;
219
+ },
220
+ },
221
+ {
222
+ type: 'list',
223
+ name: 'fieldType',
224
+ message: (answers) => `Select field type for '${answers.fieldName}':`,
225
+ choices: ormTypeChoices,
226
+ default: initialValues.type && ormTypeChoices.includes(initialValues.type) ? initialValues.type : ormTypeChoices[0],
227
+ },
228
+ {
229
+ type: 'confirm',
230
+ name: 'isOptional',
231
+ message: (answers) => `Is '${answers.fieldName}' optional?`,
232
+ default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
233
+ },
234
+ ]);
235
+ };
236
+
237
+ while (managing) {
238
+ console.log(
239
+ chalk.gray(`\nCurrent Fields (${fields.length}): `) +
240
+ (fields.length > 0
241
+ ? fields.map(f => chalk.yellow(`${f.name} (${f.type}${f.isOptional ? '?' : ''})`)).join(', ')
242
+ : chalk.gray('None'))
243
+ );
244
+
245
+ const { action } = await inquirer.prompt([{
246
+ type: 'list',
247
+ name: 'action',
248
+ message: 'What do you want to do?',
249
+ choices: [
250
+ { name: 'āž• Add new field', value: 'add' },
251
+ { name: 'āœļø Edit an existing field', value: 'edit' },
252
+ { name: 'šŸ—‘ļø Delete a field', value: 'delete' },
253
+ { name: 'šŸ’¾ Save changes & update files', value: 'save' },
254
+ { name: '⚔ Run database migration / schema sync', value: 'migrate' },
255
+ { name: '🌱 Seed database table', value: 'seed' },
256
+ { name: 'āŒ Cancel / Exit', value: 'cancel' },
257
+ ],
258
+ }]);
259
+
260
+ if (action === 'add') {
261
+ const newField = await promptSingleField();
262
+ fields.push({
263
+ name: newField.fieldName.trim(),
264
+ type: newField.fieldType,
265
+ isOptional: newField.isOptional,
266
+ });
267
+ } else if (action === 'edit') {
268
+ if (fields.length === 0) {
269
+ console.log(chalk.yellow('āš ļø No fields to edit.'));
270
+ continue;
271
+ }
272
+
273
+ const { fieldToEditIndex } = await inquirer.prompt([{
274
+ type: 'list',
275
+ name: 'fieldToEditIndex',
276
+ message: 'Select field to edit:',
277
+ choices: fields.map((f, index) => ({
278
+ name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
279
+ value: index,
280
+ })),
281
+ }]);
282
+
283
+ const editedField = await promptSingleField(fields[fieldToEditIndex]);
284
+ fields[fieldToEditIndex] = {
285
+ name: editedField.fieldName.trim(),
286
+ type: editedField.fieldType,
287
+ isOptional: editedField.isOptional,
288
+ };
289
+ console.log(chalk.green(`āœ“ Field updated.`));
290
+ } else if (action === 'delete') {
291
+ if (fields.length === 0) {
292
+ console.log(chalk.yellow('āš ļø No fields to delete.'));
293
+ continue;
294
+ }
295
+
296
+ const { fieldToDeleteIndex } = await inquirer.prompt([{
297
+ type: 'list',
298
+ name: 'fieldToDeleteIndex',
299
+ message: 'Select field to delete:',
300
+ choices: fields.map((f, index) => ({
301
+ name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
302
+ value: index,
303
+ })),
304
+ }]);
305
+
306
+ const deletedName = fields[fieldToDeleteIndex].name;
307
+ fields.splice(fieldToDeleteIndex, 1);
308
+ console.log(chalk.red(`šŸ—‘ļø Field '${deletedName}' removed.`));
309
+ } else if (action === 'save') {
310
+ await regenerateModuleComponents(moduleName, fields, targetDir);
311
+ console.log(chalk.green(`\nāœ… Successfully updated fields for module "${kebabName}"!`));
312
+ managing = false; // exit loop after successful save
313
+
314
+ const { runDbNow } = await inquirer.prompt([{
315
+ type: 'confirm',
316
+ name: 'runDbNow',
317
+ message: 'Run database migration & seed now?',
318
+ default: false,
319
+ }]);
320
+
321
+ if (runDbNow) {
322
+ await runDatabaseMigration(targetDir, orm, pm);
323
+ await runDatabaseSeed(targetDir, orm, pm);
324
+ }
325
+ } else if (action === 'migrate') {
326
+ await runDatabaseMigration(targetDir, orm, pm);
327
+ } else if (action === 'seed') {
328
+ await runDatabaseSeed(targetDir, orm, pm);
329
+ } else if (action === 'cancel') {
330
+ console.log(chalk.gray('Exited field manager.'));
331
+ managing = false;
332
+ }
333
+ }
334
+ } catch (error) {
335
+ console.error(chalk.red('\nāŒ Field management failed:'), error.message);
336
+ }
337
+ }
338
+
192
339
  module.exports = { manageFields };
package/src/index.js CHANGED
@@ -1,13 +1,22 @@
1
- /**
2
- * Module exports for CLI source files
3
- * @module src
4
- */
5
-
6
- module.exports = {
7
- ...require('./constants'),
8
- ...require('./utils'),
9
- ...require('./prompts'),
10
- ...require('./generator'),
11
- ...require('./postSetup'),
12
- ...require('./moduleGenerator'),
13
- };
1
+ /**
2
+ * Module exports for CLI source files
3
+ * @module src
4
+ */
5
+
6
+ const constants = require('./constants');
7
+ const utils = require('./utils');
8
+ const prompts = require('./prompts');
9
+ const generator = require('./generator');
10
+ const postSetup = require('./postSetup');
11
+ const moduleGenerator = require('./moduleGenerator');
12
+ const fieldManager = require('./fieldManager');
13
+
14
+ module.exports = {
15
+ ...constants,
16
+ ...utils,
17
+ ...prompts,
18
+ ...generator,
19
+ ...postSetup,
20
+ ...moduleGenerator,
21
+ ...fieldManager,
22
+ };