servcraft 0.3.1 ā 0.4.3
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/README.md +26 -0
- package/ROADMAP.md +54 -14
- package/dist/cli/index.cjs +489 -166
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +470 -147
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +31 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/generate.ts +25 -10
- package/src/cli/commands/scaffold.ts +211 -0
- package/src/cli/commands/templates.ts +147 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/utils/template-loader.ts +80 -0
- package/src/modules/user/user.repository.ts +18 -1
package/package.json
CHANGED
|
@@ -48,6 +48,7 @@ import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
|
48
48
|
import { controllerTestTemplate } from '../templates/controller-test.js';
|
|
49
49
|
import { serviceTestTemplate } from '../templates/service-test.js';
|
|
50
50
|
import { integrationTestTemplate } from '../templates/integration-test.js';
|
|
51
|
+
import { getTemplate } from '../utils/template-loader.js';
|
|
51
52
|
|
|
52
53
|
export const generateCommand = new Command('generate')
|
|
53
54
|
.alias('g')
|
|
@@ -100,41 +101,50 @@ generateCommand
|
|
|
100
101
|
// Use dynamic templates if fields are provided
|
|
101
102
|
const hasFields = fields.length > 0;
|
|
102
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
|
+
|
|
103
113
|
const files = [
|
|
104
114
|
{
|
|
105
115
|
name: `${kebabName}.types.ts`,
|
|
106
116
|
content: hasFields
|
|
107
117
|
? dynamicTypesTemplate(kebabName, pascalName, fields)
|
|
108
|
-
:
|
|
118
|
+
: typesTpl(kebabName, pascalName),
|
|
109
119
|
},
|
|
110
120
|
{
|
|
111
121
|
name: `${kebabName}.schemas.ts`,
|
|
112
122
|
content: hasFields
|
|
113
123
|
? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType)
|
|
114
|
-
:
|
|
124
|
+
: schemasTpl(kebabName, pascalName, camelName),
|
|
115
125
|
},
|
|
116
126
|
{
|
|
117
127
|
name: `${kebabName}.service.ts`,
|
|
118
|
-
content:
|
|
128
|
+
content: serviceTpl(kebabName, pascalName, camelName),
|
|
119
129
|
},
|
|
120
130
|
{
|
|
121
131
|
name: `${kebabName}.controller.ts`,
|
|
122
|
-
content:
|
|
132
|
+
content: controllerTpl(kebabName, pascalName, camelName),
|
|
123
133
|
},
|
|
124
|
-
{ name: 'index.ts', content:
|
|
134
|
+
{ name: 'index.ts', content: moduleIndexTpl(kebabName, pascalName, camelName) },
|
|
125
135
|
];
|
|
126
136
|
|
|
127
137
|
if (options.repository !== false) {
|
|
128
138
|
files.push({
|
|
129
139
|
name: `${kebabName}.repository.ts`,
|
|
130
|
-
content:
|
|
140
|
+
content: repositoryTpl(kebabName, pascalName, camelName, pluralName),
|
|
131
141
|
});
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
if (options.routes !== false) {
|
|
135
145
|
files.push({
|
|
136
146
|
name: `${kebabName}.routes.ts`,
|
|
137
|
-
content:
|
|
147
|
+
content: routesTpl(kebabName, pascalName, camelName, pluralName),
|
|
138
148
|
});
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -147,19 +157,24 @@ generateCommand
|
|
|
147
157
|
if (options.withTests) {
|
|
148
158
|
const testDir = path.join(moduleDir, '__tests__');
|
|
149
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
|
+
|
|
150
165
|
await writeFile(
|
|
151
166
|
path.join(testDir, `${kebabName}.controller.test.ts`),
|
|
152
|
-
|
|
167
|
+
controllerTestTpl(kebabName, pascalName, camelName)
|
|
153
168
|
);
|
|
154
169
|
|
|
155
170
|
await writeFile(
|
|
156
171
|
path.join(testDir, `${kebabName}.service.test.ts`),
|
|
157
|
-
|
|
172
|
+
serviceTestTpl(kebabName, pascalName, camelName)
|
|
158
173
|
);
|
|
159
174
|
|
|
160
175
|
await writeFile(
|
|
161
176
|
path.join(testDir, `${kebabName}.integration.test.ts`),
|
|
162
|
-
|
|
177
|
+
integrationTestTpl(kebabName, pascalName, camelName)
|
|
163
178
|
);
|
|
164
179
|
}
|
|
165
180
|
|
|
@@ -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
|
+
);
|
|
@@ -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));
|
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { removeCommand } from './commands/remove.js';
|
|
|
11
11
|
import { doctorCommand } from './commands/doctor.js';
|
|
12
12
|
import { updateCommand } from './commands/update.js';
|
|
13
13
|
import { completionCommand } from './commands/completion.js';
|
|
14
|
+
import { scaffoldCommand } from './commands/scaffold.js';
|
|
15
|
+
import { templatesCommand } from './commands/templates.js';
|
|
14
16
|
|
|
15
17
|
const program = new Command();
|
|
16
18
|
|
|
@@ -49,4 +51,10 @@ program.addCommand(updateCommand);
|
|
|
49
51
|
// Shell completion
|
|
50
52
|
program.addCommand(completionCommand);
|
|
51
53
|
|
|
54
|
+
// Scaffold resource
|
|
55
|
+
program.addCommand(scaffoldCommand);
|
|
56
|
+
|
|
57
|
+
// Template management
|
|
58
|
+
program.addCommand(templatesCommand);
|
|
59
|
+
|
|
52
60
|
program.parse();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getProjectRoot } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Template loader that checks for custom templates in priority order:
|
|
7
|
+
* 1. Project templates (.servcraft/templates/)
|
|
8
|
+
* 2. User templates (~/.servcraft/templates/)
|
|
9
|
+
* 3. Built-in templates (fallback)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type TemplateType =
|
|
13
|
+
| 'controller'
|
|
14
|
+
| 'service'
|
|
15
|
+
| 'repository'
|
|
16
|
+
| 'types'
|
|
17
|
+
| 'schemas'
|
|
18
|
+
| 'routes'
|
|
19
|
+
| 'module-index'
|
|
20
|
+
| 'controller-test'
|
|
21
|
+
| 'service-test'
|
|
22
|
+
| 'integration-test';
|
|
23
|
+
|
|
24
|
+
interface TemplateFunction {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
[key: string]: (...args: any[]) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load a custom template if available, otherwise return null
|
|
31
|
+
*/
|
|
32
|
+
export async function loadCustomTemplate(
|
|
33
|
+
templateType: TemplateType
|
|
34
|
+
): Promise<TemplateFunction | null> {
|
|
35
|
+
const projectRoot = getProjectRoot();
|
|
36
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
37
|
+
|
|
38
|
+
const locations = [
|
|
39
|
+
path.join(projectRoot, '.servcraft', 'templates', `${templateType}.ts`),
|
|
40
|
+
path.join(homeDir, '.servcraft', 'templates', `${templateType}.ts`),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const location of locations) {
|
|
44
|
+
try {
|
|
45
|
+
await fs.access(location);
|
|
46
|
+
// Template file exists, dynamically import it
|
|
47
|
+
const templateModule = (await import(`file://${location}`)) as TemplateFunction;
|
|
48
|
+
|
|
49
|
+
// Look for the template function (e.g., controllerTemplate, serviceTemplate)
|
|
50
|
+
const functionName = `${templateType.replace(/-/g, '')}Template`;
|
|
51
|
+
|
|
52
|
+
if (templateModule[functionName]) {
|
|
53
|
+
return templateModule;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// File doesn't exist or import failed, try next location
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get template function with fallback to built-in
|
|
66
|
+
*/
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
export async function getTemplate<T extends (...args: any[]) => string>(
|
|
69
|
+
templateType: TemplateType,
|
|
70
|
+
builtInTemplate: T
|
|
71
|
+
): Promise<T> {
|
|
72
|
+
const customTemplate = await loadCustomTemplate(templateType);
|
|
73
|
+
|
|
74
|
+
if (customTemplate) {
|
|
75
|
+
const functionName = `${templateType.replace(/-/g, '')}Template`;
|
|
76
|
+
return customTemplate[functionName] as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return builtInTemplate;
|
|
80
|
+
}
|
|
@@ -2,7 +2,24 @@ import { prisma } from '../../database/prisma.js';
|
|
|
2
2
|
import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
3
3
|
import { createPaginatedResult, getSkip } from '../../utils/pagination.js';
|
|
4
4
|
import type { User, CreateUserData, UpdateUserData, UserFilters } from './types.js';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// Use string literal enums for ESM/CommonJS compatibility
|
|
7
|
+
const UserRole = {
|
|
8
|
+
USER: 'USER',
|
|
9
|
+
MODERATOR: 'MODERATOR',
|
|
10
|
+
ADMIN: 'ADMIN',
|
|
11
|
+
SUPER_ADMIN: 'SUPER_ADMIN',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
const UserStatus = {
|
|
15
|
+
ACTIVE: 'ACTIVE',
|
|
16
|
+
INACTIVE: 'INACTIVE',
|
|
17
|
+
SUSPENDED: 'SUSPENDED',
|
|
18
|
+
BANNED: 'BANNED',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
type UserRole = (typeof UserRole)[keyof typeof UserRole];
|
|
22
|
+
type UserStatus = (typeof UserStatus)[keyof typeof UserStatus];
|
|
6
23
|
|
|
7
24
|
/**
|
|
8
25
|
* User Repository - Prisma Implementation
|