servcraft 0.1.7 ā 0.3.1
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/.github/workflows/ci.yml +9 -4
- package/README.md +63 -2
- package/ROADMAP.md +86 -41
- package/dist/cli/index.cjs +1510 -172
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1516 -172
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/add-module.ts +36 -7
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/generate.ts +73 -1
- package/src/cli/commands/init.ts +29 -10
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/src/cli/utils/dry-run.ts +155 -0
- package/src/cli/utils/error-handler.ts +184 -0
- package/src/cli/utils/helpers.ts +13 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
package/package.json
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { EnvManager } from '../utils/env-manager.js';
|
|
17
17
|
import { TemplateManager } from '../utils/template-manager.js';
|
|
18
18
|
import { InteractivePrompt } from '../utils/interactive-prompt.js';
|
|
19
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
20
|
+
import { ErrorTypes, displayError, validateProject } from '../utils/error-handler.js';
|
|
19
21
|
|
|
20
22
|
// Pre-built modules that can be added
|
|
21
23
|
const AVAILABLE_MODULES = {
|
|
@@ -160,10 +162,17 @@ export const addModuleCommand = new Command('add')
|
|
|
160
162
|
.option('-f, --force', 'Force overwrite existing module')
|
|
161
163
|
.option('-u, --update', 'Update existing module (smart merge)')
|
|
162
164
|
.option('--skip-existing', 'Skip if module already exists')
|
|
165
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
163
166
|
.action(
|
|
164
167
|
async (
|
|
165
168
|
moduleName?: string,
|
|
166
|
-
options?: {
|
|
169
|
+
options?: {
|
|
170
|
+
list?: boolean;
|
|
171
|
+
force?: boolean;
|
|
172
|
+
update?: boolean;
|
|
173
|
+
skipExisting?: boolean;
|
|
174
|
+
dryRun?: boolean;
|
|
175
|
+
}
|
|
167
176
|
) => {
|
|
168
177
|
if (options?.list || !moduleName) {
|
|
169
178
|
console.log(chalk.bold('\nš¦ Available Modules:\n'));
|
|
@@ -180,11 +189,24 @@ export const addModuleCommand = new Command('add')
|
|
|
180
189
|
return;
|
|
181
190
|
}
|
|
182
191
|
|
|
192
|
+
// Enable dry-run mode if specified
|
|
193
|
+
const dryRun = DryRunManager.getInstance();
|
|
194
|
+
if (options?.dryRun) {
|
|
195
|
+
dryRun.enable();
|
|
196
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
const module = AVAILABLE_MODULES[moduleName as keyof typeof AVAILABLE_MODULES];
|
|
184
200
|
|
|
185
201
|
if (!module) {
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate project structure
|
|
207
|
+
const projectError = validateProject();
|
|
208
|
+
if (projectError) {
|
|
209
|
+
displayError(projectError);
|
|
188
210
|
return;
|
|
189
211
|
}
|
|
190
212
|
|
|
@@ -319,10 +341,17 @@ export const addModuleCommand = new Command('add')
|
|
|
319
341
|
}
|
|
320
342
|
}
|
|
321
343
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
344
|
+
if (!options?.dryRun) {
|
|
345
|
+
console.log('\nš Next steps:');
|
|
346
|
+
info(' 1. Configure environment variables in .env (if needed)');
|
|
347
|
+
info(' 2. Register the module in your main app file');
|
|
348
|
+
info(' 3. Run database migrations if needed');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Show dry-run summary if enabled
|
|
352
|
+
if (options?.dryRun) {
|
|
353
|
+
dryRun.printSummary();
|
|
354
|
+
}
|
|
326
355
|
} catch (err) {
|
|
327
356
|
spinner.fail('Failed to add module');
|
|
328
357
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable no-useless-escape */
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
const bashScript = `
|
|
6
|
+
# servcraft bash completion script
|
|
7
|
+
_servcraft_completions() {
|
|
8
|
+
local cur prev words cword
|
|
9
|
+
_init_completion || return
|
|
10
|
+
|
|
11
|
+
# Main commands
|
|
12
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
13
|
+
|
|
14
|
+
# Generate subcommands
|
|
15
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
16
|
+
|
|
17
|
+
case "\${words[1]}" in
|
|
18
|
+
generate|g)
|
|
19
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
20
|
+
COMPREPLY=( \$(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
21
|
+
fi
|
|
22
|
+
;;
|
|
23
|
+
add|remove|rm|update)
|
|
24
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
25
|
+
# Get available modules
|
|
26
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
27
|
+
COMPREPLY=( \$(compgen -W "\${modules}" -- "\${cur}") )
|
|
28
|
+
fi
|
|
29
|
+
;;
|
|
30
|
+
completion)
|
|
31
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
32
|
+
COMPREPLY=( \$(compgen -W "bash zsh" -- "\${cur}") )
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
37
|
+
COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
|
|
38
|
+
fi
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
complete -F _servcraft_completions servcraft
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const zshScript = `
|
|
47
|
+
#compdef servcraft
|
|
48
|
+
|
|
49
|
+
_servcraft() {
|
|
50
|
+
local context state state_descr line
|
|
51
|
+
typeset -A opt_args
|
|
52
|
+
|
|
53
|
+
_arguments -C \\
|
|
54
|
+
'1: :_servcraft_commands' \\
|
|
55
|
+
'*::arg:->args'
|
|
56
|
+
|
|
57
|
+
case $state in
|
|
58
|
+
args)
|
|
59
|
+
case $line[1] in
|
|
60
|
+
generate|g)
|
|
61
|
+
_servcraft_generate
|
|
62
|
+
;;
|
|
63
|
+
add|remove|rm|update)
|
|
64
|
+
_servcraft_modules
|
|
65
|
+
;;
|
|
66
|
+
completion)
|
|
67
|
+
_arguments '1: :(bash zsh)'
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_servcraft_commands() {
|
|
75
|
+
local commands
|
|
76
|
+
commands=(
|
|
77
|
+
'init:Initialize a new ServCraft project'
|
|
78
|
+
'add:Add a pre-built module to your project'
|
|
79
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
80
|
+
'list:List available and installed modules'
|
|
81
|
+
'remove:Remove an installed module'
|
|
82
|
+
'doctor:Diagnose project configuration'
|
|
83
|
+
'update:Update installed modules'
|
|
84
|
+
'completion:Generate shell completion scripts'
|
|
85
|
+
'docs:Open documentation'
|
|
86
|
+
'--version:Show version'
|
|
87
|
+
'--help:Show help'
|
|
88
|
+
)
|
|
89
|
+
_describe 'command' commands
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_servcraft_generate() {
|
|
93
|
+
local subcommands
|
|
94
|
+
subcommands=(
|
|
95
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
96
|
+
'controller:Generate a controller'
|
|
97
|
+
'service:Generate a service'
|
|
98
|
+
'repository:Generate a repository'
|
|
99
|
+
'types:Generate TypeScript types'
|
|
100
|
+
'schema:Generate validation schema'
|
|
101
|
+
'routes:Generate routes file'
|
|
102
|
+
'm:Alias for module'
|
|
103
|
+
'c:Alias for controller'
|
|
104
|
+
's:Alias for service'
|
|
105
|
+
'r:Alias for repository'
|
|
106
|
+
't:Alias for types'
|
|
107
|
+
)
|
|
108
|
+
_describe 'subcommand' subcommands
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_servcraft_modules() {
|
|
112
|
+
local modules
|
|
113
|
+
modules=(
|
|
114
|
+
'auth:Authentication & Authorization'
|
|
115
|
+
'cache:Redis caching'
|
|
116
|
+
'rate-limit:Rate limiting'
|
|
117
|
+
'notification:Email/SMS notifications'
|
|
118
|
+
'payment:Payment integration'
|
|
119
|
+
'oauth:OAuth providers'
|
|
120
|
+
'mfa:Multi-factor authentication'
|
|
121
|
+
'queue:Background jobs'
|
|
122
|
+
'websocket:WebSocket support'
|
|
123
|
+
'upload:File upload handling'
|
|
124
|
+
)
|
|
125
|
+
_describe 'module' modules
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_servcraft "$@"
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
export const completionCommand = new Command('completion')
|
|
132
|
+
.description('Generate shell completion scripts')
|
|
133
|
+
.argument('<shell>', 'Shell type (bash or zsh)')
|
|
134
|
+
.action((shell: string) => {
|
|
135
|
+
const shellLower = shell.toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (shellLower === 'bash') {
|
|
138
|
+
console.log(bashScript);
|
|
139
|
+
} else if (shellLower === 'zsh') {
|
|
140
|
+
console.log(zshScript);
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
143
|
+
console.error('Supported shells: bash, zsh');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
interface Check {
|
|
7
|
+
name: string;
|
|
8
|
+
status: 'pass' | 'warn' | 'fail';
|
|
9
|
+
message: string;
|
|
10
|
+
suggestion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function checkNodeVersion(): Promise<Check> {
|
|
14
|
+
const version = process.version;
|
|
15
|
+
const major = parseInt(version.slice(1).split('.')[0] || '0', 10);
|
|
16
|
+
|
|
17
|
+
if (major >= 18) {
|
|
18
|
+
return { name: 'Node.js', status: 'pass', message: `${version} ā` };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: 'Node.js',
|
|
22
|
+
status: 'fail',
|
|
23
|
+
message: `${version} (< 18)`,
|
|
24
|
+
suggestion: 'Upgrade to Node.js 18+',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function checkPackageJson(): Promise<Check[]> {
|
|
29
|
+
const checks: Check[] = [];
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile('package.json', 'utf-8');
|
|
32
|
+
const pkg = JSON.parse(content);
|
|
33
|
+
|
|
34
|
+
checks.push({ name: 'package.json', status: 'pass', message: 'Found' });
|
|
35
|
+
|
|
36
|
+
if (pkg.dependencies?.fastify) {
|
|
37
|
+
checks.push({ name: 'Fastify', status: 'pass', message: 'Installed' });
|
|
38
|
+
} else {
|
|
39
|
+
checks.push({
|
|
40
|
+
name: 'Fastify',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
message: 'Missing',
|
|
43
|
+
suggestion: 'npm install fastify',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
checks.push({
|
|
48
|
+
name: 'package.json',
|
|
49
|
+
status: 'fail',
|
|
50
|
+
message: 'Not found',
|
|
51
|
+
suggestion: 'Run servcraft init',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return checks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkDirectories(): Promise<Check[]> {
|
|
58
|
+
const checks: Check[] = [];
|
|
59
|
+
const dirs = ['src', 'node_modules', '.git', '.env'];
|
|
60
|
+
|
|
61
|
+
for (const dir of dirs) {
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(dir);
|
|
64
|
+
checks.push({ name: dir, status: 'pass', message: 'Exists' });
|
|
65
|
+
} catch {
|
|
66
|
+
const isCritical = dir === 'src' || dir === 'node_modules';
|
|
67
|
+
checks.push({
|
|
68
|
+
name: dir,
|
|
69
|
+
status: isCritical ? 'fail' : 'warn',
|
|
70
|
+
message: 'Not found',
|
|
71
|
+
suggestion:
|
|
72
|
+
dir === 'node_modules' ? 'npm install' : dir === '.env' ? 'Create .env file' : undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return checks;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const doctorCommand = new Command('doctor')
|
|
80
|
+
.description('Diagnose project configuration and dependencies')
|
|
81
|
+
.action(async () => {
|
|
82
|
+
console.log(chalk.bold.cyan('\nš ServCraft Doctor\n'));
|
|
83
|
+
|
|
84
|
+
const allChecks: Check[] = [];
|
|
85
|
+
|
|
86
|
+
allChecks.push(await checkNodeVersion());
|
|
87
|
+
allChecks.push(...(await checkPackageJson()));
|
|
88
|
+
allChecks.push(...(await checkDirectories()));
|
|
89
|
+
|
|
90
|
+
// Display results
|
|
91
|
+
allChecks.forEach((check) => {
|
|
92
|
+
const icon =
|
|
93
|
+
check.status === 'pass'
|
|
94
|
+
? chalk.green('ā')
|
|
95
|
+
: check.status === 'warn'
|
|
96
|
+
? chalk.yellow('ā ')
|
|
97
|
+
: chalk.red('ā');
|
|
98
|
+
const color =
|
|
99
|
+
check.status === 'pass' ? chalk.green : check.status === 'warn' ? chalk.yellow : chalk.red;
|
|
100
|
+
|
|
101
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
102
|
+
if (check.suggestion) {
|
|
103
|
+
console.log(chalk.gray(` ā ${check.suggestion}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const pass = allChecks.filter((c) => c.status === 'pass').length;
|
|
108
|
+
const warn = allChecks.filter((c) => c.status === 'warn').length;
|
|
109
|
+
const fail = allChecks.filter((c) => c.status === 'fail').length;
|
|
110
|
+
|
|
111
|
+
console.log(chalk.gray('\n' + 'ā'.repeat(60)));
|
|
112
|
+
console.log(
|
|
113
|
+
`\n${chalk.green(pass + ' passed')} | ${chalk.yellow(warn + ' warnings')} | ${chalk.red(fail + ' failed')}\n`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (fail === 0 && warn === 0) {
|
|
117
|
+
console.log(chalk.green.bold('⨠Everything looks good!\n'));
|
|
118
|
+
} else if (fail > 0) {
|
|
119
|
+
console.log(chalk.red.bold('ā Fix critical issues before using ServCraft.\n'));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.yellow.bold('ā Some warnings, but should work.\n'));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -14,7 +14,26 @@ import {
|
|
|
14
14
|
info,
|
|
15
15
|
getModulesDir,
|
|
16
16
|
} from '../utils/helpers.js';
|
|
17
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
17
19
|
import { parseFields, type FieldDefinition } from '../utils/field-parser.js';
|
|
20
|
+
import { ErrorTypes, displayError } from '../utils/error-handler.js';
|
|
21
|
+
|
|
22
|
+
// Helper to enable dry-run mode
|
|
23
|
+
function enableDryRunIfNeeded(options: { dryRun?: boolean }): void {
|
|
24
|
+
const dryRun = DryRunManager.getInstance();
|
|
25
|
+
if (options.dryRun) {
|
|
26
|
+
dryRun.enable();
|
|
27
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helper to show dry-run summary
|
|
32
|
+
function showDryRunSummary(options: { dryRun?: boolean }): void {
|
|
33
|
+
if (options.dryRun) {
|
|
34
|
+
DryRunManager.getInstance().printSummary();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
18
37
|
import { controllerTemplate } from '../templates/controller.js';
|
|
19
38
|
import { serviceTemplate } from '../templates/service.js';
|
|
20
39
|
import { repositoryTemplate } from '../templates/repository.js';
|
|
@@ -26,6 +45,9 @@ import { prismaModelTemplate } from '../templates/prisma-model.js';
|
|
|
26
45
|
import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
|
|
27
46
|
import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
|
|
28
47
|
import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
48
|
+
import { controllerTestTemplate } from '../templates/controller-test.js';
|
|
49
|
+
import { serviceTestTemplate } from '../templates/service-test.js';
|
|
50
|
+
import { integrationTestTemplate } from '../templates/integration-test.js';
|
|
29
51
|
|
|
30
52
|
export const generateCommand = new Command('generate')
|
|
31
53
|
.alias('g')
|
|
@@ -43,7 +65,10 @@ generateCommand
|
|
|
43
65
|
.option('--prisma', 'Generate Prisma model suggestion')
|
|
44
66
|
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
45
67
|
.option('-i, --interactive', 'Interactive mode to define fields')
|
|
68
|
+
.option('--with-tests', 'Generate test files (__tests__ directory)')
|
|
69
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
46
70
|
.action(async (name: string, fieldsArgs: string[], options) => {
|
|
71
|
+
enableDryRunIfNeeded(options);
|
|
47
72
|
let fields: FieldDefinition[] = [];
|
|
48
73
|
|
|
49
74
|
// Parse fields from command line or interactive mode
|
|
@@ -118,6 +143,26 @@ generateCommand
|
|
|
118
143
|
await writeFile(path.join(moduleDir, file.name), file.content);
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
// Generate test files if --with-tests flag is provided
|
|
147
|
+
if (options.withTests) {
|
|
148
|
+
const testDir = path.join(moduleDir, '__tests__');
|
|
149
|
+
|
|
150
|
+
await writeFile(
|
|
151
|
+
path.join(testDir, `${kebabName}.controller.test.ts`),
|
|
152
|
+
controllerTestTemplate(kebabName, pascalName, camelName)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await writeFile(
|
|
156
|
+
path.join(testDir, `${kebabName}.service.test.ts`),
|
|
157
|
+
serviceTestTemplate(kebabName, pascalName, camelName)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await writeFile(
|
|
161
|
+
path.join(testDir, `${kebabName}.integration.test.ts`),
|
|
162
|
+
integrationTestTemplate(kebabName, pascalName, camelName)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
122
167
|
|
|
123
168
|
// Show Prisma model if requested or fields provided
|
|
@@ -148,6 +193,12 @@ generateCommand
|
|
|
148
193
|
console.log('\nš Files created:');
|
|
149
194
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
150
195
|
|
|
196
|
+
if (options.withTests) {
|
|
197
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
198
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
199
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
200
|
+
}
|
|
201
|
+
|
|
151
202
|
console.log('\nš Next steps:');
|
|
152
203
|
if (!hasFields) {
|
|
153
204
|
info(' 1. Update the types in ' + `${kebabName}.types.ts`);
|
|
@@ -161,6 +212,9 @@ generateCommand
|
|
|
161
212
|
info(` ${hasFields ? '3' : '4'}. Add the Prisma model to schema.prisma`);
|
|
162
213
|
info(` ${hasFields ? '4' : '5'}. Run: npm run db:migrate`);
|
|
163
214
|
}
|
|
215
|
+
|
|
216
|
+
// Show dry-run summary if enabled
|
|
217
|
+
showDryRunSummary(options);
|
|
164
218
|
} catch (err) {
|
|
165
219
|
spinner.fail('Failed to generate module');
|
|
166
220
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -173,7 +227,9 @@ generateCommand
|
|
|
173
227
|
.alias('c')
|
|
174
228
|
.description('Generate a controller')
|
|
175
229
|
.option('-m, --module <module>', 'Target module name')
|
|
230
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
176
231
|
.action(async (name: string, options) => {
|
|
232
|
+
enableDryRunIfNeeded(options);
|
|
177
233
|
const spinner = ora('Generating controller...').start();
|
|
178
234
|
|
|
179
235
|
try {
|
|
@@ -187,7 +243,7 @@ generateCommand
|
|
|
187
243
|
|
|
188
244
|
if (await fileExists(filePath)) {
|
|
189
245
|
spinner.stop();
|
|
190
|
-
|
|
246
|
+
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
191
247
|
return;
|
|
192
248
|
}
|
|
193
249
|
|
|
@@ -195,6 +251,7 @@ generateCommand
|
|
|
195
251
|
|
|
196
252
|
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
197
253
|
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
254
|
+
showDryRunSummary(options);
|
|
198
255
|
} catch (err) {
|
|
199
256
|
spinner.fail('Failed to generate controller');
|
|
200
257
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -207,7 +264,9 @@ generateCommand
|
|
|
207
264
|
.alias('s')
|
|
208
265
|
.description('Generate a service')
|
|
209
266
|
.option('-m, --module <module>', 'Target module name')
|
|
267
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
210
268
|
.action(async (name: string, options) => {
|
|
269
|
+
enableDryRunIfNeeded(options);
|
|
211
270
|
const spinner = ora('Generating service...').start();
|
|
212
271
|
|
|
213
272
|
try {
|
|
@@ -229,6 +288,7 @@ generateCommand
|
|
|
229
288
|
|
|
230
289
|
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
231
290
|
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
291
|
+
showDryRunSummary(options);
|
|
232
292
|
} catch (err) {
|
|
233
293
|
spinner.fail('Failed to generate service');
|
|
234
294
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -241,7 +301,9 @@ generateCommand
|
|
|
241
301
|
.alias('r')
|
|
242
302
|
.description('Generate a repository')
|
|
243
303
|
.option('-m, --module <module>', 'Target module name')
|
|
304
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
244
305
|
.action(async (name: string, options) => {
|
|
306
|
+
enableDryRunIfNeeded(options);
|
|
245
307
|
const spinner = ora('Generating repository...').start();
|
|
246
308
|
|
|
247
309
|
try {
|
|
@@ -264,6 +326,7 @@ generateCommand
|
|
|
264
326
|
|
|
265
327
|
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
266
328
|
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
329
|
+
showDryRunSummary(options);
|
|
267
330
|
} catch (err) {
|
|
268
331
|
spinner.fail('Failed to generate repository');
|
|
269
332
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -276,7 +339,9 @@ generateCommand
|
|
|
276
339
|
.alias('t')
|
|
277
340
|
.description('Generate types/interfaces')
|
|
278
341
|
.option('-m, --module <module>', 'Target module name')
|
|
342
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
279
343
|
.action(async (name: string, options) => {
|
|
344
|
+
enableDryRunIfNeeded(options);
|
|
280
345
|
const spinner = ora('Generating types...').start();
|
|
281
346
|
|
|
282
347
|
try {
|
|
@@ -297,6 +362,7 @@ generateCommand
|
|
|
297
362
|
|
|
298
363
|
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
299
364
|
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
365
|
+
showDryRunSummary(options);
|
|
300
366
|
} catch (err) {
|
|
301
367
|
spinner.fail('Failed to generate types');
|
|
302
368
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -309,7 +375,9 @@ generateCommand
|
|
|
309
375
|
.alias('v')
|
|
310
376
|
.description('Generate validation schemas')
|
|
311
377
|
.option('-m, --module <module>', 'Target module name')
|
|
378
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
312
379
|
.action(async (name: string, options) => {
|
|
380
|
+
enableDryRunIfNeeded(options);
|
|
313
381
|
const spinner = ora('Generating schemas...').start();
|
|
314
382
|
|
|
315
383
|
try {
|
|
@@ -331,6 +399,7 @@ generateCommand
|
|
|
331
399
|
|
|
332
400
|
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
333
401
|
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
402
|
+
showDryRunSummary(options);
|
|
334
403
|
} catch (err) {
|
|
335
404
|
spinner.fail('Failed to generate schemas');
|
|
336
405
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -342,7 +411,9 @@ generateCommand
|
|
|
342
411
|
.command('routes <name>')
|
|
343
412
|
.description('Generate routes')
|
|
344
413
|
.option('-m, --module <module>', 'Target module name')
|
|
414
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
345
415
|
.action(async (name: string, options) => {
|
|
416
|
+
enableDryRunIfNeeded(options);
|
|
346
417
|
const spinner = ora('Generating routes...').start();
|
|
347
418
|
|
|
348
419
|
try {
|
|
@@ -365,6 +436,7 @@ generateCommand
|
|
|
365
436
|
|
|
366
437
|
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
367
438
|
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
439
|
+
showDryRunSummary(options);
|
|
368
440
|
} catch (err) {
|
|
369
441
|
spinner.fail('Failed to generate routes');
|
|
370
442
|
error(err instanceof Error ? err.message : String(err));
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -6,6 +6,7 @@ import inquirer from 'inquirer';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
import { ensureDir, writeFile, error, warn } from '../utils/helpers.js';
|
|
9
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
9
10
|
|
|
10
11
|
interface InitOptions {
|
|
11
12
|
name: string;
|
|
@@ -27,6 +28,7 @@ export const initCommand = new Command('init')
|
|
|
27
28
|
.option('--esm', 'Use ES Modules (import/export) - default')
|
|
28
29
|
.option('--cjs, --commonjs', 'Use CommonJS (require/module.exports)')
|
|
29
30
|
.option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
|
|
31
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
30
32
|
.action(
|
|
31
33
|
async (
|
|
32
34
|
name?: string,
|
|
@@ -37,8 +39,16 @@ export const initCommand = new Command('init')
|
|
|
37
39
|
esm?: boolean;
|
|
38
40
|
commonjs?: boolean;
|
|
39
41
|
db?: string;
|
|
42
|
+
dryRun?: boolean;
|
|
40
43
|
}
|
|
41
44
|
) => {
|
|
45
|
+
// Enable dry-run mode if specified
|
|
46
|
+
const dryRun = DryRunManager.getInstance();
|
|
47
|
+
if (cmdOptions?.dryRun) {
|
|
48
|
+
dryRun.enable();
|
|
49
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
console.log(
|
|
43
53
|
chalk.blue(`
|
|
44
54
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -274,19 +284,23 @@ export const initCommand = new Command('init')
|
|
|
274
284
|
|
|
275
285
|
spinner.succeed('Project files generated!');
|
|
276
286
|
|
|
277
|
-
// Install dependencies
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
// Install dependencies (skip in dry-run mode)
|
|
288
|
+
if (!cmdOptions?.dryRun) {
|
|
289
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
|
|
293
|
+
installSpinner.succeed('Dependencies installed!');
|
|
294
|
+
} catch {
|
|
295
|
+
installSpinner.warn('Failed to install dependencies automatically');
|
|
296
|
+
warn(' Run "npm install" manually in the project directory');
|
|
297
|
+
}
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
// Print success message
|
|
289
|
-
|
|
301
|
+
if (!cmdOptions?.dryRun) {
|
|
302
|
+
console.log('\n' + chalk.green('⨠Project created successfully!'));
|
|
303
|
+
}
|
|
290
304
|
console.log('\n' + chalk.bold('š Project structure:'));
|
|
291
305
|
console.log(`
|
|
292
306
|
${options.name}/
|
|
@@ -317,6 +331,11 @@ export const initCommand = new Command('init')
|
|
|
317
331
|
${chalk.yellow('servcraft generate service <name>')} Generate a service
|
|
318
332
|
${chalk.yellow('servcraft add auth')} Add authentication module
|
|
319
333
|
`);
|
|
334
|
+
|
|
335
|
+
// Show dry-run summary if enabled
|
|
336
|
+
if (cmdOptions?.dryRun) {
|
|
337
|
+
dryRun.printSummary();
|
|
338
|
+
}
|
|
320
339
|
} catch (err) {
|
|
321
340
|
spinner.fail('Failed to create project');
|
|
322
341
|
error(err instanceof Error ? err.message : String(err));
|