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.
@@ -0,0 +1,147 @@
1
+ /* eslint-disable no-console */
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { getProjectRoot } from '../utils/helpers.js';
7
+ import { validateProject, displayError } from '../utils/error-handler.js';
8
+
9
+ // Template types available for customization
10
+ const TEMPLATE_TYPES = [
11
+ 'controller',
12
+ 'service',
13
+ 'repository',
14
+ 'types',
15
+ 'schemas',
16
+ 'routes',
17
+ 'module-index',
18
+ 'controller-test',
19
+ 'service-test',
20
+ 'integration-test',
21
+ ];
22
+
23
+ async function initTemplates(): Promise<void> {
24
+ const projectError = validateProject();
25
+ if (projectError) {
26
+ displayError(projectError);
27
+ return;
28
+ }
29
+
30
+ const projectRoot = getProjectRoot();
31
+ const templatesDir = path.join(projectRoot, '.servcraft', 'templates');
32
+
33
+ try {
34
+ // Create .servcraft/templates directory
35
+ await fs.mkdir(templatesDir, { recursive: true });
36
+
37
+ console.log(chalk.cyan('\nšŸ“ Creating custom template directory...\n'));
38
+
39
+ // Create example template files
40
+ const exampleController = `// Custom controller template
41
+ // Available variables: name, pascalName, camelName, pluralName
42
+ export function controllerTemplate(name: string, pascalName: string, camelName: string): string {
43
+ return \`import type { FastifyRequest, FastifyReply } from 'fastify';
44
+ import type { \${pascalName}Service } from './\${name}.service.js';
45
+
46
+ export class \${pascalName}Controller {
47
+ constructor(private \${camelName}Service: \${pascalName}Service) {}
48
+
49
+ // Add your custom controller methods here
50
+ async getAll(request: FastifyRequest, reply: FastifyReply) {
51
+ const data = await this.\${camelName}Service.getAll();
52
+ return reply.send({ data });
53
+ }
54
+ }
55
+ \`;
56
+ }
57
+ `;
58
+
59
+ await fs.writeFile(
60
+ path.join(templatesDir, 'controller.example.ts'),
61
+ exampleController,
62
+ 'utf-8'
63
+ );
64
+
65
+ console.log(chalk.green('āœ” Created template directory: .servcraft/templates/'));
66
+ console.log(chalk.green('āœ” Created example template: controller.example.ts\n'));
67
+
68
+ console.log(chalk.bold('šŸ“‹ Available template types:\n'));
69
+ TEMPLATE_TYPES.forEach((type) => {
70
+ console.log(chalk.gray(` • ${type}.ts`));
71
+ });
72
+
73
+ console.log(chalk.yellow('\nšŸ’” To customize a template:'));
74
+ console.log(chalk.gray(' 1. Copy the example template'));
75
+ console.log(chalk.gray(' 2. Rename it (remove .example)'));
76
+ console.log(chalk.gray(' 3. Modify the template code'));
77
+ console.log(chalk.gray(' 4. Use --template flag when generating\n'));
78
+ } catch (error) {
79
+ if (error instanceof Error) {
80
+ console.error(chalk.red(`\nāœ— Failed to initialize templates: ${error.message}\n`));
81
+ }
82
+ }
83
+ }
84
+
85
+ async function listTemplates(): Promise<void> {
86
+ const projectError = validateProject();
87
+ if (projectError) {
88
+ displayError(projectError);
89
+ return;
90
+ }
91
+
92
+ const projectRoot = getProjectRoot();
93
+ const projectTemplatesDir = path.join(projectRoot, '.servcraft', 'templates');
94
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
95
+ const userTemplatesDir = path.join(homeDir, '.servcraft', 'templates');
96
+
97
+ console.log(chalk.bold.cyan('\nšŸ“‹ Available Templates\n'));
98
+
99
+ // Check project templates
100
+ console.log(chalk.bold('Project templates (.servcraft/templates/):'));
101
+ try {
102
+ const files = await fs.readdir(projectTemplatesDir);
103
+ const templates = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.example.ts'));
104
+
105
+ if (templates.length > 0) {
106
+ templates.forEach((t) => {
107
+ console.log(chalk.green(` āœ“ ${t}`));
108
+ });
109
+ } else {
110
+ console.log(chalk.gray(' (none)'));
111
+ }
112
+ } catch {
113
+ console.log(chalk.gray(' (directory not found)'));
114
+ }
115
+
116
+ // Check user templates
117
+ console.log(chalk.bold('\nUser templates (~/.servcraft/templates/):'));
118
+ try {
119
+ const files = await fs.readdir(userTemplatesDir);
120
+ const templates = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.example.ts'));
121
+
122
+ if (templates.length > 0) {
123
+ templates.forEach((t) => {
124
+ console.log(chalk.green(` āœ“ ${t}`));
125
+ });
126
+ } else {
127
+ console.log(chalk.gray(' (none)'));
128
+ }
129
+ } catch {
130
+ console.log(chalk.gray(' (directory not found)'));
131
+ }
132
+
133
+ // Show built-in templates
134
+ console.log(chalk.bold('\nBuilt-in templates:'));
135
+ TEMPLATE_TYPES.forEach((t) => {
136
+ console.log(chalk.cyan(` • ${t}.ts`));
137
+ });
138
+
139
+ console.log(chalk.gray('\nšŸ’” Run "servcraft templates init" to create custom templates\n'));
140
+ }
141
+
142
+ export const templatesCommand = new Command('templates')
143
+ .description('Manage code generation templates')
144
+ .addCommand(
145
+ new Command('init').description('Initialize custom templates directory').action(initTemplates)
146
+ )
147
+ .addCommand(new Command('list').description('List available templates').action(listTemplates));
@@ -0,0 +1,221 @@
1
+ /* eslint-disable no-console */
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import inquirer from 'inquirer';
8
+ import { getProjectRoot, getModulesDir } from '../utils/helpers.js';
9
+ import { validateProject, displayError } from '../utils/error-handler.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ // Get list of available modules (same as list.ts)
15
+ const AVAILABLE_MODULES = [
16
+ 'auth',
17
+ 'users',
18
+ 'email',
19
+ 'mfa',
20
+ 'oauth',
21
+ 'rate-limit',
22
+ 'cache',
23
+ 'upload',
24
+ 'search',
25
+ 'notification',
26
+ 'webhook',
27
+ 'websocket',
28
+ 'queue',
29
+ 'payment',
30
+ 'i18n',
31
+ 'feature-flag',
32
+ 'analytics',
33
+ 'media-processing',
34
+ 'api-versioning',
35
+ 'audit',
36
+ 'swagger',
37
+ 'validation',
38
+ ];
39
+
40
+ async function getInstalledModules(): Promise<string[]> {
41
+ try {
42
+ const modulesDir = getModulesDir();
43
+
44
+ const entries = await fs.readdir(modulesDir, { withFileTypes: true });
45
+ const installedModules = entries
46
+ .filter((entry) => entry.isDirectory())
47
+ .map((entry) => entry.name)
48
+ .filter((name) => AVAILABLE_MODULES.includes(name));
49
+
50
+ return installedModules;
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ async function copyModuleFiles(moduleName: string, _projectRoot: string): Promise<void> {
57
+ const cliRoot = path.resolve(__dirname, '../../../');
58
+ const sourceModulePath = path.join(cliRoot, 'src', 'modules', moduleName);
59
+ const targetModulesDir = getModulesDir();
60
+ const targetModulePath = path.join(targetModulesDir, moduleName);
61
+
62
+ // Check if source module exists
63
+ try {
64
+ await fs.access(sourceModulePath);
65
+ } catch {
66
+ throw new Error(`Module source not found: ${moduleName}`);
67
+ }
68
+
69
+ // Copy module files
70
+ await fs.cp(sourceModulePath, targetModulePath, { recursive: true });
71
+ }
72
+
73
+ async function updateModule(moduleName: string, options: { check?: boolean }): Promise<void> {
74
+ const projectError = validateProject();
75
+ if (projectError) {
76
+ displayError(projectError);
77
+ return;
78
+ }
79
+
80
+ const projectRoot = getProjectRoot();
81
+ const installedModules = await getInstalledModules();
82
+
83
+ if (!installedModules.includes(moduleName)) {
84
+ console.log(chalk.yellow(`\n⚠ Module "${moduleName}" is not installed\n`));
85
+ console.log(
86
+ chalk.gray(`Run ${chalk.cyan(`servcraft add ${moduleName}`)} to install it first.\n`)
87
+ );
88
+ return;
89
+ }
90
+
91
+ if (options.check) {
92
+ console.log(chalk.cyan(`\nšŸ“¦ Checking updates for "${moduleName}"...\n`));
93
+ console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
94
+ console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
95
+ return;
96
+ }
97
+
98
+ // Confirm update
99
+ const { confirmed } = await inquirer.prompt([
100
+ {
101
+ type: 'confirm',
102
+ name: 'confirmed',
103
+ message: `Update "${moduleName}" module? This will overwrite existing files.`,
104
+ default: false,
105
+ },
106
+ ]);
107
+
108
+ if (!confirmed) {
109
+ console.log(chalk.yellow('\n⚠ Update cancelled\n'));
110
+ return;
111
+ }
112
+
113
+ console.log(chalk.cyan(`\nšŸ”„ Updating "${moduleName}" module...\n`));
114
+
115
+ try {
116
+ await copyModuleFiles(moduleName, projectRoot);
117
+ console.log(chalk.green(`āœ” Module "${moduleName}" updated successfully!\n`));
118
+ console.log(
119
+ chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
120
+ );
121
+ } catch (error) {
122
+ if (error instanceof Error) {
123
+ console.error(chalk.red(`\nāœ— Failed to update module: ${error.message}\n`));
124
+ }
125
+ }
126
+ }
127
+
128
+ async function updateAllModules(options: { check?: boolean }): Promise<void> {
129
+ const projectError = validateProject();
130
+ if (projectError) {
131
+ displayError(projectError);
132
+ return;
133
+ }
134
+
135
+ const installedModules = await getInstalledModules();
136
+
137
+ if (installedModules.length === 0) {
138
+ console.log(chalk.yellow('\n⚠ No modules installed\n'));
139
+ console.log(chalk.gray(`Run ${chalk.cyan('servcraft list')} to see available modules.\n`));
140
+ return;
141
+ }
142
+
143
+ if (options.check) {
144
+ console.log(chalk.cyan('\nšŸ“¦ Checking updates for all modules...\n'));
145
+ console.log(chalk.bold('Installed modules:'));
146
+ installedModules.forEach((mod) => {
147
+ console.log(` • ${chalk.cyan(mod)}`);
148
+ });
149
+ console.log();
150
+ console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
151
+ console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
152
+ return;
153
+ }
154
+
155
+ console.log(chalk.cyan(`\nšŸ“¦ Found ${installedModules.length} installed module(s):\n`));
156
+ installedModules.forEach((mod) => {
157
+ console.log(` • ${chalk.cyan(mod)}`);
158
+ });
159
+ console.log();
160
+
161
+ // Confirm update all
162
+ const { confirmed } = await inquirer.prompt([
163
+ {
164
+ type: 'confirm',
165
+ name: 'confirmed',
166
+ message: 'Update all modules? This will overwrite existing files.',
167
+ default: false,
168
+ },
169
+ ]);
170
+
171
+ if (!confirmed) {
172
+ console.log(chalk.yellow('\n⚠ Update cancelled\n'));
173
+ return;
174
+ }
175
+
176
+ console.log(chalk.cyan('\nšŸ”„ Updating all modules...\n'));
177
+
178
+ const projectRoot = getProjectRoot();
179
+ let successCount = 0;
180
+ let failCount = 0;
181
+
182
+ for (const moduleName of installedModules) {
183
+ try {
184
+ await copyModuleFiles(moduleName, projectRoot);
185
+ console.log(chalk.green(`āœ” Updated: ${moduleName}`));
186
+ successCount++;
187
+ } catch {
188
+ console.error(chalk.red(`āœ— Failed: ${moduleName}`));
189
+ failCount++;
190
+ }
191
+ }
192
+
193
+ console.log();
194
+ console.log(
195
+ chalk.bold(
196
+ `\nāœ” Update complete: ${chalk.green(successCount)} succeeded, ${chalk.red(failCount)} failed\n`
197
+ )
198
+ );
199
+
200
+ if (successCount > 0) {
201
+ console.log(
202
+ chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
203
+ );
204
+ }
205
+ }
206
+
207
+ export const updateCommand = new Command('update')
208
+ .description('Update installed modules to latest version')
209
+ .argument('[module]', 'Specific module to update')
210
+ .option('--check', 'Check for updates without applying')
211
+ .option('-y, --yes', 'Skip confirmation prompt')
212
+ .action(async (moduleName?: string, options?: { check?: boolean; yes?: boolean }) => {
213
+ // If --yes flag is provided, we'll handle it by auto-confirming in the inquirer prompts
214
+ // For now, we'll just pass through to the update functions
215
+
216
+ if (moduleName) {
217
+ await updateModule(moduleName, { check: options?.check });
218
+ } else {
219
+ await updateAllModules({ check: options?.check });
220
+ }
221
+ });
package/src/cli/index.ts CHANGED
@@ -9,6 +9,10 @@ import { docsCommand } from './commands/docs.js';
9
9
  import { listCommand } from './commands/list.js';
10
10
  import { removeCommand } from './commands/remove.js';
11
11
  import { doctorCommand } from './commands/doctor.js';
12
+ import { updateCommand } from './commands/update.js';
13
+ import { completionCommand } from './commands/completion.js';
14
+ import { scaffoldCommand } from './commands/scaffold.js';
15
+ import { templatesCommand } from './commands/templates.js';
12
16
 
13
17
  const program = new Command();
14
18
 
@@ -41,4 +45,16 @@ program.addCommand(removeCommand);
41
45
  // Diagnose project
42
46
  program.addCommand(doctorCommand);
43
47
 
48
+ // Update modules
49
+ program.addCommand(updateCommand);
50
+
51
+ // Shell completion
52
+ program.addCommand(completionCommand);
53
+
54
+ // Scaffold resource
55
+ program.addCommand(scaffoldCommand);
56
+
57
+ // Template management
58
+ program.addCommand(templatesCommand);
59
+
44
60
  program.parse();
@@ -0,0 +1,110 @@
1
+ export function controllerTestTemplate(
2
+ name: string,
3
+ pascalName: string,
4
+ camelName: string
5
+ ): string {
6
+ return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
7
+ import { build } from '../../app.js';
8
+ import { FastifyInstance } from 'fastify';
9
+
10
+ describe('${pascalName}Controller', () => {
11
+ let app: FastifyInstance;
12
+
13
+ beforeAll(async () => {
14
+ app = await build();
15
+ await app.ready();
16
+ });
17
+
18
+ afterAll(async () => {
19
+ await app.close();
20
+ });
21
+
22
+ describe('GET /${name}', () => {
23
+ it('should return list of ${name}', async () => {
24
+ const response = await app.inject({
25
+ method: 'GET',
26
+ url: '/${name}',
27
+ });
28
+
29
+ expect(response.statusCode).toBe(200);
30
+ expect(response.json()).toHaveProperty('data');
31
+ });
32
+ });
33
+
34
+ describe('GET /${name}/:id', () => {
35
+ it('should return a single ${camelName}', async () => {
36
+ // TODO: Create test ${camelName} first
37
+ const response = await app.inject({
38
+ method: 'GET',
39
+ url: '/${name}/1',
40
+ });
41
+
42
+ expect(response.statusCode).toBe(200);
43
+ expect(response.json()).toHaveProperty('data');
44
+ });
45
+
46
+ it('should return 404 for non-existent ${camelName}', async () => {
47
+ const response = await app.inject({
48
+ method: 'GET',
49
+ url: '/${name}/999999',
50
+ });
51
+
52
+ expect(response.statusCode).toBe(404);
53
+ });
54
+ });
55
+
56
+ describe('POST /${name}', () => {
57
+ it('should create a new ${camelName}', async () => {
58
+ const response = await app.inject({
59
+ method: 'POST',
60
+ url: '/${name}',
61
+ payload: {
62
+ // TODO: Add required fields
63
+ },
64
+ });
65
+
66
+ expect(response.statusCode).toBe(201);
67
+ expect(response.json()).toHaveProperty('data');
68
+ });
69
+
70
+ it('should return 400 for invalid data', async () => {
71
+ const response = await app.inject({
72
+ method: 'POST',
73
+ url: '/${name}',
74
+ payload: {},
75
+ });
76
+
77
+ expect(response.statusCode).toBe(400);
78
+ });
79
+ });
80
+
81
+ describe('PUT /${name}/:id', () => {
82
+ it('should update a ${camelName}', async () => {
83
+ // TODO: Create test ${camelName} first
84
+ const response = await app.inject({
85
+ method: 'PUT',
86
+ url: '/${name}/1',
87
+ payload: {
88
+ // TODO: Add fields to update
89
+ },
90
+ });
91
+
92
+ expect(response.statusCode).toBe(200);
93
+ expect(response.json()).toHaveProperty('data');
94
+ });
95
+ });
96
+
97
+ describe('DELETE /${name}/:id', () => {
98
+ it('should delete a ${camelName}', async () => {
99
+ // TODO: Create test ${camelName} first
100
+ const response = await app.inject({
101
+ method: 'DELETE',
102
+ url: '/${name}/1',
103
+ });
104
+
105
+ expect(response.statusCode).toBe(204);
106
+ });
107
+ });
108
+ });
109
+ `;
110
+ }
@@ -0,0 +1,139 @@
1
+ export function integrationTestTemplate(
2
+ name: string,
3
+ pascalName: string,
4
+ camelName: string
5
+ ): string {
6
+ return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
7
+ import { build } from '../../app.js';
8
+ import { FastifyInstance } from 'fastify';
9
+ import { prisma } from '../../lib/prisma.js';
10
+
11
+ describe('${pascalName} Integration Tests', () => {
12
+ let app: FastifyInstance;
13
+
14
+ beforeAll(async () => {
15
+ app = await build();
16
+ await app.ready();
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await app.close();
21
+ await prisma.$disconnect();
22
+ });
23
+
24
+ beforeEach(async () => {
25
+ // Clean up test data
26
+ // await prisma.${camelName}.deleteMany();
27
+ });
28
+
29
+ describe('Full CRUD workflow', () => {
30
+ it('should create, read, update, and delete a ${camelName}', async () => {
31
+ // Create
32
+ const createResponse = await app.inject({
33
+ method: 'POST',
34
+ url: '/${name}',
35
+ payload: {
36
+ // TODO: Add required fields
37
+ },
38
+ });
39
+
40
+ expect(createResponse.statusCode).toBe(201);
41
+ const created = createResponse.json().data;
42
+ expect(created.id).toBeDefined();
43
+
44
+ // Read
45
+ const readResponse = await app.inject({
46
+ method: 'GET',
47
+ url: \`/${name}/\${created.id}\`,
48
+ });
49
+
50
+ expect(readResponse.statusCode).toBe(200);
51
+ const read = readResponse.json().data;
52
+ expect(read.id).toBe(created.id);
53
+
54
+ // Update
55
+ const updateResponse = await app.inject({
56
+ method: 'PUT',
57
+ url: \`/${name}/\${created.id}\`,
58
+ payload: {
59
+ // TODO: Add fields to update
60
+ },
61
+ });
62
+
63
+ expect(updateResponse.statusCode).toBe(200);
64
+ const updated = updateResponse.json().data;
65
+ expect(updated.id).toBe(created.id);
66
+
67
+ // Delete
68
+ const deleteResponse = await app.inject({
69
+ method: 'DELETE',
70
+ url: \`/${name}/\${created.id}\`,
71
+ });
72
+
73
+ expect(deleteResponse.statusCode).toBe(204);
74
+
75
+ // Verify deletion
76
+ const verifyResponse = await app.inject({
77
+ method: 'GET',
78
+ url: \`/${name}/\${created.id}\`,
79
+ });
80
+
81
+ expect(verifyResponse.statusCode).toBe(404);
82
+ });
83
+ });
84
+
85
+ describe('List and pagination', () => {
86
+ it('should list ${name} with pagination', async () => {
87
+ // Create multiple ${name}
88
+ const count = 5;
89
+ for (let i = 0; i < count; i++) {
90
+ await app.inject({
91
+ method: 'POST',
92
+ url: '/${name}',
93
+ payload: {
94
+ // TODO: Add required fields
95
+ },
96
+ });
97
+ }
98
+
99
+ // Test pagination
100
+ const response = await app.inject({
101
+ method: 'GET',
102
+ url: '/${name}?page=1&limit=3',
103
+ });
104
+
105
+ expect(response.statusCode).toBe(200);
106
+ const result = response.json();
107
+ expect(result.data).toBeDefined();
108
+ expect(result.data.length).toBeLessThanOrEqual(3);
109
+ expect(result.total).toBeGreaterThanOrEqual(count);
110
+ });
111
+ });
112
+
113
+ describe('Validation', () => {
114
+ it('should validate required fields on create', async () => {
115
+ const response = await app.inject({
116
+ method: 'POST',
117
+ url: '/${name}',
118
+ payload: {},
119
+ });
120
+
121
+ expect(response.statusCode).toBe(400);
122
+ expect(response.json()).toHaveProperty('error');
123
+ });
124
+
125
+ it('should validate data types', async () => {
126
+ const response = await app.inject({
127
+ method: 'POST',
128
+ url: '/${name}',
129
+ payload: {
130
+ // TODO: Add invalid field types
131
+ },
132
+ });
133
+
134
+ expect(response.statusCode).toBe(400);
135
+ });
136
+ });
137
+ });
138
+ `;
139
+ }