servcraft 0.1.7 → 0.2.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.
@@ -0,0 +1,102 @@
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 fs from 'fs/promises';
7
+ import inquirer from 'inquirer';
8
+ import { getModulesDir, success, error, info } from '../utils/helpers.js';
9
+ import { ServCraftError, displayError, validateProject } from '../utils/error-handler.js';
10
+
11
+ export const removeCommand = new Command('remove')
12
+ .alias('rm')
13
+ .description('Remove an installed module from your project')
14
+ .argument('<module>', 'Module to remove')
15
+ .option('-y, --yes', 'Skip confirmation prompt')
16
+ .option('--keep-env', 'Keep environment variables')
17
+ .action(async (moduleName: string, options?: { yes?: boolean; keepEnv?: boolean }) => {
18
+ // Validate project
19
+ const projectError = validateProject();
20
+ if (projectError) {
21
+ displayError(projectError);
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold.cyan('\nšŸ—‘ļø ServCraft Module Removal\n'));
26
+
27
+ const moduleDir = path.join(getModulesDir(), moduleName);
28
+
29
+ try {
30
+ // Check if module exists
31
+ const exists = await fs
32
+ .access(moduleDir)
33
+ .then(() => true)
34
+ .catch(() => false);
35
+
36
+ if (!exists) {
37
+ displayError(
38
+ new ServCraftError(`Module "${moduleName}" is not installed`, [
39
+ `Run ${chalk.cyan('servcraft list --installed')} to see installed modules`,
40
+ `Check the spelling of the module name`,
41
+ ])
42
+ );
43
+ return;
44
+ }
45
+
46
+ // Get list of files
47
+ const files = await fs.readdir(moduleDir);
48
+ const fileCount = files.length;
49
+
50
+ // Confirm removal
51
+ if (!options?.yes) {
52
+ console.log(chalk.yellow(`⚠ This will remove the "${moduleName}" module:`));
53
+ console.log(chalk.gray(` Directory: ${moduleDir}`));
54
+ console.log(chalk.gray(` Files: ${fileCount} file(s)`));
55
+ console.log();
56
+
57
+ const { confirm } = await inquirer.prompt([
58
+ {
59
+ type: 'confirm',
60
+ name: 'confirm',
61
+ message: 'Are you sure you want to remove this module?',
62
+ default: false,
63
+ },
64
+ ]);
65
+
66
+ if (!confirm) {
67
+ console.log(chalk.yellow('\nāœ– Removal cancelled\n'));
68
+ return;
69
+ }
70
+ }
71
+
72
+ const spinner = ora('Removing module...').start();
73
+
74
+ // Remove module directory
75
+ await fs.rm(moduleDir, { recursive: true, force: true });
76
+
77
+ spinner.succeed(`Module "${moduleName}" removed successfully!`);
78
+
79
+ // Show what was removed
80
+ console.log('\n' + chalk.bold('āœ“ Removed:'));
81
+ success(` src/modules/${moduleName}/ (${fileCount} files)`);
82
+
83
+ // Instructions for cleanup
84
+ if (!options?.keepEnv) {
85
+ console.log('\n' + chalk.bold('šŸ“Œ Manual cleanup needed:'));
86
+ info(' 1. Remove environment variables related to this module from .env');
87
+ info(' 2. Remove module imports from your main app file');
88
+ info(' 3. Remove related database migrations if any');
89
+ info(' 4. Update your routes if they reference this module');
90
+ } else {
91
+ console.log('\n' + chalk.bold('šŸ“Œ Manual cleanup needed:'));
92
+ info(' 1. Environment variables were kept (--keep-env flag)');
93
+ info(' 2. Remove module imports from your main app file');
94
+ info(' 3. Update your routes if they reference this module');
95
+ }
96
+
97
+ console.log();
98
+ } catch (err) {
99
+ error(err instanceof Error ? err.message : String(err));
100
+ console.log();
101
+ }
102
+ });
package/src/cli/index.ts CHANGED
@@ -6,6 +6,9 @@ import { generateCommand } from './commands/generate.js';
6
6
  import { addModuleCommand } from './commands/add-module.js';
7
7
  import { dbCommand } from './commands/db.js';
8
8
  import { docsCommand } from './commands/docs.js';
9
+ import { listCommand } from './commands/list.js';
10
+ import { removeCommand } from './commands/remove.js';
11
+ import { doctorCommand } from './commands/doctor.js';
9
12
 
10
13
  const program = new Command();
11
14
 
@@ -29,4 +32,13 @@ program.addCommand(dbCommand);
29
32
  // Documentation commands
30
33
  program.addCommand(docsCommand);
31
34
 
35
+ // List modules
36
+ program.addCommand(listCommand);
37
+
38
+ // Remove module
39
+ program.addCommand(removeCommand);
40
+
41
+ // Diagnose project
42
+ program.addCommand(doctorCommand);
43
+
32
44
  program.parse();
@@ -0,0 +1,155 @@
1
+ /* eslint-disable no-console */
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+
5
+ export interface FileOperation {
6
+ type: 'create' | 'modify' | 'delete';
7
+ path: string;
8
+ content?: string;
9
+ size?: number;
10
+ }
11
+
12
+ export class DryRunManager {
13
+ private static instance: DryRunManager;
14
+ private enabled = false;
15
+ private operations: FileOperation[] = [];
16
+
17
+ private constructor() {}
18
+
19
+ static getInstance(): DryRunManager {
20
+ if (!DryRunManager.instance) {
21
+ DryRunManager.instance = new DryRunManager();
22
+ }
23
+ return DryRunManager.instance;
24
+ }
25
+
26
+ enable(): void {
27
+ this.enabled = true;
28
+ this.operations = [];
29
+ }
30
+
31
+ disable(): void {
32
+ this.enabled = false;
33
+ this.operations = [];
34
+ }
35
+
36
+ isEnabled(): boolean {
37
+ return this.enabled;
38
+ }
39
+
40
+ addOperation(operation: FileOperation): void {
41
+ if (this.enabled) {
42
+ this.operations.push(operation);
43
+ }
44
+ }
45
+
46
+ getOperations(): FileOperation[] {
47
+ return [...this.operations];
48
+ }
49
+
50
+ printSummary(): void {
51
+ if (!this.enabled || this.operations.length === 0) {
52
+ return;
53
+ }
54
+
55
+ console.log(chalk.bold.yellow('\nšŸ“‹ Dry Run - Preview of changes:\n'));
56
+ console.log(chalk.gray('No files will be written. Remove --dry-run to apply changes.\n'));
57
+
58
+ const createOps = this.operations.filter((op) => op.type === 'create');
59
+ const modifyOps = this.operations.filter((op) => op.type === 'modify');
60
+ const deleteOps = this.operations.filter((op) => op.type === 'delete');
61
+
62
+ if (createOps.length > 0) {
63
+ console.log(chalk.green.bold(`\nāœ“ Files to be created (${createOps.length}):`));
64
+ createOps.forEach((op) => {
65
+ const size = op.content ? `${op.content.length} bytes` : 'unknown size';
66
+ console.log(` ${chalk.green('+')} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
67
+ });
68
+ }
69
+
70
+ if (modifyOps.length > 0) {
71
+ console.log(chalk.yellow.bold(`\n~ Files to be modified (${modifyOps.length}):`));
72
+ modifyOps.forEach((op) => {
73
+ console.log(` ${chalk.yellow('~')} ${chalk.cyan(op.path)}`);
74
+ });
75
+ }
76
+
77
+ if (deleteOps.length > 0) {
78
+ console.log(chalk.red.bold(`\n- Files to be deleted (${deleteOps.length}):`));
79
+ deleteOps.forEach((op) => {
80
+ console.log(` ${chalk.red('-')} ${chalk.cyan(op.path)}`);
81
+ });
82
+ }
83
+
84
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
85
+ console.log(
86
+ chalk.bold(` Total operations: ${this.operations.length}`) +
87
+ chalk.gray(
88
+ ` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
89
+ )
90
+ );
91
+ console.log(chalk.gray('─'.repeat(60)));
92
+ console.log(chalk.yellow('\n⚠ This was a dry run. No files were created or modified.'));
93
+ console.log(chalk.gray(' Remove --dry-run to apply these changes.\n'));
94
+ }
95
+
96
+ // Helper to format file path relative to cwd
97
+ relativePath(filePath: string): string {
98
+ return path.relative(process.cwd(), filePath);
99
+ }
100
+ }
101
+
102
+ // Wrapper functions for file operations
103
+ export async function dryRunWriteFile(
104
+ filePath: string,
105
+ content: string,
106
+ actualWriteFn: (path: string, content: string) => Promise<void>
107
+ ): Promise<void> {
108
+ const dryRun = DryRunManager.getInstance();
109
+
110
+ if (dryRun.isEnabled()) {
111
+ dryRun.addOperation({
112
+ type: 'create',
113
+ path: dryRun.relativePath(filePath),
114
+ content,
115
+ size: content.length,
116
+ });
117
+ return;
118
+ }
119
+
120
+ await actualWriteFn(filePath, content);
121
+ }
122
+
123
+ export async function dryRunModifyFile(
124
+ filePath: string,
125
+ actualModifyFn: (path: string) => Promise<void>
126
+ ): Promise<void> {
127
+ const dryRun = DryRunManager.getInstance();
128
+
129
+ if (dryRun.isEnabled()) {
130
+ dryRun.addOperation({
131
+ type: 'modify',
132
+ path: dryRun.relativePath(filePath),
133
+ });
134
+ return;
135
+ }
136
+
137
+ await actualModifyFn(filePath);
138
+ }
139
+
140
+ export async function dryRunDeleteFile(
141
+ filePath: string,
142
+ actualDeleteFn: (path: string) => Promise<void>
143
+ ): Promise<void> {
144
+ const dryRun = DryRunManager.getInstance();
145
+
146
+ if (dryRun.isEnabled()) {
147
+ dryRun.addOperation({
148
+ type: 'delete',
149
+ path: dryRun.relativePath(filePath),
150
+ });
151
+ return;
152
+ }
153
+
154
+ await actualDeleteFn(filePath);
155
+ }
@@ -0,0 +1,184 @@
1
+ /* eslint-disable no-console */
2
+ import chalk from 'chalk';
3
+
4
+ export interface ErrorSuggestion {
5
+ message: string;
6
+ suggestions: string[];
7
+ docsLink?: string;
8
+ }
9
+
10
+ export class ServCraftError extends Error {
11
+ suggestions: string[];
12
+ docsLink?: string;
13
+
14
+ constructor(message: string, suggestions: string[] = [], docsLink?: string) {
15
+ super(message);
16
+ this.name = 'ServCraftError';
17
+ this.suggestions = suggestions;
18
+ this.docsLink = docsLink;
19
+ }
20
+ }
21
+
22
+ // Common error types with suggestions
23
+ export const ErrorTypes = {
24
+ MODULE_NOT_FOUND: (moduleName: string): ServCraftError =>
25
+ new ServCraftError(
26
+ `Module "${moduleName}" not found`,
27
+ [
28
+ `Run ${chalk.cyan('servcraft list')} to see available modules`,
29
+ `Check the spelling of the module name`,
30
+ `Visit ${chalk.blue('https://github.com/Le-Sourcier/servcraft#modules')} for module list`,
31
+ ],
32
+ 'https://github.com/Le-Sourcier/servcraft#add-pre-built-modules'
33
+ ),
34
+
35
+ MODULE_ALREADY_EXISTS: (moduleName: string): ServCraftError =>
36
+ new ServCraftError(`Module "${moduleName}" already exists`, [
37
+ `Use ${chalk.cyan('servcraft add ' + moduleName + ' --force')} to overwrite`,
38
+ `Use ${chalk.cyan('servcraft add ' + moduleName + ' --update')} to update`,
39
+ `Use ${chalk.cyan('servcraft add ' + moduleName + ' --skip-existing')} to skip`,
40
+ ]),
41
+
42
+ NOT_IN_PROJECT: (): ServCraftError =>
43
+ new ServCraftError(
44
+ 'Not in a ServCraft project directory',
45
+ [
46
+ `Run ${chalk.cyan('servcraft init')} to create a new project`,
47
+ `Navigate to your ServCraft project directory`,
48
+ `Check if ${chalk.yellow('package.json')} exists`,
49
+ ],
50
+ 'https://github.com/Le-Sourcier/servcraft#initialize-project'
51
+ ),
52
+
53
+ FILE_ALREADY_EXISTS: (fileName: string): ServCraftError =>
54
+ new ServCraftError(`File "${fileName}" already exists`, [
55
+ `Use ${chalk.cyan('--force')} flag to overwrite`,
56
+ `Choose a different name`,
57
+ `Delete the existing file first`,
58
+ ]),
59
+
60
+ INVALID_DATABASE: (database: string): ServCraftError =>
61
+ new ServCraftError(`Invalid database type: "${database}"`, [
62
+ `Valid options: ${chalk.cyan('postgresql, mysql, sqlite, mongodb, none')}`,
63
+ `Use ${chalk.cyan('servcraft init --db postgresql')} for PostgreSQL`,
64
+ ]),
65
+
66
+ INVALID_VALIDATOR: (validator: string): ServCraftError =>
67
+ new ServCraftError(`Invalid validator type: "${validator}"`, [
68
+ `Valid options: ${chalk.cyan('zod, joi, yup')}`,
69
+ `Default is ${chalk.cyan('zod')}`,
70
+ ]),
71
+
72
+ MISSING_DEPENDENCY: (dependency: string, command: string): ServCraftError =>
73
+ new ServCraftError(`Missing dependency: "${dependency}"`, [
74
+ `Run ${chalk.cyan(command)} to install`,
75
+ `Check your ${chalk.yellow('package.json')}`,
76
+ ]),
77
+
78
+ INVALID_FIELD_FORMAT: (field: string): ServCraftError =>
79
+ new ServCraftError(`Invalid field format: "${field}"`, [
80
+ `Expected format: ${chalk.cyan('name:type')}`,
81
+ `Example: ${chalk.cyan('name:string age:number isActive:boolean')}`,
82
+ `Supported types: string, number, boolean, date`,
83
+ ]),
84
+
85
+ GIT_NOT_INITIALIZED: (): ServCraftError =>
86
+ new ServCraftError('Git repository not initialized', [
87
+ `Run ${chalk.cyan('git init')} to initialize git`,
88
+ `This is required for some ServCraft features`,
89
+ ]),
90
+ };
91
+
92
+ // Display error with suggestions
93
+ export function displayError(error: Error | ServCraftError): void {
94
+ console.error('\n' + chalk.red.bold('āœ— Error: ') + chalk.red(error.message));
95
+
96
+ if (error instanceof ServCraftError) {
97
+ if (error.suggestions.length > 0) {
98
+ console.log('\n' + chalk.yellow.bold('šŸ’” Suggestions:'));
99
+ error.suggestions.forEach((suggestion) => {
100
+ console.log(chalk.yellow(' • ') + suggestion);
101
+ });
102
+ }
103
+
104
+ if (error.docsLink) {
105
+ console.log(
106
+ '\n' + chalk.blue.bold('šŸ“š Documentation: ') + chalk.blue.underline(error.docsLink)
107
+ );
108
+ }
109
+ }
110
+
111
+ console.log(); // Empty line for spacing
112
+ }
113
+
114
+ // Handle common Node.js errors
115
+ export function handleSystemError(err: NodeJS.ErrnoException): ServCraftError {
116
+ switch (err.code) {
117
+ case 'ENOENT':
118
+ return new ServCraftError(`File or directory not found: ${err.path}`, [
119
+ `Check if the path exists`,
120
+ `Create the directory first`,
121
+ ]);
122
+
123
+ case 'EACCES':
124
+ case 'EPERM':
125
+ return new ServCraftError(`Permission denied: ${err.path}`, [
126
+ `Check file permissions`,
127
+ `Try running with elevated privileges (not recommended)`,
128
+ `Change ownership of the directory`,
129
+ ]);
130
+
131
+ case 'EEXIST':
132
+ return new ServCraftError(`File or directory already exists: ${err.path}`, [
133
+ `Use a different name`,
134
+ `Remove the existing file first`,
135
+ `Use ${chalk.cyan('--force')} to overwrite`,
136
+ ]);
137
+
138
+ case 'ENOTDIR':
139
+ return new ServCraftError(`Not a directory: ${err.path}`, [
140
+ `Check the path`,
141
+ `A file exists where a directory is expected`,
142
+ ]);
143
+
144
+ case 'EISDIR':
145
+ return new ServCraftError(`Is a directory: ${err.path}`, [
146
+ `Cannot perform this operation on a directory`,
147
+ `Did you mean to target a file?`,
148
+ ]);
149
+
150
+ default:
151
+ return new ServCraftError(err.message, [
152
+ `Check system error code: ${err.code}`,
153
+ `Review the error details above`,
154
+ ]);
155
+ }
156
+ }
157
+
158
+ // Validate project structure
159
+ export function validateProject(): ServCraftError | null {
160
+ try {
161
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
162
+ const fs = require('fs');
163
+
164
+ if (!fs.existsSync('package.json')) {
165
+ return ErrorTypes.NOT_IN_PROJECT();
166
+ }
167
+
168
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
169
+
170
+ if (!packageJson.dependencies?.fastify) {
171
+ return new ServCraftError('This does not appear to be a ServCraft project', [
172
+ `ServCraft projects require Fastify`,
173
+ `Run ${chalk.cyan('servcraft init')} to create a new project`,
174
+ ]);
175
+ }
176
+
177
+ return null;
178
+ } catch {
179
+ return new ServCraftError('Failed to validate project', [
180
+ `Ensure you are in the project root directory`,
181
+ `Check if ${chalk.yellow('package.json')} is valid`,
182
+ ]);
183
+ }
184
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
+ import { DryRunManager } from './dry-run.js';
4
5
 
5
6
  export function toPascalCase(str: string): string {
6
7
  return str
@@ -52,6 +53,18 @@ export async function ensureDir(dirPath: string): Promise<void> {
52
53
  }
53
54
 
54
55
  export async function writeFile(filePath: string, content: string): Promise<void> {
56
+ const dryRun = DryRunManager.getInstance();
57
+
58
+ if (dryRun.isEnabled()) {
59
+ dryRun.addOperation({
60
+ type: 'create',
61
+ path: dryRun.relativePath(filePath),
62
+ content,
63
+ size: content.length,
64
+ });
65
+ return;
66
+ }
67
+
55
68
  await ensureDir(path.dirname(filePath));
56
69
  await fs.writeFile(filePath, content, 'utf-8');
57
70
  }