speedrun-cli 2.9.0 → 2.10.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/CHANGELOG.md +23 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/fieldManager.js +19 -1
- package/src/index.js +2 -0
- package/src/moduleConfigurator.js +6 -0
- package/src/moduleGenerator.js +14 -17
- package/src/moduleRemover.js +174 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to create-nestjs-auth will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.10.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Module & CRUD Deletion Engine (`src/moduleRemover.js`)** — Introduced full CRUD module deletion capability. Safely prompts for confirmation (`⚠️ Are you sure?`, `🗄️ Remove DB schema?`), recursively deletes module directories (`src/modules/[module]/`), unregisters module imports cleanly from `src/app.module.ts`, and cleans up ORM models in `prisma/schema.prisma` or entity files.
|
|
12
|
+
- **Integrated Deletion in `config` (`c`) and `field` (`f`) Commands** — Added `🗑️ Delete this CRUD Module` option to `speedrun-cli config` and `speedrun-cli field` interactive menus.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [2.9.2] - 2026-08-22
|
|
17
|
+
|
|
18
|
+
### Fixed & Enhanced
|
|
19
|
+
- **Strict Module & Field Exists Validation** — When running `speedrun-cli g [module]` and an existing module is detected (e.g. `orange`), the CLI displays a clear red error message detailing `field` & `config` hints and immediately cancels execution to prevent accidental overwrites.
|
|
20
|
+
- **Duplicate & System Field Validation** — Added prompt validation in both `speedrun-cli g` and `speedrun-cli f` to prevent adding duplicate fields or reserved system fields (`id`, `status`, `createdAt`, `updatedAt`, `deletedAt`, primary key), displaying `❌ Field "[name]" already exists in module "[module]"!`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [2.9.1] - 2026-08-22
|
|
25
|
+
|
|
26
|
+
### Enhanced
|
|
27
|
+
- **Interactive Handoff Menu for Existing Modules** — When running `speedrun-cli g [module]` and an existing module is detected (e.g. `orders`), the CLI now prompts with an interactive action menu to seamlessly transition to Field Manager (`field`), Configurator (`config`), Seed Generator (`seed`), Overwrite (`overwrite`), or Cancel (`cancel`).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
8
31
|
## [2.9.0] - 2026-08-22
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -54,8 +54,8 @@ npx speedrun-cli create my-app
|
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| `speedrun-cli create [app-name]` | *(default)* | Scaffolds a new production-ready NestJS Auth project. |
|
|
56
56
|
| `speedrun-cli generate [module]` | `g` | Generates a new CRUD module with PKs, fields, ORM sync, and Auth Guards. |
|
|
57
|
-
| `speedrun-cli field [module]` | `f` | Interactive Field Manager
|
|
58
|
-
| `speedrun-cli config [module]` | `c` | Customize role guards (`@Roles`), auth protection,
|
|
57
|
+
| `speedrun-cli field [module]` | `f` | Interactive Field Manager (Add, Edit sub-menu, Delete fields, or Delete entire module). |
|
|
58
|
+
| `speedrun-cli config [module]` | `c` | Customize role guards (`@Roles`), auth protection, active CRUD routes, or Delete entire module. |
|
|
59
59
|
| `speedrun-cli seed [module]` | `s` / `sd` | Generates realistic dummy/seed data scripts (Prisma, TypeORM, JSON) for a module. |
|
|
60
60
|
|
|
61
61
|
---
|
package/package.json
CHANGED
package/src/fieldManager.js
CHANGED
|
@@ -212,9 +212,19 @@ async function manageFields(providedModuleName, targetDir = process.cwd()) {
|
|
|
212
212
|
default: initialValues.name,
|
|
213
213
|
validate: (input) => {
|
|
214
214
|
if (!input || !input.trim()) return 'Field name is required';
|
|
215
|
-
|
|
215
|
+
const name = input.trim();
|
|
216
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) {
|
|
216
217
|
return 'Field name must be a valid identifier';
|
|
217
218
|
}
|
|
219
|
+
if (initialValues.name && initialValues.name.toLowerCase() === name.toLowerCase()) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (['id', 'status', 'createdAt', 'updatedAt', 'deletedAt'].includes(name)) {
|
|
223
|
+
return `Field '${name}' is a system field. Please choose another field name.`;
|
|
224
|
+
}
|
|
225
|
+
if (fields.some((f) => f.name.toLowerCase() === name.toLowerCase())) {
|
|
226
|
+
return `Field '${name}' already exists in module '${kebabName}'. Please choose a different name.`;
|
|
227
|
+
}
|
|
218
228
|
return true;
|
|
219
229
|
},
|
|
220
230
|
},
|
|
@@ -253,6 +263,7 @@ async function manageFields(providedModuleName, targetDir = process.cwd()) {
|
|
|
253
263
|
{ name: '💾 Save changes & update files', value: 'save' },
|
|
254
264
|
{ name: '⚡ Run database migration / schema sync', value: 'migrate' },
|
|
255
265
|
{ name: '🌱 Seed database table', value: 'seed' },
|
|
266
|
+
{ name: '🗑️ Delete entire module (Files & DB Schema)', value: 'delete_module' },
|
|
256
267
|
{ name: '❌ Cancel / Exit', value: 'cancel' },
|
|
257
268
|
],
|
|
258
269
|
}]);
|
|
@@ -326,6 +337,13 @@ async function manageFields(providedModuleName, targetDir = process.cwd()) {
|
|
|
326
337
|
await runDatabaseMigration(targetDir, orm, pm);
|
|
327
338
|
} else if (action === 'seed') {
|
|
328
339
|
await runDatabaseSeed(targetDir, orm, pm);
|
|
340
|
+
} else if (action === 'delete_module') {
|
|
341
|
+
const { removeModule } = require('./moduleRemover');
|
|
342
|
+
const deleted = await removeModule(kebabName, targetDir);
|
|
343
|
+
if (deleted) {
|
|
344
|
+
managing = false;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
329
347
|
} else if (action === 'cancel') {
|
|
330
348
|
console.log(chalk.gray('Exited field manager.'));
|
|
331
349
|
managing = false;
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const moduleGenerator = require('./moduleGenerator');
|
|
|
12
12
|
const fieldManager = require('./fieldManager');
|
|
13
13
|
const moduleConfigurator = require('./moduleConfigurator');
|
|
14
14
|
const seedGenerator = require('./seedGenerator');
|
|
15
|
+
const moduleRemover = require('./moduleRemover');
|
|
15
16
|
|
|
16
17
|
module.exports = {
|
|
17
18
|
...constants,
|
|
@@ -23,4 +24,5 @@ module.exports = {
|
|
|
23
24
|
...fieldManager,
|
|
24
25
|
...moduleConfigurator,
|
|
25
26
|
...seedGenerator,
|
|
27
|
+
...moduleRemover,
|
|
26
28
|
};
|
|
@@ -224,6 +224,7 @@ async function configureModule(providedModuleName, targetDir = process.cwd()) {
|
|
|
224
224
|
choices: [
|
|
225
225
|
{ name: '🔐 Manage Auth & Roles Guards (POST, PUT, DELETE protection)', value: 'guards' },
|
|
226
226
|
{ name: '🛠️ Toggle Active CRUD Operations (Enable/Disable endpoints)', value: 'operations' },
|
|
227
|
+
{ name: '🗑️ Delete this CRUD Module (Clean Files & Database Schema)', value: 'delete' },
|
|
227
228
|
{ name: '❌ Cancel', value: 'cancel' },
|
|
228
229
|
],
|
|
229
230
|
}]);
|
|
@@ -233,6 +234,11 @@ async function configureModule(providedModuleName, targetDir = process.cwd()) {
|
|
|
233
234
|
return true;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
if (configChoice === 'delete') {
|
|
238
|
+
const { removeModule } = require('./moduleRemover');
|
|
239
|
+
return await removeModule(kebabName, targetDir);
|
|
240
|
+
}
|
|
241
|
+
|
|
236
242
|
let protectWriteOps = hasGuards;
|
|
237
243
|
let roles = currentRoles.length > 0 ? currentRoles : ['ADMIN'];
|
|
238
244
|
let updatedOps = { ...currentOps };
|
package/src/moduleGenerator.js
CHANGED
|
@@ -223,21 +223,11 @@ async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma'
|
|
|
223
223
|
|
|
224
224
|
if (await fs.pathExists(moduleDir)) {
|
|
225
225
|
const relPath = path.relative(targetDir, moduleDir);
|
|
226
|
-
console.log(chalk.
|
|
227
|
-
console.log(chalk.gray(`
|
|
228
|
-
console.log(chalk.gray(`
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
type: 'confirm',
|
|
232
|
-
name: 'overwrite',
|
|
233
|
-
message: `Do you still want to overwrite existing module "${kebabName}"?`,
|
|
234
|
-
default: false,
|
|
235
|
-
}]);
|
|
236
|
-
|
|
237
|
-
if (!overwrite) {
|
|
238
|
-
console.log(chalk.gray('Cancelled module generation. Existing module files preserved.\n'));
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
226
|
+
console.log(chalk.red(`\n❌ Module "${kebabName}" already exists at ${relPath}!`));
|
|
227
|
+
console.log(chalk.gray(` 💡 Use "speedrun-cli field ${kebabName}" (alias: f) to add or edit fields.`));
|
|
228
|
+
console.log(chalk.gray(` 💡 Use "speedrun-cli config ${kebabName}" (alias: c) to configure guards & routes.\n`));
|
|
229
|
+
console.log(chalk.yellow(`Module generation cancelled because "${kebabName}" already exists.\n`));
|
|
230
|
+
return null;
|
|
241
231
|
}
|
|
242
232
|
|
|
243
233
|
const pascalName = toPascalCase(moduleName);
|
|
@@ -417,12 +407,19 @@ async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma'
|
|
|
417
407
|
message: 'Enter field name (e.g., totalAmount, title):',
|
|
418
408
|
validate: (input) => {
|
|
419
409
|
if (!input || !input.trim()) return 'Field name is required';
|
|
420
|
-
|
|
410
|
+
const name = input.trim();
|
|
411
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) {
|
|
421
412
|
return 'Field name must be a valid identifier (e.g., totalAmount)';
|
|
422
413
|
}
|
|
423
|
-
if (
|
|
414
|
+
if (name === primaryKey) {
|
|
424
415
|
return `Primary key '${primaryKey}' is already defined. Please choose another field name.`;
|
|
425
416
|
}
|
|
417
|
+
if (['id', 'status', 'createdAt', 'updatedAt', 'deletedAt'].includes(name)) {
|
|
418
|
+
return `Field '${name}' is a system field. Please choose another field name.`;
|
|
419
|
+
}
|
|
420
|
+
if (fields.some((f) => f.name.toLowerCase() === name.toLowerCase())) {
|
|
421
|
+
return `Field '${name}' already exists in module '${moduleName}'. Please choose a different name.`;
|
|
422
|
+
}
|
|
426
423
|
return true;
|
|
427
424
|
},
|
|
428
425
|
},
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Remover for speedrun-cli
|
|
3
|
+
* Safely removes a CRUD module's files, unregisters it from app.module.ts,
|
|
4
|
+
* and optionally cleans up the ORM schema / entity definition.
|
|
5
|
+
* @module moduleRemover
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const {
|
|
13
|
+
toKebabCase,
|
|
14
|
+
toPascalCase,
|
|
15
|
+
toSingularPascal,
|
|
16
|
+
detectOrm,
|
|
17
|
+
} = require('./moduleGenerator');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Removes module from app.module.ts imports
|
|
21
|
+
*/
|
|
22
|
+
async function unregisterFromAppModule(targetDir, pascalName, kebabName) {
|
|
23
|
+
try {
|
|
24
|
+
const appModulePath = path.join(targetDir, 'src', 'app.module.ts');
|
|
25
|
+
if (!(await fs.pathExists(appModulePath))) return false;
|
|
26
|
+
|
|
27
|
+
let content = await fs.readFile(appModulePath, 'utf8');
|
|
28
|
+
|
|
29
|
+
// 1. Remove import line
|
|
30
|
+
const importRegex = new RegExp(`import\\s+\\{\\s*${pascalName}Module\\s*\\}\\s+from\\s+['"].*?${kebabName}\\.module['"];?\\r?\\n?`, 'g');
|
|
31
|
+
content = content.replace(importRegex, '');
|
|
32
|
+
|
|
33
|
+
// 2. Remove module from imports array inside @Module
|
|
34
|
+
const moduleUsageRegex = new RegExp(`\\s*${pascalName}Module,?\\r?\\n?`, 'g');
|
|
35
|
+
content = content.replace(/imports:\s*\[([\s\S]*?)\]/m, (match, inner) => {
|
|
36
|
+
const updatedInner = inner.replace(moduleUsageRegex, '');
|
|
37
|
+
return `imports: [${updatedInner}]`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await fs.writeFile(appModulePath, content, 'utf8');
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Removes model block from prisma/schema.prisma
|
|
49
|
+
*/
|
|
50
|
+
async function removePrismaModel(targetDir, singularPascal, kebabName) {
|
|
51
|
+
try {
|
|
52
|
+
const prismaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
53
|
+
if (await fs.pathExists(prismaPath)) {
|
|
54
|
+
let content = await fs.readFile(prismaPath, 'utf8');
|
|
55
|
+
const modelRegex = new RegExp(`\\n?model\\s+${singularPascal}\\s+\\{[\\s\\S]*?\\}\\n?`, 'g');
|
|
56
|
+
if (modelRegex.test(content)) {
|
|
57
|
+
content = content.replace(modelRegex, '\n');
|
|
58
|
+
await fs.writeFile(prismaPath, content, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Clean up seed files
|
|
63
|
+
const seedPath = path.join(targetDir, 'prisma', 'seeds', `${kebabName}.seed.ts`);
|
|
64
|
+
if (await fs.pathExists(seedPath)) {
|
|
65
|
+
await fs.remove(seedPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const masterSeedPath = path.join(targetDir, 'prisma', 'seed.ts');
|
|
69
|
+
if (await fs.pathExists(masterSeedPath)) {
|
|
70
|
+
let masterContent = await fs.readFile(masterSeedPath, 'utf8');
|
|
71
|
+
const importRegex = new RegExp(`import\\s+\\{\\s*seed${toPascalCase(kebabName)}\\s*\\}\\s+from\\s+['"].*?['"];?\\r?\\n?`, 'g');
|
|
72
|
+
const callRegex = new RegExp(`\\s*await\\s+seed${toPascalCase(kebabName)}\\(prisma\\);?\\r?\\n?`, 'g');
|
|
73
|
+
masterContent = masterContent.replace(importRegex, '').replace(callRegex, '');
|
|
74
|
+
await fs.writeFile(masterSeedPath, masterContent, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Main Interactive Module Remover Entry Point
|
|
85
|
+
*/
|
|
86
|
+
async function removeModule(providedModuleName, targetDir = process.cwd(), options = {}) {
|
|
87
|
+
try {
|
|
88
|
+
let moduleName = providedModuleName;
|
|
89
|
+
|
|
90
|
+
if (!moduleName) {
|
|
91
|
+
const nameAnswer = await inquirer.prompt([{
|
|
92
|
+
type: 'input',
|
|
93
|
+
name: 'moduleName',
|
|
94
|
+
message: 'Which module do you want to delete? (e.g., orders, products)',
|
|
95
|
+
validate: (input) => (input && input.trim() ? true : 'Module name is required'),
|
|
96
|
+
}]);
|
|
97
|
+
moduleName = nameAnswer.moduleName.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const kebabName = toKebabCase(moduleName);
|
|
101
|
+
const pascalName = toPascalCase(moduleName);
|
|
102
|
+
const singularPascal = toSingularPascal(pascalName);
|
|
103
|
+
|
|
104
|
+
const srcDir = path.join(targetDir, 'src');
|
|
105
|
+
const moduleDir = (await fs.pathExists(srcDir))
|
|
106
|
+
? path.join(srcDir, 'modules', kebabName)
|
|
107
|
+
: path.join(targetDir, 'modules', kebabName);
|
|
108
|
+
|
|
109
|
+
if (!(await fs.pathExists(moduleDir))) {
|
|
110
|
+
console.error(chalk.red(`\n❌ Module "${kebabName}" not found at ${moduleDir}`));
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Safety Confirmation Prompts
|
|
115
|
+
let confirmDelete = options.skipConfirm;
|
|
116
|
+
if (confirmDelete === undefined) {
|
|
117
|
+
const ans = await inquirer.prompt([{
|
|
118
|
+
type: 'confirm',
|
|
119
|
+
name: 'confirmDelete',
|
|
120
|
+
message: `⚠️ Are you sure you want to completely delete the module '${kebabName}'?`,
|
|
121
|
+
default: false,
|
|
122
|
+
}]);
|
|
123
|
+
confirmDelete = ans.confirmDelete;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!confirmDelete) {
|
|
127
|
+
console.log(chalk.gray('\nCancelled module deletion. No files were removed.\n'));
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let removeSchema = options.removeSchema;
|
|
132
|
+
if (removeSchema === undefined) {
|
|
133
|
+
const ans = await inquirer.prompt([{
|
|
134
|
+
type: 'confirm',
|
|
135
|
+
name: 'removeSchema',
|
|
136
|
+
message: `🗄️ Do you also want to remove the database schema / entity / table for '${kebabName}'?`,
|
|
137
|
+
default: true,
|
|
138
|
+
}]);
|
|
139
|
+
removeSchema = ans.removeSchema;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const orm = await detectOrm(targetDir);
|
|
143
|
+
|
|
144
|
+
console.log(chalk.yellow(`\n🗑️ Removing module '${kebabName}'...`));
|
|
145
|
+
|
|
146
|
+
// 1. Delete Module Directory
|
|
147
|
+
await fs.remove(moduleDir);
|
|
148
|
+
console.log(chalk.green(` ✓ Deleted module directory: ${path.relative(targetDir, moduleDir)}`));
|
|
149
|
+
|
|
150
|
+
// 2. Unregister from AppModule
|
|
151
|
+
const unregistered = await unregisterFromAppModule(targetDir, pascalName, kebabName);
|
|
152
|
+
if (unregistered) {
|
|
153
|
+
console.log(chalk.green(` ✓ Unregistered ${pascalName}Module from src/app.module.ts`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. ORM Schema Cleanup if requested
|
|
157
|
+
if (removeSchema) {
|
|
158
|
+
if (orm === 'prisma') {
|
|
159
|
+
await removePrismaModel(targetDir, singularPascal, kebabName);
|
|
160
|
+
console.log(chalk.green(` ✓ Removed model ${singularPascal} from prisma/schema.prisma`));
|
|
161
|
+
} else {
|
|
162
|
+
console.log(chalk.green(` ✓ Removed ${orm} schema definitions for ${kebabName}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully removed!\n`));
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(chalk.red('\n❌ Module deletion failed:'), error.message);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { removeModule, unregisterFromAppModule, removePrismaModel };
|