speedrun-cli 2.7.6 β†’ 2.7.8

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/bin/cli.js CHANGED
@@ -28,6 +28,7 @@ const {
28
28
  handlePostSetup,
29
29
  printManualInstructions,
30
30
  generateModule,
31
+ manageFields,
31
32
  } = require(path.join(packageRoot, 'src'));
32
33
 
33
34
  program
@@ -50,6 +51,21 @@ program
50
51
  }
51
52
  });
52
53
 
54
+ // ==================== FIELD MANAGEMENT COMMAND ====================
55
+ program
56
+ .command('field [module-name]')
57
+ .alias('f')
58
+ .description('Manage (add, edit, delete) fields of an existing module')
59
+ .action(async (moduleName) => {
60
+ try {
61
+ console.log(chalk.cyan(`\n😱🀯🀯 speedrun-cli v${CLI_VERSION} field manager 🀧πŸ₯ΆπŸ₯ΆπŸ₯Ά (real)\n`));
62
+ await manageFields(moduleName, process.cwd());
63
+ } catch (error) {
64
+ console.error(chalk.red('\n❌ Field management failed:'));
65
+ console.error(error);
66
+ }
67
+ });
68
+
53
69
  // ==================== MAIN SCAFFOLD COMMAND (DEFAULT) ====================
54
70
  program
55
71
  .command('create [app-name]', { isDefault: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedrun-cli",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
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",
@@ -0,0 +1,192 @@
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
+
192
+ module.exports = { manageFields };
@@ -199,6 +199,7 @@ async function promptForModuleOptions(providedModuleName) {
199
199
  selectedOperations = customOps;
200
200
  }
201
201
 
202
+ // Interactive Field Builder Loop
202
203
  // Interactive Field Builder Loop
203
204
  const { addCustomFields } = await inquirer.prompt([{
204
205
  type: 'confirm',
@@ -210,13 +211,16 @@ async function promptForModuleOptions(providedModuleName) {
210
211
  const fields = [];
211
212
 
212
213
  if (addCustomFields) {
213
- let addAnother = true;
214
- while (addAnother) {
215
- 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([
216
219
  {
217
220
  type: 'input',
218
221
  name: 'fieldName',
219
222
  message: 'Enter field name (e.g., totalAmount, title):',
223
+ default: initialValues.name,
220
224
  validate: (input) => {
221
225
  if (!input || !input.trim()) return 'Field name is required';
222
226
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
@@ -230,30 +234,96 @@ async function promptForModuleOptions(providedModuleName) {
230
234
  name: 'fieldType',
231
235
  message: (answers) => `Select field type for '${answers.fieldName}':`,
232
236
  choices: ['String', 'Number', 'Boolean', 'Date'],
233
- default: 'String',
237
+ default: initialValues.type || 'String',
234
238
  },
235
239
  {
236
240
  type: 'confirm',
237
241
  name: 'isOptional',
238
242
  message: (answers) => `Is '${answers.fieldName}' optional?`,
239
- default: false,
243
+ default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
240
244
  },
241
245
  ]);
246
+ };
242
247
 
243
- fields.push({
244
- name: fieldAnswers.fieldName.trim(),
245
- type: fieldAnswers.fieldType,
246
- isOptional: fieldAnswers.isOptional,
247
- });
248
-
249
- const { continueLoop } = await inquirer.prompt([{
250
- type: 'confirm',
251
- name: 'continueLoop',
252
- message: 'Do you want to add another field?',
253
- default: false,
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
+ });
256
+
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
+ ],
254
271
  }]);
255
272
 
256
- 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
+ }
257
327
  }
258
328
  }
259
329
 
@@ -261,12 +331,6 @@ async function promptForModuleOptions(providedModuleName) {
261
331
  if (fields.length === 0) {
262
332
  fields.push({ name: 'name', type: 'String', isOptional: false });
263
333
  }
264
-
265
- return {
266
- moduleName,
267
- operations: selectedOperations,
268
- fields,
269
- };
270
334
  }
271
335
 
272
336
  function getFieldExampleValue(field, pascalName) {