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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servcraft",
3
- "version": "0.1.7",
3
+ "version": "0.3.1",
4
4
  "description": "A modular, production-ready Node.js backend framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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?: { list?: boolean; force?: boolean; update?: boolean; skipExisting?: boolean }
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
- error(`Unknown module: ${moduleName}`);
187
- info('Run "servcraft add --list" to see available modules');
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
- console.log('\nšŸ“Œ Next steps:');
323
- info(' 1. Configure environment variables in .env (if needed)');
324
- info(' 2. Register the module in your main app file');
325
- info(' 3. Run database migrations if needed');
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
- error(`Controller "${kebabName}" already exists`);
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));
@@ -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
- const installSpinner = ora('Installing dependencies...').start();
279
-
280
- try {
281
- execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
282
- installSpinner.succeed('Dependencies installed!');
283
- } catch {
284
- installSpinner.warn('Failed to install dependencies automatically');
285
- warn(' Run "npm install" manually in the project directory');
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
- console.log('\n' + chalk.green('✨ Project created successfully!'));
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));