servcraft 0.2.0 → 0.4.2

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": "servcraft",
3
- "version": "0.2.0",
3
+ "version": "0.4.2",
4
4
  "description": "A modular, production-ready Node.js backend framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
+ });
@@ -1,8 +1,123 @@
1
+ /* eslint-disable no-console */
1
2
  import { Command } from 'commander';
2
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
+ }
3
78
 
4
79
  export const doctorCommand = new Command('doctor')
5
80
  .description('Diagnose project configuration and dependencies')
6
81
  .action(async () => {
7
- console.log(chalk.bold.cyan('\nServCraft Doctor - Coming soon!\n'));
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
+ }
8
123
  });
@@ -45,6 +45,10 @@ import { prismaModelTemplate } from '../templates/prisma-model.js';
45
45
  import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
46
46
  import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
47
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';
51
+ import { getTemplate } from '../utils/template-loader.js';
48
52
 
49
53
  export const generateCommand = new Command('generate')
50
54
  .alias('g')
@@ -62,6 +66,7 @@ generateCommand
62
66
  .option('--prisma', 'Generate Prisma model suggestion')
63
67
  .option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
64
68
  .option('-i, --interactive', 'Interactive mode to define fields')
69
+ .option('--with-tests', 'Generate test files (__tests__ directory)')
65
70
  .option('--dry-run', 'Preview changes without writing files')
66
71
  .action(async (name: string, fieldsArgs: string[], options) => {
67
72
  enableDryRunIfNeeded(options);
@@ -96,41 +101,50 @@ generateCommand
96
101
  // Use dynamic templates if fields are provided
97
102
  const hasFields = fields.length > 0;
98
103
 
104
+ // Load templates (custom or built-in)
105
+ const controllerTpl = await getTemplate('controller', controllerTemplate);
106
+ const serviceTpl = await getTemplate('service', serviceTemplate);
107
+ const repositoryTpl = await getTemplate('repository', repositoryTemplate);
108
+ const typesTpl = await getTemplate('types', typesTemplate);
109
+ const schemasTpl = await getTemplate('schemas', schemasTemplate);
110
+ const routesTpl = await getTemplate('routes', routesTemplate);
111
+ const moduleIndexTpl = await getTemplate('module-index', moduleIndexTemplate);
112
+
99
113
  const files = [
100
114
  {
101
115
  name: `${kebabName}.types.ts`,
102
116
  content: hasFields
103
117
  ? dynamicTypesTemplate(kebabName, pascalName, fields)
104
- : typesTemplate(kebabName, pascalName),
118
+ : typesTpl(kebabName, pascalName),
105
119
  },
106
120
  {
107
121
  name: `${kebabName}.schemas.ts`,
108
122
  content: hasFields
109
123
  ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType)
110
- : schemasTemplate(kebabName, pascalName, camelName),
124
+ : schemasTpl(kebabName, pascalName, camelName),
111
125
  },
112
126
  {
113
127
  name: `${kebabName}.service.ts`,
114
- content: serviceTemplate(kebabName, pascalName, camelName),
128
+ content: serviceTpl(kebabName, pascalName, camelName),
115
129
  },
116
130
  {
117
131
  name: `${kebabName}.controller.ts`,
118
- content: controllerTemplate(kebabName, pascalName, camelName),
132
+ content: controllerTpl(kebabName, pascalName, camelName),
119
133
  },
120
- { name: 'index.ts', content: moduleIndexTemplate(kebabName, pascalName, camelName) },
134
+ { name: 'index.ts', content: moduleIndexTpl(kebabName, pascalName, camelName) },
121
135
  ];
122
136
 
123
137
  if (options.repository !== false) {
124
138
  files.push({
125
139
  name: `${kebabName}.repository.ts`,
126
- content: repositoryTemplate(kebabName, pascalName, camelName, pluralName),
140
+ content: repositoryTpl(kebabName, pascalName, camelName, pluralName),
127
141
  });
128
142
  }
129
143
 
130
144
  if (options.routes !== false) {
131
145
  files.push({
132
146
  name: `${kebabName}.routes.ts`,
133
- content: routesTemplate(kebabName, pascalName, camelName, pluralName),
147
+ content: routesTpl(kebabName, pascalName, camelName, pluralName),
134
148
  });
135
149
  }
136
150
 
@@ -139,6 +153,31 @@ generateCommand
139
153
  await writeFile(path.join(moduleDir, file.name), file.content);
140
154
  }
141
155
 
156
+ // Generate test files if --with-tests flag is provided
157
+ if (options.withTests) {
158
+ const testDir = path.join(moduleDir, '__tests__');
159
+
160
+ // Load test templates (custom or built-in)
161
+ const controllerTestTpl = await getTemplate('controller-test', controllerTestTemplate);
162
+ const serviceTestTpl = await getTemplate('service-test', serviceTestTemplate);
163
+ const integrationTestTpl = await getTemplate('integration-test', integrationTestTemplate);
164
+
165
+ await writeFile(
166
+ path.join(testDir, `${kebabName}.controller.test.ts`),
167
+ controllerTestTpl(kebabName, pascalName, camelName)
168
+ );
169
+
170
+ await writeFile(
171
+ path.join(testDir, `${kebabName}.service.test.ts`),
172
+ serviceTestTpl(kebabName, pascalName, camelName)
173
+ );
174
+
175
+ await writeFile(
176
+ path.join(testDir, `${kebabName}.integration.test.ts`),
177
+ integrationTestTpl(kebabName, pascalName, camelName)
178
+ );
179
+ }
180
+
142
181
  spinner.succeed(`Module "${pascalName}" generated successfully!`);
143
182
 
144
183
  // Show Prisma model if requested or fields provided
@@ -169,6 +208,12 @@ generateCommand
169
208
  console.log('\nšŸ“ Files created:');
170
209
  files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
171
210
 
211
+ if (options.withTests) {
212
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
213
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
214
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
215
+ }
216
+
172
217
  console.log('\nšŸ“Œ Next steps:');
173
218
  if (!hasFields) {
174
219
  info(' 1. Update the types in ' + `${kebabName}.types.ts`);
@@ -193,7 +193,7 @@ export const listCommand = new Command('list')
193
193
  byCategory[mod.category] = [];
194
194
  }
195
195
 
196
- byCategory[mod.category].push({
196
+ byCategory[mod.category]?.push({
197
197
  id: key,
198
198
  name: mod.name,
199
199
  description: mod.description,
@@ -0,0 +1,211 @@
1
+ /* eslint-disable no-console */
2
+ import { Command } from 'commander';
3
+ import path from 'path';
4
+ import ora from 'ora';
5
+ import chalk from 'chalk';
6
+ import {
7
+ toPascalCase,
8
+ toCamelCase,
9
+ toKebabCase,
10
+ pluralize,
11
+ writeFile,
12
+ success,
13
+ info,
14
+ getModulesDir,
15
+ } from '../utils/helpers.js';
16
+ import { DryRunManager } from '../utils/dry-run.js';
17
+ import { parseFields } from '../utils/field-parser.js';
18
+ import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
19
+ import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
20
+ import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
21
+ import { controllerTemplate } from '../templates/controller.js';
22
+ import { serviceTemplate } from '../templates/service.js';
23
+ import { repositoryTemplate } from '../templates/repository.js';
24
+ import { routesTemplate } from '../templates/routes.js';
25
+ import { moduleIndexTemplate } from '../templates/module-index.js';
26
+ import { controllerTestTemplate } from '../templates/controller-test.js';
27
+ import { serviceTestTemplate } from '../templates/service-test.js';
28
+ import { integrationTestTemplate } from '../templates/integration-test.js';
29
+ import { getTemplate } from '../utils/template-loader.js';
30
+
31
+ export const scaffoldCommand = new Command('scaffold')
32
+ .description('Generate complete CRUD with Prisma model')
33
+ .argument('<name>', 'Resource name (e.g., product, user)')
34
+ .option(
35
+ '--fields <fields>',
36
+ 'Field definitions: "name:string email:string? age:number category:relation"'
37
+ )
38
+ .option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
39
+ .option('--dry-run', 'Preview changes without writing files')
40
+ .action(
41
+ async (
42
+ name: string,
43
+ options: { fields?: string; validator?: ValidatorType; dryRun?: boolean }
44
+ ) => {
45
+ const dryRun = DryRunManager.getInstance();
46
+ if (options.dryRun) {
47
+ dryRun.enable();
48
+ console.log(chalk.yellow('\n⚠ DRY RUN MODE - No files will be written\n'));
49
+ }
50
+
51
+ if (!options.fields) {
52
+ console.log(chalk.red('\nāœ— Error: --fields option is required\n'));
53
+ console.log(chalk.gray('Example:'));
54
+ console.log(
55
+ chalk.cyan(
56
+ ' servcraft scaffold product --fields "name:string price:number category:relation"'
57
+ )
58
+ );
59
+ process.exit(1);
60
+ }
61
+
62
+ const spinner = ora('Scaffolding resource...').start();
63
+
64
+ try {
65
+ // Parse fields
66
+ const fields = parseFields(options.fields || '');
67
+
68
+ if (!fields || fields.length === 0) {
69
+ spinner.fail('No valid fields provided');
70
+ console.log(chalk.gray(`\nReceived: ${options.fields}`));
71
+ console.log(chalk.gray(`Parsed: ${JSON.stringify(fields)}`));
72
+ process.exit(1);
73
+ }
74
+
75
+ // Generate names
76
+ const kebabName = toKebabCase(name);
77
+ const pascalName = toPascalCase(name);
78
+ const camelName = toCamelCase(name);
79
+ const pluralName = pluralize(camelName);
80
+ const tableName = pluralize(kebabName);
81
+
82
+ // Create module directory
83
+ const modulesDir = getModulesDir();
84
+ const moduleDir = path.join(modulesDir, kebabName);
85
+
86
+ // Load templates (custom or built-in)
87
+ const controllerTpl = await getTemplate('controller', controllerTemplate);
88
+ const serviceTpl = await getTemplate('service', serviceTemplate);
89
+ const repositoryTpl = await getTemplate('repository', repositoryTemplate);
90
+ const routesTpl = await getTemplate('routes', routesTemplate);
91
+ const moduleIndexTpl = await getTemplate('module-index', moduleIndexTemplate);
92
+
93
+ // Generate all files
94
+ const files = [
95
+ {
96
+ name: `${kebabName}.types.ts`,
97
+ content: dynamicTypesTemplate(kebabName, pascalName, fields),
98
+ },
99
+ {
100
+ name: `${kebabName}.schemas.ts`,
101
+ content: dynamicSchemasTemplate(
102
+ kebabName,
103
+ pascalName,
104
+ camelName,
105
+ fields,
106
+ options.validator || 'zod'
107
+ ),
108
+ },
109
+ {
110
+ name: `${kebabName}.service.ts`,
111
+ content: serviceTpl(kebabName, pascalName, camelName),
112
+ },
113
+ {
114
+ name: `${kebabName}.controller.ts`,
115
+ content: controllerTpl(kebabName, pascalName, camelName),
116
+ },
117
+ {
118
+ name: 'index.ts',
119
+ content: moduleIndexTpl(kebabName, pascalName, camelName),
120
+ },
121
+ {
122
+ name: `${kebabName}.repository.ts`,
123
+ content: repositoryTpl(kebabName, pascalName, camelName, pluralName),
124
+ },
125
+ {
126
+ name: `${kebabName}.routes.ts`,
127
+ content: routesTpl(kebabName, pascalName, camelName, pluralName),
128
+ },
129
+ ];
130
+
131
+ // Write all files
132
+ for (const file of files) {
133
+ await writeFile(path.join(moduleDir, file.name), file.content);
134
+ }
135
+
136
+ // Generate test files
137
+ const testDir = path.join(moduleDir, '__tests__');
138
+
139
+ // Load test templates (custom or built-in)
140
+ const controllerTestTpl = await getTemplate('controller-test', controllerTestTemplate);
141
+ const serviceTestTpl = await getTemplate('service-test', serviceTestTemplate);
142
+ const integrationTestTpl = await getTemplate('integration-test', integrationTestTemplate);
143
+
144
+ await writeFile(
145
+ path.join(testDir, `${kebabName}.controller.test.ts`),
146
+ controllerTestTpl(kebabName, pascalName, camelName)
147
+ );
148
+
149
+ await writeFile(
150
+ path.join(testDir, `${kebabName}.service.test.ts`),
151
+ serviceTestTpl(kebabName, pascalName, camelName)
152
+ );
153
+
154
+ await writeFile(
155
+ path.join(testDir, `${kebabName}.integration.test.ts`),
156
+ integrationTestTpl(kebabName, pascalName, camelName)
157
+ );
158
+
159
+ spinner.succeed(`Resource "${pascalName}" scaffolded successfully!`);
160
+
161
+ // Show Prisma model
162
+ console.log('\n' + '─'.repeat(70));
163
+ info('šŸ“‹ Prisma model to add to schema.prisma:');
164
+ console.log(chalk.gray('\n// Copy this to your schema.prisma file:\n'));
165
+ console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
166
+ console.log('─'.repeat(70));
167
+
168
+ // Show fields summary
169
+ console.log('\nšŸ“‹ Fields scaffolded:');
170
+ fields.forEach((f) => {
171
+ const opts = [];
172
+ if (f.isOptional) opts.push('optional');
173
+ if (f.isArray) opts.push('array');
174
+ if (f.isUnique) opts.push('unique');
175
+ if (f.relation) opts.push(`relation: ${f.relation.model}`);
176
+ const optsStr = opts.length > 0 ? ` (${opts.join(', ')})` : '';
177
+ success(` ${f.name}: ${f.type}${optsStr}`);
178
+ });
179
+
180
+ // Show files created
181
+ console.log('\nšŸ“ Files created:');
182
+ files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
183
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
184
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
185
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
186
+
187
+ // Show next steps
188
+ console.log('\nšŸ“Œ Next steps:');
189
+ info(' 1. Add the Prisma model to your schema.prisma file');
190
+ info(' 2. Run: npx prisma db push (or prisma migrate dev)');
191
+ info(' 3. Run: npx prisma generate');
192
+ info(' 4. Register the module routes in your app');
193
+ info(' 5. Update the test files with actual test data');
194
+
195
+ console.log(
196
+ chalk.gray('\nšŸ’” Tip: Use --dry-run to preview changes before applying them\n')
197
+ );
198
+
199
+ if (options.dryRun) {
200
+ dryRun.printSummary();
201
+ }
202
+ } catch (error) {
203
+ spinner.fail('Failed to scaffold resource');
204
+ if (error instanceof Error) {
205
+ console.error(chalk.red(`\nāœ— ${error.message}\n`));
206
+ console.error(chalk.gray(error.stack));
207
+ }
208
+ process.exit(1);
209
+ }
210
+ }
211
+ );